Anton Tarasenko
2 years ago
41 changed files with 6098 additions and 4330 deletions
@ -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" |
@ -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" |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class ACommandFakers extends Command |
||||
dependsOn(VotingModel); |
||||
|
||||
var private array<UserID> 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 { |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<Voting> 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 { |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<EPlayer> 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 "<command> <sub_command>"). |
||||
// |
||||
// 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<Parameter> required; |
||||
var array<Parameter> 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<Parameter> required; |
||||
var array<Parameter> 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<SubCommand> subCommands; |
||||
var protected array<Option> options; |
||||
var protected bool requiresTarget; |
||||
}; |
||||
var protected Data commandData; |
||||
|
||||
// We do not really ever need to create more than one instance of each class |
||||
// of `Command`, so we will simply store and reuse one created instance. |
||||
var private Command mainInstance; |
||||
|
||||
|
||||
// When command is being executed we create several instances of `ConsoleWriter` that can be used |
||||
// for command output. They will also be automatically deallocated once command is executed. |
||||
// |
||||
// DO NOT modify them or deallocate any of them manually. |
||||
// |
||||
// This should make output more convenient and standardized. |
||||
// |
||||
// 1. `publicConsole` - sends messages to all present players; |
||||
// 2. `callerConsole` - sends messages to the player that called the command; |
||||
// 3. `targetConsole` - sends messages to the player that is currently being targeted |
||||
// (different each call of `ExecutedFor()` and `none` during `Executed()` call); |
||||
// 4. `othersConsole` - sends messaged to every player that is neither "caller" or "target". |
||||
var protected ConsoleWriter publicConsole, othersConsole; |
||||
var protected ConsoleWriter callerConsole, targetConsole; |
||||
|
||||
protected function Constructor() { |
||||
local CommandDataBuilder dataBuilder; |
||||
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
||||
BuildData(dataBuilder); |
||||
commandData = dataBuilder.BorrowData(); |
||||
dataBuilder.FreeSelf(); |
||||
dataBuilder = none; |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
local int i; |
||||
local array<SubCommand> subCommands; |
||||
local array<Option> options; |
||||
|
||||
DeallocateConsoles(); |
||||
_.memory.Free(commandData.name); |
||||
_.memory.Free(commandData.summary); |
||||
subCommands = commandData.subCommands; |
||||
for (i = 0; i < options.length; i += 1) { |
||||
_.memory.Free(subCommands[i].name); |
||||
_.memory.Free(subCommands[i].description); |
||||
CleanParameters(subCommands[i].required); |
||||
CleanParameters(subCommands[i].optional); |
||||
subCommands[i].required.length = 0; |
||||
subCommands[i].optional.length = 0; |
||||
} |
||||
commandData.subCommands.length = 0; |
||||
options = commandData.options; |
||||
for (i = 0; i < options.length; i += 1) { |
||||
_.memory.Free(options[i].longName); |
||||
_.memory.Free(options[i].description); |
||||
CleanParameters(options[i].required); |
||||
CleanParameters(options[i].optional); |
||||
options[i].required.length = 0; |
||||
options[i].optional.length = 0; |
||||
} |
||||
commandData.options.length = 0; |
||||
} |
||||
|
||||
private final function CleanParameters(array<Parameter> parameters) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < parameters.length; i += 1) { |
||||
_.memory.Free(parameters[i].displayName); |
||||
_.memory.Free(parameters[i].variableName); |
||||
_.memory.Free(parameters[i].aliasSourceName); |
||||
} |
||||
} |
||||
|
||||
/// Overload this method to use `builder` to define parameters and options for your command. |
||||
protected function BuildData(CommandDataBuilder builder){} |
||||
|
||||
/// Overload this method to perform required actions when your command is called. |
||||
/// |
||||
/// [`arguments`] is a `struct` filled with parameters that your command has been called with. |
||||
/// Guaranteed to not be in error state. |
||||
/// |
||||
/// [`instigator`] is a player that instigated this execution. |
||||
protected function Executed(CallData arguments, EPlayer instigator){} |
||||
|
||||
/// Overload this method to perform required actions when your command is called with a given player |
||||
/// as a target. |
||||
/// |
||||
/// If several players have been specified - this method will be called once for each. |
||||
/// |
||||
/// If your command does not require a target - this method will not be called. |
||||
/// |
||||
/// [`target`] is a player that this command must perform an action on. |
||||
/// [`arguments`] is a `struct` filled with parameters that your command has been called with. |
||||
/// Guaranteed to not be in error state. |
||||
/// |
||||
/// [`instigator`] is a player that instigated this execution. |
||||
protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) {} |
||||
|
||||
/// Returns an instance of command (of particular class) that is stored "as a singleton" in |
||||
/// command's class itself. Do not deallocate it. |
||||
public final static function Command GetInstance() { |
||||
if (default.mainInstance == none) { |
||||
default.mainInstance = Command(__().memory.Allocate(default.class)); |
||||
} |
||||
return default.mainInstance; |
||||
} |
||||
|
||||
/// Forces command to process (parse) player's input, producing a structure with parsed data in |
||||
/// Acedia's format instead. |
||||
/// |
||||
/// Use `Execute()` for actually performing command's actions. |
||||
/// |
||||
/// [`subCommandName`] can be optionally specified to use as sub-command. |
||||
/// If this argument's value is `none` - sub-command name will be parsed from the `parser`'s data. |
||||
/// |
||||
/// Returns `CallData` structure that contains all the information about parameters specified in |
||||
/// `parser`'s contents. |
||||
/// Returned structure contains objects that must be deallocated, which can easily be done by |
||||
/// the auxiliary `DeallocateCallData()` method. |
||||
public final function CallData ParseInputWith( |
||||
Parser parser, |
||||
EPlayer callerPlayer, |
||||
optional BaseText subCommandName |
||||
) { |
||||
local array<EPlayer> targetPlayers; |
||||
local CommandParser commandParser; |
||||
local CallData callData; |
||||
|
||||
if (parser == none || !parser.Ok()) { |
||||
callData.parsingError = CET_BadParser; |
||||
return callData; |
||||
} |
||||
// Parse targets and handle errors that can arise here |
||||
if (commandData.requiresTarget) { |
||||
targetPlayers = ParseTargets(parser, callerPlayer); |
||||
if (!parser.Ok()) { |
||||
callData.parsingError = CET_IncorrectTargetList; |
||||
return callData; |
||||
} |
||||
if (targetPlayers.length <= 0) { |
||||
callData.parsingError = CET_EmptyTargetList; |
||||
return callData; |
||||
} |
||||
} |
||||
// Parse parameters themselves |
||||
commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); |
||||
callData = commandParser.ParseWith( |
||||
parser, |
||||
commandData, |
||||
callerPlayer, |
||||
subCommandName); |
||||
callData.targetPlayers = targetPlayers; |
||||
commandParser.FreeSelf(); |
||||
return callData; |
||||
} |
||||
|
||||
/// Executes caller `Command` with data provided by `callData` if it is in a correct state and |
||||
/// reports error to `callerPlayer` if `callData` is invalid. |
||||
/// |
||||
/// Returns `true` if command was successfully executed and `false` otherwise. |
||||
/// Execution is considered successful if `Execute()` call was made, regardless of whether `Command` |
||||
/// can actually perform required action. |
||||
/// For example, giving a weapon to a player can fail because he does not have enough space in his |
||||
/// inventory, but it will still be considered a successful execution as far as return value is |
||||
/// concerned. |
||||
public final function bool Execute(CallData callData, EPlayer callerPlayer) { |
||||
local int i; |
||||
local array<EPlayer> targetPlayers; |
||||
|
||||
if (callerPlayer == none) return false; |
||||
if (!callerPlayer.IsExistent()) return false; |
||||
|
||||
// Report or execute |
||||
if (callData.parsingError != CET_None) { |
||||
ReportError(callData, callerPlayer); |
||||
return false; |
||||
} |
||||
targetPlayers = callData.targetPlayers; |
||||
publicConsole = _.console.ForAll(); |
||||
callerConsole = _.console.For(callerPlayer); |
||||
callerConsole |
||||
.Write(P("Executing command `")) |
||||
.Write(commandData.name) |
||||
.Say(P("`")); |
||||
// `othersConsole` should also exist in time for `Executed()` call |
||||
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); |
||||
Executed(callData, callerPlayer); |
||||
_.memory.Free(othersConsole); |
||||
if (commandData.requiresTarget) { |
||||
for (i = 0; i < targetPlayers.length; i += 1) { |
||||
targetConsole = _.console.For(targetPlayers[i]); |
||||
othersConsole = _.console |
||||
.ForAll() |
||||
.ButPlayer(callerPlayer) |
||||
.ButPlayer(targetPlayers[i]); |
||||
ExecutedFor(targetPlayers[i], callData, callerPlayer); |
||||
_.memory.Free(othersConsole); |
||||
_.memory.Free(targetConsole); |
||||
} |
||||
} |
||||
othersConsole = none; |
||||
targetConsole = none; |
||||
DeallocateConsoles(); |
||||
return true; |
||||
} |
||||
|
||||
private final function DeallocateConsoles() { |
||||
if (publicConsole != none && publicConsole.IsAllocated()) { |
||||
_.memory.Free(publicConsole); |
||||
} |
||||
if (callerConsole != none && callerConsole.IsAllocated()) { |
||||
_.memory.Free(callerConsole); |
||||
} |
||||
if (targetConsole != none && targetConsole.IsAllocated()) { |
||||
_.memory.Free(targetConsole); |
||||
} |
||||
if (othersConsole != none && othersConsole.IsAllocated()) { |
||||
_.memory.Free(othersConsole); |
||||
} |
||||
publicConsole = none; |
||||
callerConsole = none; |
||||
targetConsole = none; |
||||
othersConsole = none; |
||||
} |
||||
|
||||
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure. |
||||
public final static function DeallocateCallData(/* take */ CallData callData) { |
||||
__().memory.Free(callData.subCommandName); |
||||
__().memory.Free(callData.parameters); |
||||
__().memory.Free(callData.options); |
||||
__().memory.Free(callData.errorCause); |
||||
__().memory.FreeMany(callData.targetPlayers); |
||||
if (callData.targetPlayers.length > 0) { |
||||
callData.targetPlayers.length = 0; |
||||
} |
||||
} |
||||
|
||||
// Reports given error to the `callerPlayer`, appropriately picking |
||||
// message color |
||||
private final function ReportError(CallData callData, EPlayer callerPlayer) { |
||||
local Text errorMessage; |
||||
local ConsoleWriter console; |
||||
|
||||
if (callerPlayer == none) return; |
||||
if (!callerPlayer.IsExistent()) return; |
||||
|
||||
// Setup console color |
||||
console = callerPlayer.BorrowConsole(); |
||||
if (callData.parsingError == CET_EmptyTargetList) { |
||||
console.UseColor(_.color.textWarning); |
||||
} else { |
||||
console.UseColor(_.color.textFailure); |
||||
} |
||||
// Send message |
||||
errorMessage = PrintErrorMessage(callData); |
||||
console.Say(errorMessage); |
||||
errorMessage.FreeSelf(); |
||||
// Restore console color |
||||
console.ResetColor().Flush(); |
||||
} |
||||
|
||||
private final function Text PrintErrorMessage(CallData callData) { |
||||
local Text result; |
||||
local MutableText builder; |
||||
|
||||
builder = _.text.Empty(); |
||||
switch (callData.parsingError) { |
||||
case CET_BadParser: |
||||
builder.Append(P("Internal error occurred: invalid parser")); |
||||
break; |
||||
case CET_NoSubCommands: |
||||
builder.Append(P("Ill defined command: no subcommands")); |
||||
break; |
||||
case CET_BadSubCommand: |
||||
builder |
||||
.Append(P("Ill defined sub-command: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_NoRequiredParam: |
||||
builder |
||||
.Append(P("Missing required parameter: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_NoRequiredParamForOption: |
||||
builder |
||||
.Append(P("Missing required parameter for option: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnknownOption: |
||||
builder |
||||
.Append(P("Invalid option specified: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnknownShortOption: |
||||
builder.Append(P("Invalid short option specified")); |
||||
break; |
||||
case CET_RepeatedOption: |
||||
builder |
||||
.Append(P("Option specified several times: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnusedCommandParameters: |
||||
builder.Append(P("Part of command could not be parsed: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_MultipleOptionsWithParams: |
||||
builder |
||||
.Append(P("Multiple short options in one declarations require parameters: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_IncorrectTargetList: |
||||
builder |
||||
.Append(P("Target players are incorrectly specified.")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_EmptyTargetList: |
||||
builder |
||||
.Append(P("List of target players is empty")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
default: |
||||
} |
||||
result = builder.Copy(); |
||||
builder.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
// Auxiliary method for parsing list of targeted players. |
||||
// Assumes given parser is not `none` and not in a failed state. |
||||
// If parsing failed, guaranteed to return an empty array. |
||||
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) { |
||||
local array<EPlayer> targetPlayers; |
||||
local PlayersParser targetsParser; |
||||
|
||||
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); |
||||
targetsParser.SetSelf(callerPlayer); |
||||
targetsParser.ParseWith(parser); |
||||
if (parser.Ok()) { |
||||
targetPlayers = targetsParser.GetPlayers(); |
||||
} |
||||
targetsParser.FreeSelf(); |
||||
return targetPlayers; |
||||
} |
||||
|
||||
/// Returns name (in lower case) of the caller command class. |
||||
public final function Text GetName() { |
||||
if (commandData.name == none) { |
||||
return P("").Copy(); |
||||
} |
||||
return commandData.name.LowerCopy(); |
||||
} |
||||
|
||||
/// Returns group name (in lower case) of the caller command class. |
||||
public final function Text GetGroupName() { |
||||
if (commandData.group == none) { |
||||
return P("").Copy(); |
||||
} |
||||
return commandData.group.LowerCopy(); |
||||
} |
||||
|
||||
/// Returns `Command.Data` struct that describes caller `Command`. |
||||
/// |
||||
/// Returned struct contains `Text` references that are used internally by the `Command` and |
||||
/// not their copies. |
||||
/// |
||||
/// Generally this is undesired approach and leaves `Command` more vulnerable to modification, |
||||
/// but copying all the data inside would not only introduce a largely pointless computational |
||||
/// overhead, but also would require some cumbersome logic. |
||||
/// This might change in the future, so deallocating any objects in the returned `struct` would lead |
||||
/// to undefined behavior. |
||||
public final function Data BorrowData() { |
||||
return commandData; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,228 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class CommandAPI extends AcediaObject; |
||||
|
||||
// Classes registered to be registered in async way |
||||
var private array< class<Command> > pendingClasses; |
||||
// Job that is supposed to register pending commands |
||||
var private CommandRegistrationJob registeringJob; |
||||
|
||||
// Saves `HashTable` with command locks. |
||||
// Locks are simply boolean switches that mark for commands whether they can be executed. |
||||
// |
||||
// Lock is considered "unlocked" if this `HashTable` stores `true` at the key with its name |
||||
// and `false` otherwise. |
||||
var private HashTable commandLocks; |
||||
|
||||
var private Commands_Feature commandsFeature; |
||||
|
||||
// DO NOT CALL MANUALLY |
||||
public final /*internal*/ function class<Command> _popPending() { |
||||
local class<Command> 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; |
||||
} |
||||
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()`. |
||||
/// |
||||
/// Unless you need command right now, it is recommended to use `RegisterCommandAsync()` instead. |
||||
/// |
||||
/// 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<Command> commandClass) { |
||||
if (commandsFeature != none) { |
||||
return commandsFeature.RegisterCommand(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<Command> 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 |
||||
/// circumstances and it is not efficient. |
||||
/// It is linear on the current amount of commands. |
||||
public final function RemoveCommand(class<Command> 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); |
||||
} |
||||
} |
||||
|
||||
/// Closes a command lock with a given case-insensitive name. |
||||
/// |
||||
/// Command locks are basically just boolean values that commands can use to check for whether they |
||||
/// are allowed to perform certain actions (e.g. cheats). |
||||
public final function bool Lock(BaseText lockName) { |
||||
local Text lowerCaseName; |
||||
|
||||
if (lockName == none) return false; |
||||
if (commandsFeature == none) return false; |
||||
|
||||
if (commandLocks == none) { |
||||
commandLocks = _.collections.EmptyHashTable(); |
||||
} |
||||
lowerCaseName = lockName.LowerCopy(); |
||||
commandLocks.SetBool(lowerCaseName, true); |
||||
lowerCaseName.FreeSelf(); |
||||
return true; |
||||
} |
||||
|
||||
/// Opens a command lock with a given case-insensitive name. |
||||
/// |
||||
/// Command locks are basically just boolean values that commands can use to check for whether they |
||||
/// are allowed to perform certain actions (e.g. cheats). |
||||
public final function bool Unlock(BaseText lockName) { |
||||
local Text lowerCaseName; |
||||
|
||||
if (lockName == none) return false; |
||||
if (commandsFeature == none) return false; |
||||
|
||||
if (commandLocks == none) { |
||||
commandLocks = _.collections.EmptyHashTable(); |
||||
} |
||||
lowerCaseName = lockName.LowerCopy(); |
||||
commandLocks.SetBool(lowerCaseName, false); |
||||
lowerCaseName.FreeSelf(); |
||||
return true; |
||||
} |
||||
|
||||
/// Checks if a command lock with a given case-insensitive name is closed. |
||||
/// |
||||
/// Command locks are basically just boolean values that commands can use to check for whether they |
||||
/// are allowed to perform certain actions (e.g. cheats). |
||||
public final function bool IsLocked(BaseText lockName) { |
||||
local bool result; |
||||
local Text lowerCaseName; |
||||
|
||||
if (lockName == none) return true; |
||||
if (commandsFeature == none) return true; |
||||
if (commandLocks == none) return true; |
||||
|
||||
lowerCaseName = lockName.LowerCopy(); |
||||
result = commandLocks.GetBool(lowerCaseName); |
||||
lowerCaseName.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
/// Closes all command locks. |
||||
/// |
||||
/// Command locks are basically just boolean values that commands can use to check for whether they |
||||
/// are allowed to perform certain actions (e.g. cheats). |
||||
public final function CloseAllLocks() { |
||||
_.memory.Free(commandLocks); |
||||
commandLocks = none; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,910 @@
|
||||
/** |
||||
* 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; |
||||
} |
||||
|
||||
// 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; |
||||
} |
||||
|
||||
/// 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 name of the command that caller [`CommandDataBuilder`] constructs. |
||||
public final function Name(BaseText newName) { |
||||
if (newName != none && newName == commandName) { |
||||
return; |
||||
} |
||||
_.memory.Free(commandName); |
||||
if (newName != none) { |
||||
commandName = newName.Copy(); |
||||
} else { |
||||
commandName = none; |
||||
} |
||||
} |
||||
|
||||
/// 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; |
||||
} |
||||
_.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 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; |
||||
|
||||
RecordSelection(); |
||||
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) { |
||||
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`. |
||||
/// |
||||
/// [`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)); |
||||
} |
||||
} |
||||
|
||||
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.") |
||||
} |
@ -0,0 +1,958 @@
|
||||
/** |
||||
* 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 CommandParser extends AcediaObject |
||||
dependson(Command); |
||||
|
||||
|
||||
//! 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". |
||||
// |
||||
// 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. |
||||
CPT_LastNecessaryParameter, |
||||
// We are not parsing extra parameters that can be safely omitted. |
||||
CPT_ExtraParameter, |
||||
}; |
||||
|
||||
// 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<Command.Option> 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 |
||||
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 |
||||
var private bool currentTargetIsOption; |
||||
// 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`. |
||||
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. |
||||
var private array<Command.Option> usedOptions; |
||||
|
||||
// Literals that can be used as boolean values |
||||
var private array<string> booleanTrueEquivalents; |
||||
var private array<string> booleanFalseEquivalents; |
||||
|
||||
var LoggerAPI.Definition errNoSubCommands; |
||||
|
||||
protected function Finalizer() { |
||||
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; |
||||
} |
||||
|
||||
// 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(); |
||||
} |
||||
if (commandParser != none) { |
||||
commandParser.Fail(); |
||||
} |
||||
} |
||||
|
||||
// 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<Command.SubCommand> allSubCommands; |
||||
|
||||
allSubCommands = commandData.subCommands; |
||||
if (allSubcommands.length == 0) { |
||||
_.logger.Auto(errNoSubCommands).ArgClass(class); |
||||
pickedSubCommand = emptySubCommand; |
||||
return; |
||||
} |
||||
// Get candidate name |
||||
confirmedState = commandParser.GetCurrentState(); |
||||
if (specifiedSubCommand != none) { |
||||
candidateSubCommandName = specifiedSubCommand.MutableCopy(); |
||||
} else { |
||||
commandParser.Skip().MUntil(candidateSubCommandName,, true); |
||||
} |
||||
// Try matching it to sub commands |
||||
pickedSubCommand = allSubcommands[0]; |
||||
if (candidateSubCommandName.IsEmpty()) { |
||||
candidateSubCommandName.FreeSelf(); |
||||
return; |
||||
} |
||||
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. |
||||
commandParser.RestoreState(confirmedState); |
||||
} |
||||
|
||||
/// 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, |
||||
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()) { |
||||
DeclareError(CET_BadParser, none); |
||||
toReturn = nextResult; |
||||
Reset(); |
||||
return toReturn; |
||||
} |
||||
commandParser = parser; |
||||
availableOptions = commandData.options; |
||||
currentPlayersParser = |
||||
PlayersParser(_.memory.Allocate(class'PlayersParser')); |
||||
currentPlayersParser.SetSelf(callerPlayer); |
||||
// (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 |
||||
if (commandParser.Ok()) { |
||||
nextResult.parameters = commandParameters; |
||||
} else { |
||||
_.memory.Free(commandParameters); |
||||
} |
||||
// 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() { |
||||
local Text remainder; |
||||
|
||||
if (!commandParser.Ok()) return; |
||||
if (commandParser.Skip().GetRemainingLength() <= 0) return; |
||||
|
||||
remainder = commandParser.GetRemainder(); |
||||
DeclareError(CET_UnusedCommandParameters, remainder); |
||||
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. |
||||
private final function HashTable ParseParameterArrays( |
||||
array<Command.Parameter> requiredParameters, |
||||
array<Command.Parameter> optionalParameters |
||||
) { |
||||
local HashTable parsedParameters; |
||||
|
||||
if (!commandParser.Ok()) { |
||||
return none; |
||||
} |
||||
parsedParameters = _.collections.EmptyHashTable(); |
||||
// Parse parameters |
||||
ParseRequiredParameterArray(parsedParameters, requiredParameters); |
||||
ParseOptionalParameterArray(parsedParameters, optionalParameters); |
||||
// 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`. |
||||
private final function ParseRequiredParameterArray( |
||||
HashTable parsedParameters, |
||||
array<Command.Parameter> requiredParameters |
||||
) { |
||||
local int i; |
||||
|
||||
if (!commandParser.Ok()) { |
||||
return; |
||||
} |
||||
currentTarget = CPT_NecessaryParameter; |
||||
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) { |
||||
DeclareError( CET_NoRequiredParamForOption, |
||||
targetOption.longName); |
||||
} else { |
||||
DeclareError( CET_NoRequiredParam, |
||||
requiredParameters[i].displayName); |
||||
} |
||||
return; |
||||
} |
||||
i += 1; |
||||
} |
||||
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. |
||||
private final function ParseOptionalParameterArray( |
||||
HashTable parsedParameters, |
||||
array<Command.Parameter> optionalParameters |
||||
) { |
||||
local int i; |
||||
|
||||
if (!commandParser.Ok()) { |
||||
return; |
||||
} |
||||
while (i < optionalParameters.length) { |
||||
confirmedState = commandParser.GetCurrentState(); |
||||
// 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 |
||||
commandParser.RestoreState(confirmedState); |
||||
break; |
||||
} |
||||
i += 1; |
||||
} |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local bool parsedEnough; |
||||
|
||||
confirmedState = commandParser.GetCurrentState(); |
||||
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 |
||||
if (!expectedParameter.allowsList) { |
||||
return true; |
||||
} |
||||
if (commandParser.Skip().HasFinished()) { |
||||
return true; |
||||
} |
||||
confirmedState = commandParser.GetCurrentState(); |
||||
} |
||||
// 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 |
||||
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`. |
||||
// |
||||
// 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 |
||||
if (expectedParameter.type == CPT_Remainder) { |
||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
||||
} |
||||
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 |
||||
if (expectedParameter.type == CPT_Remainder) { |
||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
||||
} |
||||
// Propagate errors after parsing options |
||||
if (nextResult.parsingError != CET_None) { |
||||
return false; |
||||
} |
||||
// Try parsing one of the variable types |
||||
if (expectedParameter.type == CPT_Boolean) { |
||||
return ParseBooleanValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Integer) { |
||||
return ParseIntegerValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Number) { |
||||
return ParseNumberValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Text) { |
||||
return ParseTextValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Remainder) { |
||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Object) { |
||||
return ParseObjectValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_Array) { |
||||
return ParseArrayValue(parsedParameters, expectedParameter); |
||||
} else if (expectedParameter.type == CPT_JSON) { |
||||
return ParseJSONValue(parsedParameters, expectedParameter); |
||||
} 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. |
||||
private final function bool ParseBooleanValue( |
||||
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()) { |
||||
_.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; |
||||
break; |
||||
} |
||||
} |
||||
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)); |
||||
return true; |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local int integerValue; |
||||
|
||||
commandParser.Skip().MInteger(integerValue); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
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. |
||||
private final function bool ParseNumberValue( |
||||
HashTable parsedParameters, |
||||
Command.Parameter expectedParameter |
||||
) { |
||||
local float numberValue; |
||||
|
||||
commandParser.Skip().MNumber(numberValue); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
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. |
||||
private final function bool ParseTextValue( |
||||
HashTable parsedParameters, |
||||
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) |
||||
initialState = commandParser.Skip().GetCurrentState(); |
||||
// 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) { |
||||
_.memory.Free(textValue); |
||||
commandParser.RestoreState(initialState).MString(textValue); |
||||
failedParsing = (!commandParser.Ok() || textValue.IsEmpty()); |
||||
} |
||||
if (failedParsing) { |
||||
_.memory.Free(textValue); |
||||
commandParser.Fail(); |
||||
return false; |
||||
} |
||||
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 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 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); |
||||
} else if (aliasSourceName.Compare(P("color"))) { |
||||
resolvedValue = _.alias.ResolveColor(textValue, true); |
||||
} else if (aliasSourceName.Compare(P("feature"))) { |
||||
resolvedValue = _.alias.ResolveFeature(textValue, true); |
||||
} else if (aliasSourceName.Compare(P("entity"))) { |
||||
resolvedValue = _.alias.ResolveEntity(textValue, true); |
||||
} else { |
||||
resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true); |
||||
} |
||||
result.SetItem(P("value"), resolvedValue); |
||||
_.memory.Free2(resolvedValue, immutableValue); |
||||
return result; |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local MutableText value; |
||||
|
||||
commandParser.Skip().MUntil(value); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
RecordParameter(parsedParameters, expectedParameter, value.IntoText()); |
||||
return true; |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local HashTable objectValue; |
||||
|
||||
objectValue = _.json.ParseHashTableWith(commandParser); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
RecordParameter(parsedParameters, expectedParameter, objectValue); |
||||
return true; |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local ArrayList arrayValue; |
||||
|
||||
arrayValue = _.json.ParseArrayListWith(commandParser); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
RecordParameter(parsedParameters, expectedParameter, arrayValue); |
||||
return true; |
||||
} |
||||
|
||||
// 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 |
||||
) { |
||||
local AcediaObject jsonValue; |
||||
|
||||
jsonValue = _.json.ParseWith(commandParser); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
RecordParameter(parsedParameters, expectedParameter, jsonValue); |
||||
return true; |
||||
} |
||||
|
||||
// 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) |
||||
{ |
||||
local ArrayList resultPlayerList; |
||||
local array<EPlayer> targetPlayers; |
||||
|
||||
currentPlayersParser.ParseWith(commandParser); |
||||
if (commandParser.Ok()) { |
||||
targetPlayers = currentPlayersParser.GetPlayers(); |
||||
} else { |
||||
return false; |
||||
} |
||||
resultPlayerList = _.collections.NewArrayList(targetPlayers); |
||||
_.memory.FreeMany(targetPlayers); |
||||
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`. |
||||
private final function RecordParameter( |
||||
HashTable parametersArray, |
||||
Command.Parameter parameter, |
||||
/*take*/ AcediaObject value |
||||
) { |
||||
local ArrayList parameterVariable; |
||||
|
||||
if (!parameter.allowsList) { |
||||
parametersArray.SetItem(parameter.variableName, value); |
||||
_.memory.Free(value); |
||||
return; |
||||
} |
||||
parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName)); |
||||
if (parameterVariable == none) { |
||||
parameterVariable = _.collections.EmptyArrayList(); |
||||
} |
||||
parameterVariable.AddItem(value); |
||||
_.memory.Free(value); |
||||
parametersArray.SetItem(parameter.variableName, parameterVariable); |
||||
_.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() { |
||||
local int temporaryInt; |
||||
|
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
confirmedState = commandParser.GetCurrentState(); |
||||
// 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()) { |
||||
commandParser.RestoreState(confirmedState); |
||||
return false; |
||||
} |
||||
commandParser.RestoreState(confirmedState).Skip().Match(P("-.")); |
||||
if (commandParser.Ok()) { |
||||
commandParser.RestoreState(confirmedState); |
||||
return false; |
||||
} |
||||
// Short options |
||||
commandParser.RestoreState(confirmedState).Skip().Match(P("-")); |
||||
if (commandParser.Ok()) { |
||||
return ParseShortOption(); |
||||
} |
||||
commandParser.RestoreState(confirmedState); |
||||
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; |
||||
|
||||
commandParser.MUntil(optionName,, true); |
||||
if (!commandParser.Ok()) { |
||||
return false; |
||||
} |
||||
while (optionIndex < availableOptions.length) { |
||||
if (optionName.Compare(availableOptions[optionIndex].longName)) break; |
||||
optionIndex += 1; |
||||
} |
||||
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)) { |
||||
DeclareError(CET_RepeatedOption, optionName); |
||||
optionName.FreeSelf(); |
||||
return false; |
||||
} |
||||
} |
||||
//usedOptions[usedOptions.length] = availableOptions[optionIndex]; |
||||
optionName.FreeSelf(); |
||||
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`). |
||||
private final function bool ParseShortOption() |
||||
{ |
||||
local int i; |
||||
local bool pickedOptionWithParameters; |
||||
local MutableText optionsList; |
||||
|
||||
commandParser.MUntil(optionsList,, true); |
||||
if (!commandParser.Ok()) { |
||||
optionsList.FreeSelf(); |
||||
return false; |
||||
} |
||||
for (i = 0; i < optionsList.GetLength(); i += 1) { |
||||
if (nextResult.parsingError != CET_None) break; |
||||
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. |
||||
private final function bool AddOptionByCharacter( |
||||
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)) { |
||||
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 (!_.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) { |
||||
DeclareError(CET_MultipleOptionsWithParams, optionSourceList); |
||||
return optionHasParameters; |
||||
} |
||||
// Parse parameters (even if they are empty) and bail |
||||
commandParser.Skip(); |
||||
ParseOptionParameters(availableOptions[i]); |
||||
break; |
||||
} |
||||
if (i >= availableOptions.length) { |
||||
DeclareError(CET_UnknownShortOption); |
||||
} |
||||
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) { |
||||
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) { |
||||
DeclareError(CET_NoRequiredParamForOption, targetOption.longName); |
||||
return false; |
||||
} |
||||
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 = false; |
||||
if (commandParser.Ok()) { |
||||
nextResult.options.SetItem(pickedOption.longName, optionParameters); |
||||
_.memory.Free(optionParameters); |
||||
return true; |
||||
} |
||||
_.memory.Free(optionParameters); |
||||
return false; |
||||
} |
||||
|
||||
defaultproperties { |
||||
booleanTrueEquivalents(0) = "true" |
||||
booleanTrueEquivalents(1) = "enable" |
||||
booleanTrueEquivalents(2) = "on" |
||||
booleanTrueEquivalents(3) = "yes" |
||||
booleanFalseEquivalents(0) = "false" |
||||
booleanFalseEquivalents(1) = "disable" |
||||
booleanFalseEquivalents(2) = "off" |
||||
booleanFalseEquivalents(3) = "no" |
||||
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.") |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class CommandRegistrationJob extends SchedulerJob; |
||||
|
||||
var private class<Command> 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 |
||||
{ |
||||
} |
@ -0,0 +1,60 @@
|
||||
/** |
||||
* 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 Commands extends FeatureConfig |
||||
perobjectconfig |
||||
config(AcediaSystem); |
||||
|
||||
var public config bool useChatInput; |
||||
var public config bool useMutateInput; |
||||
var public config string chatCommandPrefix; |
||||
|
||||
protected function HashTable ToData() { |
||||
local HashTable data; |
||||
|
||||
data = __().collections.EmptyHashTable(); |
||||
data.SetBool(P("useChatInput"), useChatInput, true); |
||||
data.SetBool(P("useMutateInput"), useMutateInput, true); |
||||
data.SetString(P("chatCommandPrefix"), chatCommandPrefix); |
||||
return data; |
||||
} |
||||
|
||||
protected function FromData(HashTable source) { |
||||
if (source == none) { |
||||
return; |
||||
} |
||||
useChatInput = source.GetBool(P("useChatInput")); |
||||
useMutateInput = source.GetBool(P("useMutateInput")); |
||||
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); |
||||
} |
||||
|
||||
protected function DefaultIt() { |
||||
useChatInput = true; |
||||
useMutateInput = true; |
||||
chatCommandPrefix = "!"; |
||||
} |
||||
|
||||
defaultproperties { |
||||
configName = "AcediaSystem" |
||||
useChatInput = true |
||||
useMutateInput = true |
||||
chatCommandPrefix = "!" |
||||
} |
@ -0,0 +1,719 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class Commands_Feature extends Feature; |
||||
|
||||
//! 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. |
||||
|
||||
/// Pairs [`Voting`] class with a name its registered under in lower case for quick search. |
||||
struct NamedVoting { |
||||
var public class<Voting> processClass; |
||||
/// Must be guaranteed to not be `none` and lower case as an invariant |
||||
var public Text processName; |
||||
}; |
||||
|
||||
/// 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; |
||||
}; |
||||
|
||||
/// Describes possible outcomes of starting a voting by its name |
||||
enum StartVotingResult { |
||||
/// Voting was successfully started |
||||
SVR_Success, |
||||
/// Voting wasn't started because another one was still in progress |
||||
SVR_AlreadyInProgress, |
||||
/// Voting wasn't started because voting with that name hasn't been registered |
||||
SVR_UnknownVoting |
||||
}; |
||||
|
||||
/// Delimiters that always separate command name from it's parameters |
||||
var private array<Text> commandDelimiters; |
||||
/// Registered commands, recorded as (<command_name>, <command_instance>) pairs. |
||||
/// Keys should be deallocated when their entry is removed. |
||||
var private HashTable registeredCommands; |
||||
/// [`HashTable`] of "<command_group_name>" <-> [`ArrayList`] of commands pairs to allow quick fetch |
||||
/// of commands belonging to a single group |
||||
var private HashTable groupedCommands; |
||||
|
||||
/// [`Voting`]s that were already successfully loaded, ensuring that each has a unique name |
||||
var private array<NamedVoting> loadedVotings; |
||||
|
||||
/// Currently running voting process. |
||||
/// This feature doesn't actively track when voting ends, so reference can be non-`none` even if |
||||
/// voting has already ended. |
||||
var private Voting currentVoting; |
||||
/// An array of [`Voting`] objects that have been successfully loaded and |
||||
/// each object has a unique name. |
||||
var private array<NamedVoting> registeredVotings; |
||||
|
||||
/// 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`. |
||||
var private /*config*/ bool useChatInput; |
||||
/// 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 "!". |
||||
var private /*config*/ Text chatCommandPrefix; |
||||
|
||||
var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable; |
||||
var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved; |
||||
|
||||
protected function OnEnabled() { |
||||
registeredCommands = _.collections.EmptyHashTable(); |
||||
groupedCommands = _.collections.EmptyHashTable(); |
||||
RegisterCommand(class'ACommandHelp'); |
||||
RegisterCommand(class'ACommandNotify'); |
||||
RegisterCommand(class'ACommandVote'); |
||||
if (_.environment.IsDebugging()) { |
||||
RegisterCommand(class'ACommandFakers'); |
||||
} |
||||
RegisterVotingClass(class'Voting'); |
||||
// Macro selector |
||||
commandDelimiters[0] = _.text.FromString("@"); |
||||
// Key selector |
||||
commandDelimiters[1] = _.text.FromString("#"); |
||||
// Player array (possibly JSON array) |
||||
commandDelimiters[2] = _.text.FromString("["); |
||||
// 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; |
||||
} |
||||
else { |
||||
_.chat.OnMessage(self).Disconnect(); |
||||
} |
||||
if (useMutateInput || emergencyEnabledMutate) { |
||||
if (__server() != none) { |
||||
__server().unreal.mutator.OnMutate(self).connect = HandleMutate; |
||||
} else { |
||||
_.logger.Auto(errServerAPIUnavailable); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected function OnDisabled() { |
||||
if (useChatInput) { |
||||
_.chat.OnMessage(self).Disconnect(); |
||||
} |
||||
if (useMutateInput && __server() != none) { |
||||
__server().unreal.mutator.OnMutate(self).Disconnect(); |
||||
} |
||||
useChatInput = false; |
||||
useMutateInput = false; |
||||
_.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix); |
||||
registeredCommands = none; |
||||
groupedCommands = none; |
||||
chatCommandPrefix = none; |
||||
commandDelimiters.length = 0; |
||||
ReleaseNameVotingsArray(/*out*/ registeredVotings); |
||||
} |
||||
|
||||
protected function SwapConfig(FeatureConfig config) |
||||
{ |
||||
local Commands newConfig; |
||||
|
||||
newConfig = Commands(config); |
||||
if (newConfig == none) { |
||||
return; |
||||
} |
||||
_.memory.Free(chatCommandPrefix); |
||||
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); |
||||
useChatInput = newConfig.useChatInput; |
||||
useMutateInput = newConfig.useMutateInput; |
||||
} |
||||
|
||||
/// 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()); |
||||
noWayToInputCommands = !feature.emergencyEnabledMutate |
||||
&&!feature.IsUsingMutateInput() |
||||
&& !feature.IsUsingChatInput(); |
||||
if (noWayToInputCommands) { |
||||
default.emergencyEnabledMutate = true; |
||||
feature.emergencyEnabledMutate = true; |
||||
if (__server() != none) { |
||||
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate; |
||||
} else { |
||||
__().logger.Auto(default.errServerAPIUnavailable); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// 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()); |
||||
if (instance != none) { |
||||
return instance.useChatInput; |
||||
} |
||||
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. |
||||
public final static function bool IsUsingMutateInput() { |
||||
local Commands_Feature instance; |
||||
|
||||
instance = Commands_Feature(GetEnabledInstance()); |
||||
if (instance != none) { |
||||
return instance.useMutateInput; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// 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()); |
||||
if (instance != none && instance.chatCommandPrefix != none) { |
||||
return instance.chatCommandPrefix.Copy(); |
||||
} |
||||
return none; |
||||
} |
||||
|
||||
/// Returns `true` iff some voting is currently active. |
||||
public final function bool IsVotingRunning() { |
||||
if (currentVoting != none && currentVoting.HasEnded()) { |
||||
_.memory.Free(currentVoting); |
||||
currentVoting = none; |
||||
} |
||||
return (currentVoting != none); |
||||
} |
||||
|
||||
/// Returns instance of the active voting. |
||||
/// |
||||
/// `none` iff no voting is currently active. |
||||
public final function Voting GetCurrentVoting() { |
||||
if (currentVoting != none && currentVoting.HasEnded()) { |
||||
_.memory.Free(currentVoting); |
||||
currentVoting = none; |
||||
} |
||||
if (currentVoting != none) { |
||||
currentVoting.NewRef(); |
||||
} |
||||
return currentVoting; |
||||
} |
||||
|
||||
/// `true` if voting under the given name (case-insensitive) is already registered. |
||||
public final function bool IsVotingRegistered(BaseText processName) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// Returns class of the [`Voting`] registered under given name. |
||||
public final function StartVotingResult StartVoting(BaseText processName) { |
||||
local int i; |
||||
local Text votingSettingsName; |
||||
local class<Voting> processClass; |
||||
|
||||
if (currentVoting != none && currentVoting.HasEnded()) { |
||||
_.memory.Free(currentVoting); |
||||
currentVoting = none; |
||||
} |
||||
if (currentVoting != none) { |
||||
return SVR_AlreadyInProgress; |
||||
} |
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) { |
||||
processClass = registeredVotings[i].processClass; |
||||
} |
||||
} |
||||
if (processClass == none) { |
||||
return SVR_UnknownVoting; |
||||
} |
||||
currentVoting = Voting(_.memory.Allocate(processClass)); |
||||
currentVoting.Start(votingSettingsName); |
||||
return SVR_Success; |
||||
} |
||||
|
||||
/// Registers a new voting class to be accessible through the [`Commands_Feature`]. |
||||
/// |
||||
/// When a voting class is registered, players can access it using the standard AcediaCore's "vote" |
||||
/// command. |
||||
/// However, note that registering a voting class is not mandatory for it to be usable. |
||||
/// In fact, if you want to prevent players from initiating a particular voting, you should avoid |
||||
/// registering it in this feature. |
||||
public final function RegisterVotingClass(class<Voting> newVotingClass) { |
||||
local int i; |
||||
local ACommandVote votingCommand; |
||||
local NamedVoting newRecord; |
||||
local Text votingName; |
||||
|
||||
if (newVotingClass == none) return; |
||||
votingCommand = GetVotingCommand(); |
||||
if (votingCommand == none) return; |
||||
// We can freely release this reference here, since another reference is guaranteed to be kept in registered command |
||||
_.memory.Free(votingCommand); |
||||
// But just to make sure |
||||
if (!votingCommand.IsAllocated()) return; |
||||
|
||||
// First we check whether we already added this class |
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
if (registeredVotings[i].processClass == newVotingClass) { |
||||
return; |
||||
} |
||||
} |
||||
votingName = newVotingClass.static.GetPreferredName(); |
||||
if (votingName.Compare(P("yes")) || votingName.Compare(P("no"))) { |
||||
_.logger.Auto(errYesNoVotingNamesReserved).ArgClass(newVotingClass).Arg(votingName); |
||||
return; |
||||
} |
||||
// Check for duplicates |
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
if (registeredVotings[i].processName.Compare(votingName)) { |
||||
_.logger |
||||
.Auto(errVotingWithSameNameAlreadyRegistered) |
||||
.ArgClass(newVotingClass) |
||||
.Arg(votingName) |
||||
.ArgClass(registeredVotings[i].processClass); |
||||
return; |
||||
} |
||||
} |
||||
newRecord.processClass = newVotingClass; |
||||
newRecord.processName = votingName; |
||||
registeredVotings[registeredVotings.length] = newRecord; |
||||
votingCommand.AddVotingInfo(votingName, newVotingClass); |
||||
} |
||||
|
||||
/// Unregisters a voting class from the [`Commands_Feature`], preventing players from accessing it |
||||
/// through the standard AcediaCore "vote" command. |
||||
/// |
||||
/// This method does not stop any existing voting processes associated with the unregistered class. |
||||
/// |
||||
/// Use this method to remove a voting class that is no longer needed or to prevent players from |
||||
/// initiating a particular voting. Note that removing a voting class is a linear operation that may |
||||
/// take some time if many votings are currently registered. It is not expected to be a common |
||||
/// operation and should be used sparingly. |
||||
public final function RemoveVotingClass(class<Voting> newVotingClass) { |
||||
local int i; |
||||
local ACommandVote votingCommand; |
||||
|
||||
if (newVotingClass == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
if (registeredVotings[i].processClass == newVotingClass) { |
||||
_.memory.Free(registeredVotings[i].processName); |
||||
registeredVotings.Remove(i, 1); |
||||
} |
||||
} |
||||
votingCommand = GetVotingCommand(); |
||||
if (votingCommand == none) { |
||||
return; |
||||
} |
||||
// Simply rebuild the whole voting set from scratch |
||||
votingCommand.ResetVotingInfo(); |
||||
for (i = 0; i < registeredVotings.length; i += 1) { |
||||
votingCommand.AddVotingInfo( |
||||
registeredVotings[i].processName, |
||||
registeredVotings[i].processClass); |
||||
} |
||||
_.memory.Free(votingCommand); |
||||
} |
||||
|
||||
/// 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<Command> commandClass) { |
||||
local Text commandName, groupName; |
||||
local ArrayList groupArray; |
||||
local Command newCommandInstance, existingCommandInstance; |
||||
|
||||
if (commandClass == none) return false; |
||||
if (registeredCommands == none) return false; |
||||
|
||||
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) { |
||||
_.logger.Auto(errCommandDuplicate) |
||||
.ArgClass(existingCommandInstance.class) |
||||
.Arg(commandName) |
||||
.ArgClass(commandClass); |
||||
_.memory.Free(groupName); |
||||
_.memory.Free(newCommandInstance); |
||||
_.memory.Free(existingCommandInstance); |
||||
return false; |
||||
} |
||||
// Otherwise record new command |
||||
// `commandName` used as a key, do not deallocate it |
||||
registeredCommands.SetItem(commandName, newCommandInstance); |
||||
// Add to grouped collection |
||||
groupArray = groupedCommands.GetArrayList(groupName); |
||||
if (groupArray == none) { |
||||
groupArray = _.collections.EmptyArrayList(); |
||||
} |
||||
groupArray.AddItem(newCommandInstance); |
||||
groupedCommands.SetItem(groupName, groupArray); |
||||
_.memory.Free4(groupArray, groupName, commandName, newCommandInstance); |
||||
return true; |
||||
} |
||||
|
||||
/// 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<Command> commandClass) { |
||||
local int i; |
||||
local CollectionIterator iter; |
||||
local Command nextCommand; |
||||
local Text nextCommandName; |
||||
local array<Text> commandGroup; |
||||
local array<Text> keysToRemove; |
||||
|
||||
if (commandClass == none) return; |
||||
if (registeredCommands == none) return; |
||||
|
||||
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.Free2(nextCommand, nextCommandName); |
||||
continue; |
||||
} |
||||
keysToRemove[keysToRemove.length] = nextCommandName; |
||||
commandGroup[commandGroup.length] = nextCommand.GetGroupName(); |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
iter.FreeSelf(); |
||||
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); |
||||
} |
||||
|
||||
/// 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; |
||||
|
||||
if (commandName == none) return none; |
||||
if (registeredCommands == none) return none; |
||||
|
||||
commandNameLowerCase = commandName.LowerCopy(); |
||||
commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); |
||||
commandNameLowerCase.FreeSelf(); |
||||
return commandInstance; |
||||
} |
||||
|
||||
/// Returns array of names of all available commands. |
||||
public final function array<Text> GetCommandNames() { |
||||
local array<Text> emptyResult; |
||||
|
||||
if (registeredCommands != none) { |
||||
return registeredCommands.GetTextKeys(); |
||||
} |
||||
return emptyResult; |
||||
} |
||||
|
||||
/// Returns array of names of all available commands belonging to the group [`groupName`]. |
||||
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) { |
||||
local int i; |
||||
local ArrayList groupArray; |
||||
local Command nextCommand; |
||||
local array<Text> result; |
||||
|
||||
if (groupedCommands == none) return result; |
||||
groupArray = groupedCommands.GetArrayList(groupName); |
||||
if (groupArray == none) return result; |
||||
|
||||
for (i = 0; i < groupArray.GetLength(); i += 1) { |
||||
nextCommand = Command(groupArray.GetItem(i)); |
||||
if (nextCommand != none) { |
||||
result[result.length] = nextCommand.GetName(); |
||||
} |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Returns all available command groups' names. |
||||
public final function array<Text> GetGroupsNames() { |
||||
local array<Text> emptyResult; |
||||
|
||||
if (groupedCommands != none) { |
||||
return groupedCommands.GetTextKeys(); |
||||
} |
||||
return emptyResult; |
||||
} |
||||
|
||||
/// 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 false; |
||||
} |
||||
wrapper = input.Parse(); |
||||
result = HandleInputWith(wrapper, callerPlayer); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
/// 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()) { |
||||
callerPlayer |
||||
.BorrowConsole() |
||||
.Flush() |
||||
.Say(F("{$TextFailure Command not found!}")); |
||||
} |
||||
if (parser.Ok() && commandInstance != none) { |
||||
callData = commandInstance.ParseInputWith(parser, callerPlayer, callPair.subCommandName); |
||||
errorOccured = commandInstance.Execute(callData, callerPlayer); |
||||
commandInstance.DeallocateCallData(callData); |
||||
} |
||||
_.memory.Free2(callPair.commandName, callPair.subCommandName); |
||||
return errorOccured; |
||||
} |
||||
|
||||
private final function ACommandVote GetVotingCommand() { |
||||
local AcediaObject registeredAsVote; |
||||
|
||||
if (registeredCommands != none) { |
||||
registeredAsVote = registeredCommands.GetItem(P("vote")); |
||||
if (registeredAsVote != none && registeredAsVote.class == class'ACommandVote') { |
||||
return ACommandVote(registeredAsVote); |
||||
} |
||||
_.memory.Free(registeredAsVote); |
||||
} |
||||
return none; |
||||
} |
||||
|
||||
// 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(); |
||||
} |
||||
|
||||
private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup) { |
||||
local int i; |
||||
local ArrayList groupArray; |
||||
local Command nextCommand; |
||||
|
||||
groupArray = groupedCommands.GetArrayList(commandGroup); |
||||
if (groupArray == none) { |
||||
return; |
||||
} |
||||
while (i < groupArray.GetLength()) { |
||||
nextCommand = Command(groupArray.GetItem(i)); |
||||
if (nextCommand != none && nextCommand.class == commandClass) { |
||||
groupArray.RemoveIndex(i); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
if (groupArray.GetLength() == 0) { |
||||
groupedCommands.RemoveItem(commandGroup); |
||||
} |
||||
_.memory.Free(groupArray); |
||||
} |
||||
|
||||
private final function ReleaseNameVotingsArray(out array<NamedVoting> toRelease) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < toRelease.length; i += 1) { |
||||
_.memory.Free(toRelease[i].processName); |
||||
toRelease[i].processName = none; |
||||
} |
||||
toRelease.length = 0; |
||||
} |
||||
|
||||
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.") |
||||
errVotingWithSameNameAlreadyRegistered = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the name \"%2\" when voting process `%3` is already registered. This is likely caused by conflicting mods.") |
||||
errYesNoVotingNamesReserved = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the reserved name \"%2\". This is an issue with the mod that provided the voting, please contact its author.") |
||||
} |
@ -0,0 +1,487 @@
|
||||
/** |
||||
* 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 PlayersParser extends AcediaObject |
||||
dependson(Parser); |
||||
|
||||
//! This parser is supposed to parse player set definitions as they |
||||
//! are used in commands. |
||||
//! |
||||
//! Basic use is to specify one of the selectors: |
||||
//! 1. Key selector: "#<integer>" (examples: "#1", "#5"). |
||||
//! This one is used to specify players by their key, assigned to them when they enter the game. |
||||
//! This type of selectors can be used when players have hard to type names. |
||||
//! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". |
||||
//! "@", "@me", and "@self" are identical and can be used to specify player that called |
||||
//! the command. |
||||
//! "@admin" can be used to specify all admins in the game at once. |
||||
//! "@all" specifies all current players. |
||||
//! In future it is planned to make macros extendable by allowing to bind more names to specific |
||||
//! groups of players. |
||||
//! 3. Name selectors: quoted strings and any other types of string that do not start with |
||||
//! either "#" or "@". |
||||
//! These specify name prefixes: any player with specified prefix will be considered to match |
||||
//! such selector. |
||||
//! |
||||
//! Negated selectors: "!<selector>". Specifying "!" in front of selector will select all players |
||||
//! that do not match it instead. |
||||
//! |
||||
//! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". |
||||
//! Specified selectors are process in order: from left to right. |
||||
//! First selector works as usual and selects a set of players. |
||||
//! All the following selectors either expand that list (additive ones, without "!" prefix) or |
||||
//! remove specific players from the list (the ones with "!" prefix). |
||||
//! Examples of that: |
||||
//! |
||||
//! * "[@admin, !@self]" - selects all admins, except the one who called the command |
||||
//! (whether he is admin or not). |
||||
//! * "[dkanus, 'mate']" - will select players "dkanus" and "mate". Order also matters, since: |
||||
//! * "[@admin, !@admin]" - won't select anyone, since it will first add all the admins and |
||||
//! then remove them. |
||||
//! * "[!@admin, @admin]" - will select everyone, since it will first select everyone who is |
||||
//! not an admin and then adds everyone else. |
||||
//! |
||||
//! # Usage |
||||
//! |
||||
//! 1. Allocate `PlayerParser`; |
||||
//! 2. Set caller player through `SetSelf()` method to make "@" and "@me" selectors usable; |
||||
//! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that starts with proper |
||||
//! players selector; |
||||
//! 4. Call `GetPlayers()` to obtain selected players array. |
||||
//! |
||||
//! # Implementation |
||||
//! |
||||
//! When created, `PlayersParser` takes a snapshot (array) of current players on the server. |
||||
//! Then `currentSelection` is decided based on whether first selector is positive |
||||
//! (initial selection is taken as empty array) or negative |
||||
//! (initial selection is taken as full snapshot). |
||||
//! |
||||
//! After that `PlayersParser` simply goes through specified selectors |
||||
//! (in case more than one is specified) and adds or removes appropriate players in |
||||
//! `currentSelection`, assuming that `playersSnapshot` is a current full array of players. |
||||
|
||||
// Player for which "@", "@me", and "@self" macros will refer |
||||
var private EPlayer selfPlayer; |
||||
// Copy of the list of current players at the moment of allocation of |
||||
// this `PlayersParser`. |
||||
var private array<EPlayer> playersSnapshot; |
||||
// Players, selected according to selectors we have parsed so far |
||||
var private array<EPlayer> currentSelection; |
||||
// Have we parsed our first selector? |
||||
// We need this to know whether to start with the list of |
||||
// all players (if first selector removes them) or |
||||
// with empty list (if first selector adds them). |
||||
var private bool parsedFirstSelector; |
||||
// Will be equal to a single-element array [","], used for parsing |
||||
var private array<Text> selectorDelimiters; |
||||
|
||||
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; |
||||
var const int TOPEN_BRACKET, TCLOSE_BRACKET; |
||||
|
||||
protected function Finalizer() { |
||||
// No need to deallocate `currentSelection`, |
||||
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer` |
||||
_.memory.Free(selfPlayer); |
||||
_.memory.FreeMany(playersSnapshot); |
||||
selfPlayer = none; |
||||
parsedFirstSelector = false; |
||||
playersSnapshot.length = 0; |
||||
currentSelection.length = 0; |
||||
} |
||||
|
||||
/// Set a player who will be referred to by "@", "@me" and "@self" macros. |
||||
/// |
||||
/// Passing `none` will make it so no one is referred by these macros. |
||||
public final function SetSelf(EPlayer newSelfPlayer) { |
||||
_.memory.Free(selfPlayer); |
||||
selfPlayer = none; |
||||
if (newSelfPlayer != none) { |
||||
selfPlayer = EPlayer(newSelfPlayer.Copy()); |
||||
} |
||||
} |
||||
|
||||
// Insert a new player into currently selected list of players (`currentSelection`) such that there |
||||
// will be no duplicates. |
||||
// |
||||
// `none` values are auto-discarded. |
||||
private final function InsertPlayer(EPlayer toInsert) { |
||||
local int i; |
||||
|
||||
if (toInsert == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < currentSelection.length; i += 1) { |
||||
if (currentSelection[i] == toInsert) { |
||||
return; |
||||
} |
||||
} |
||||
currentSelection[currentSelection.length] = toInsert; |
||||
} |
||||
|
||||
// Adds all the players with specified key (`key`) to the current selection. |
||||
private final function AddByKey(int key) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
||||
if (playersSnapshot[i].GetIdentity().GetKey() == key) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Removes all the players with specified key (`key`) from |
||||
// the current selection. |
||||
private final function RemoveByKey(int key) { |
||||
local int i; |
||||
|
||||
while (i < currentSelection.length) { |
||||
if (currentSelection[i].GetIdentity().GetKey() == key) { |
||||
currentSelection.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Adds all the players with specified name (`name`) to the current selection. |
||||
private final function AddByName(BaseText name) { |
||||
local int i; |
||||
local Text nextPlayerName; |
||||
|
||||
if (name == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
||||
nextPlayerName = playersSnapshot[i].GetName(); |
||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
nextPlayerName.FreeSelf(); |
||||
} |
||||
} |
||||
|
||||
// Removes all the players with specified name (`name`) from |
||||
// the current selection. |
||||
private final function RemoveByName(BaseText name) { |
||||
local int i; |
||||
local Text nextPlayerName; |
||||
|
||||
while (i < currentSelection.length) { |
||||
nextPlayerName = currentSelection[i].GetName(); |
||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||
currentSelection.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
nextPlayerName.FreeSelf(); |
||||
} |
||||
} |
||||
|
||||
// Adds all the admins to the current selection. |
||||
private final function AddAdmins() { |
||||
local int i; |
||||
|
||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
||||
if (playersSnapshot[i].IsAdmin()) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Removes all the admins from the current selection. |
||||
private final function RemoveAdmins() { |
||||
local int i; |
||||
|
||||
while (i < currentSelection.length) { |
||||
if (currentSelection[i].IsAdmin()) { |
||||
currentSelection.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Add all the players specified by `macroText` (from macro "@<macroText>"). |
||||
// Does nothing if there is no such macro. |
||||
private final function AddByMacro(BaseText macroText) { |
||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) { |
||||
AddAdmins(); |
||||
return; |
||||
} |
||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) { |
||||
currentSelection = playersSnapshot; |
||||
return; |
||||
} |
||||
if ( macroText.IsEmpty() |
||||
|| macroText.Compare(T(TSELF), SCASE_INSENSITIVE) |
||||
|| macroText.Compare(T(TME), SCASE_INSENSITIVE)) { |
||||
InsertPlayer(selfPlayer); |
||||
} |
||||
} |
||||
|
||||
// Removes all the players specified by `macroText` (from macro "@<macroText>"). |
||||
// Does nothing if there is no such macro. |
||||
private final function RemoveByMacro(BaseText macroText) { |
||||
local int i; |
||||
|
||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) { |
||||
RemoveAdmins(); |
||||
return; |
||||
} |
||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) { |
||||
currentSelection.length = 0; |
||||
return; |
||||
} |
||||
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) { |
||||
while (i < currentSelection.length) { |
||||
if (currentSelection[i] == selfPlayer) { |
||||
currentSelection.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Parses one selector from `parser`, while accordingly modifying current player selection list. |
||||
private final function ParseSelector(Parser parser) { |
||||
local bool additiveSelector; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
if (!parser.Match(T(TNOT)).Ok()) { |
||||
additiveSelector = true; |
||||
parser.RestoreState(confirmedState); |
||||
} |
||||
// Determine whether we stars with empty or full player list |
||||
if (!parsedFirstSelector) { |
||||
parsedFirstSelector = true; |
||||
if (additiveSelector) { |
||||
currentSelection.length = 0; |
||||
} |
||||
else { |
||||
currentSelection = playersSnapshot; |
||||
} |
||||
} |
||||
// Try all selector types |
||||
confirmedState = parser.GetCurrentState(); |
||||
if (parser.Match(T(TKEY)).Ok()) { |
||||
ParseKeySelector(parser, additiveSelector); |
||||
return; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
if (parser.Match(T(TMACRO)).Ok()) { |
||||
ParseMacroSelector(parser, additiveSelector); |
||||
return; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
ParseNameSelector(parser, additiveSelector); |
||||
} |
||||
|
||||
// Parse key selector (assuming "#" is already consumed), while accordingly modifying current player |
||||
// selection list. |
||||
private final function ParseKeySelector(Parser parser, bool additiveSelector) { |
||||
local int key; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
if (!parser.MInteger(key).Ok()) return; |
||||
|
||||
if (additiveSelector) { |
||||
AddByKey(key); |
||||
} else { |
||||
RemoveByKey(key); |
||||
} |
||||
} |
||||
|
||||
// Parse macro selector (assuming "@" is already consumed), while accordingly modifying current |
||||
// player selection list. |
||||
private final function ParseMacroSelector(Parser parser, bool additiveSelector) { |
||||
local MutableText macroName; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
macroName = ParseLiteral(parser); |
||||
if (!parser.Ok()) { |
||||
_.memory.Free(macroName); |
||||
return; |
||||
} |
||||
if (additiveSelector) { |
||||
AddByMacro(macroName); |
||||
} |
||||
else { |
||||
RemoveByMacro(macroName); |
||||
} |
||||
_.memory.Free(macroName); |
||||
} |
||||
|
||||
// Parse name selector, while accordingly modifying current player selection list. |
||||
private final function ParseNameSelector(Parser parser, bool additiveSelector) { |
||||
local MutableText playerName; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
playerName = ParseLiteral(parser); |
||||
if (!parser.Ok() || playerName.IsEmpty()) { |
||||
_.memory.Free(playerName); |
||||
return; |
||||
} |
||||
if (additiveSelector) { |
||||
AddByName(playerName); |
||||
} |
||||
else { |
||||
RemoveByName(playerName); |
||||
} |
||||
_.memory.Free(playerName); |
||||
} |
||||
|
||||
// Reads a string that can either be a body of name selector (some player's name prefix) or |
||||
// of a macro selector (what comes after "@"). |
||||
// |
||||
// This is different from `parser.MString()` because it also uses "," as a separator. |
||||
private final function MutableText ParseLiteral(Parser parser) { |
||||
local MutableText literal; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return none; |
||||
if (!parser.Ok()) return none; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
if (!parser.MStringLiteral(literal).Ok()) { |
||||
parser.RestoreState(confirmedState); |
||||
parser.MUntilMany(literal, selectorDelimiters, true); |
||||
} |
||||
return literal; |
||||
} |
||||
|
||||
/// Returns players parsed by the last `ParseWith()` or `Parse()` call. |
||||
/// |
||||
/// If neither were yet called - returns an empty array. |
||||
public final function array<EPlayer> GetPlayers() { |
||||
local int i; |
||||
local array<EPlayer> result; |
||||
|
||||
for (i = 0; i < currentSelection.length; i += 1) { |
||||
if (currentSelection[i].IsExistent()) { |
||||
result[result.length] = EPlayer(currentSelection[i].Copy()); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Parses players from `parser` according to the currently present players. |
||||
/// |
||||
/// Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||
/// |
||||
/// Returns `true` if parsing was successful and `false` otherwise. |
||||
public final function bool ParseWith(Parser parser) { |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return false; |
||||
if (!parser.Ok()) return false; |
||||
if (parser.HasFinished()) return false; |
||||
|
||||
Reset(); |
||||
confirmedState = parser.Skip().GetCurrentState(); |
||||
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) { |
||||
ParseSelector(parser.RestoreState(confirmedState)); |
||||
if (parser.Ok()) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
while (parser.Ok() && !parser.HasFinished()) { |
||||
confirmedState = parser.Skip().GetCurrentState(); |
||||
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) { |
||||
return true; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
if (parsedFirstSelector) { |
||||
parser.Match(T(TCOMMA)).Skip(); |
||||
} |
||||
ParseSelector(parser); |
||||
parser.Skip(); |
||||
} |
||||
parser.Fail(); |
||||
return false; |
||||
} |
||||
|
||||
// Resets this object to initial state before parsing and update |
||||
// `playersSnapshot` to contain current players. |
||||
private final function Reset() { |
||||
parsedFirstSelector = false; |
||||
currentSelection.length = 0; |
||||
_.memory.FreeMany(playersSnapshot); |
||||
playersSnapshot.length = 0; |
||||
playersSnapshot = _.players.GetAll(); |
||||
selectorDelimiters.length = 0; |
||||
selectorDelimiters[0] = T(TCOMMA); |
||||
selectorDelimiters[1] = T(TCLOSE_BRACKET); |
||||
} |
||||
|
||||
/// Parses players from according to the currently present players. |
||||
/// |
||||
/// Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||
/// Returns `true` if parsing was successful and `false` otherwise. |
||||
public final function bool Parse(BaseText toParse) { |
||||
local bool wasSuccessful; |
||||
local Parser parser; |
||||
|
||||
if (toParse == none) { |
||||
return false; |
||||
} |
||||
parser = _.text.Parse(toParse); |
||||
wasSuccessful = ParseWith(parser); |
||||
parser.FreeSelf(); |
||||
return wasSuccessful; |
||||
} |
||||
|
||||
defaultproperties { |
||||
TSELF = 0 |
||||
stringConstants(0) = "self" |
||||
TADMIN = 1 |
||||
stringConstants(1) = "admin" |
||||
TALL = 2 |
||||
stringConstants(2) = "all" |
||||
TNOT = 3 |
||||
stringConstants(3) = "!" |
||||
TKEY = 4 |
||||
stringConstants(4) = "#" |
||||
TMACRO = 5 |
||||
stringConstants(5) = "@" |
||||
TCOMMA = 6 |
||||
stringConstants(6) = "," |
||||
TOPEN_BRACKET = 7 |
||||
stringConstants(7) = "[" |
||||
TCLOSE_BRACKET = 8 |
||||
stringConstants(8) = "]" |
||||
TME = 9 |
||||
stringConstants(9) = "me" |
||||
} |
@ -0,0 +1,61 @@
|
||||
/** |
||||
* Mock command class for testing. |
||||
* Copyright 2021 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 MockCommandB extends Command; |
||||
|
||||
protected function BuildData(CommandDataBuilder builder) |
||||
{ |
||||
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 |
||||
{ |
||||
} |
@ -0,0 +1,563 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class TEST_Voting extends TestCase |
||||
abstract |
||||
dependsOn(VotingModel); |
||||
|
||||
enum ExpectedOutcome { |
||||
TEST_EO_Continue, |
||||
TEST_EO_End, |
||||
TEST_EO_EndDraw, |
||||
}; |
||||
|
||||
protected static function VotingModel MakeVotingModel(VotingModel.VotingPolicies policies) { |
||||
local VotingModel model; |
||||
|
||||
model = VotingModel(__().memory.Allocate(class'VotingModel')); |
||||
model.Initialize(policies); |
||||
return model; |
||||
} |
||||
|
||||
protected static function SetVoters( |
||||
VotingModel model, |
||||
optional string voterID0, |
||||
optional string voterID1, |
||||
optional string voterID2, |
||||
optional string voterID3, |
||||
optional string voterID4, |
||||
optional string voterID5, |
||||
optional string voterID6, |
||||
optional string voterID7, |
||||
optional string voterID8, |
||||
optional string voterID9 |
||||
) { |
||||
local UserID nextID; |
||||
local array<UserID> voterIDs; |
||||
|
||||
if (voterID0 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID0)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID1 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID1)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID2 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID2)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID3 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID3)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID4 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID4)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID5 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID5)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID6 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID6)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID7 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID7)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID8 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID8)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
if (voterID9 != "") { |
||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||
nextID.Initialize(__().text.FromString(voterID9)); |
||||
voterIDs[voterIDs.length] = nextID; |
||||
} |
||||
model.UpdatePotentialVoters(voterIDs); |
||||
} |
||||
|
||||
protected static function MakeFaultyYesVote( |
||||
VotingModel model, |
||||
string voterID, |
||||
VotingModel.VotingResult expected) { |
||||
local UserID id; |
||||
|
||||
id = UserID(__().memory.Allocate(class'UserID')); |
||||
id.Initialize(__().text.FromString(voterID)); |
||||
Issue("Illegal vote had unexpected result."); |
||||
TEST_ExpectTrue(model.CastVote(id, true) == expected); |
||||
} |
||||
|
||||
protected static function MakeFaultyNoVote( |
||||
VotingModel model, |
||||
string voterID, |
||||
VotingModel.VotingResult expected) { |
||||
local UserID id; |
||||
|
||||
id = UserID(__().memory.Allocate(class'UserID')); |
||||
id.Initialize(__().text.FromString(voterID)); |
||||
Issue("Illegal vote had unexpected result."); |
||||
TEST_ExpectTrue(model.CastVote(id, false) == expected); |
||||
} |
||||
|
||||
protected static function VoteYes(VotingModel model, string voterID, ExpectedOutcome expected) { |
||||
local UserID id; |
||||
|
||||
id = UserID(__().memory.Allocate(class'UserID')); |
||||
id.Initialize(__().text.FromString(voterID)); |
||||
Issue("Failed to add legitimate vote."); |
||||
TEST_ExpectTrue(model.CastVote(id, true) == VFR_Success); |
||||
if (expected == TEST_EO_Continue) { |
||||
Issue("Vote, that shouldn't have ended voting, ended it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
} else if (expected == TEST_EO_End) { |
||||
Issue("Vote, that should've ended voting with one side's victory, didn't do it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Success); |
||||
} else if (expected == TEST_EO_EndDraw) { |
||||
Issue("Vote, that should've ended voting with a draw, didn't do it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Draw); |
||||
} |
||||
} |
||||
|
||||
protected static function VoteNo(VotingModel model, string voterID, ExpectedOutcome expected) { |
||||
local UserID id; |
||||
|
||||
id = UserID(__().memory.Allocate(class'UserID')); |
||||
id.Initialize(__().text.FromString(voterID)); |
||||
Issue("Failed to add legitimate vote."); |
||||
TEST_ExpectTrue(model.CastVote(id, false) == VFR_Success); |
||||
if (expected == TEST_EO_Continue) { |
||||
Issue("Vote, that shouldn't have ended voting, ended it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
} else if (expected == TEST_EO_End) { |
||||
Issue("Vote, that should've ended voting with one side's victory, didn't do it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Failure); |
||||
} else if (expected == TEST_EO_EndDraw) { |
||||
Issue("Vote, that should've ended voting with a draw, didn't do it."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Draw); |
||||
} |
||||
} |
||||
|
||||
protected static function TESTS() { |
||||
Test_RestrictiveVoting(); |
||||
Test_CanLeaveVoting(); |
||||
Test_CanChangeVoting(); |
||||
Test_All(); |
||||
} |
||||
|
||||
protected static function Test_RestrictiveVoting() { |
||||
SubTest_RestrictiveYesVoting(); |
||||
SubTest_RestrictiveNoVoting(); |
||||
SubTest_RestrictiveDrawVoting(); |
||||
SubTest_RestrictiveFaultyVoting(); |
||||
SubTest_RestrictiveDisconnectVoting(); |
||||
SubTest_RestrictiveReconnectVoting(); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveYesVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"yes\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveNoVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"no\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteNo(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveDrawVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"draw\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_EndDraw); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveFaultyVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"faulty\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
MakeFaultyYesVote(model, "3", VFR_AlreadyVoted); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "7", VFR_NotAllowed); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "7", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "5", VFR_AlreadyVoted); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "3", VFR_CannotChangeVote); |
||||
VoteYes(model, "6", TEST_EO_End); |
||||
MakeFaultyYesVote(model, "4", VFR_VotingEnded); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveDisconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"disconnect\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteYes(model, "9", TEST_EO_Continue); |
||||
// Here we're at 3 "no" votes, 3 "yes" votes out of 7 total; |
||||
// disconnect "2" and "9" for "no" to win |
||||
SetVoters(model, "4", "5", "6", "8", "10"); |
||||
Issue("Unexpected result after voting users disconnected."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Failure); |
||||
} |
||||
|
||||
protected static function SubTest_RestrictiveReconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing restrictive \"reconnecting\" voting."); |
||||
model = MakeVotingModel(VP_Restrictive); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
// Disconnect 1 3 "yes" voters |
||||
SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteYes(model, "9", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "3", VFR_NotAllowed); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteNo(model, "10", TEST_EO_EndDraw); |
||||
} |
||||
/* Testing restrictive "reconnecting" voting. |
||||
Unexpected result after voting users reconnected. [1] */ |
||||
protected static function Test_CanLeaveVoting() { |
||||
SubTest_CanLeaveYesVoting(); |
||||
SubTest_CanLeaveNoVoting(); |
||||
SubTest_CanLeaveDrawVoting(); |
||||
SubTest_CanLeaveFaultyVoting(); |
||||
SubTest_CanLeaveDisconnectVoting(); |
||||
SubTest_CanLeaveReconnectVoting(); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveYesVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"yes\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
SetVoters(model, "1", "5", "6"); |
||||
Issue("Unexpected result after voting users leaves."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
VoteYes(model, "1", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveNoVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"no\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteNo(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
SetVoters(model, "3", "4", "5"); |
||||
Issue("Unexpected result after voting users leaves."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
VoteNo(model, "4", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveDrawVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"draw\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
SetVoters(model, "4"); |
||||
Issue("Unexpected result after voting users leaves."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
VoteNo(model, "4", TEST_EO_EndDraw); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveFaultyVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"faulty\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
MakeFaultyYesVote(model, "3", VFR_AlreadyVoted); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "7", VFR_NotAllowed); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "7", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "5", VFR_AlreadyVoted); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "3", VFR_CannotChangeVote); |
||||
SetVoters(model, "4", "5", "6"); |
||||
Issue("Unexpected result after voting users leaves."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_InProgress); |
||||
VoteYes(model, "6", TEST_EO_End); |
||||
MakeFaultyYesVote(model, "4", VFR_VotingEnded); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveDisconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"leave\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteYes(model, "10", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_CanLeaveReconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"reconnecting\" voting where users are allowed to leave."); |
||||
model = MakeVotingModel(VP_CanLeave); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
// Disconnect 1 3 "yes" voters |
||||
SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteNo(model, "9", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "3", VFR_NotAllowed); |
||||
VoteYes(model, "10", TEST_EO_EndDraw); |
||||
} |
||||
|
||||
protected static function Test_CanChangeVoting() { |
||||
SubTest_CanChangeYesVoting(); |
||||
SubTest_CanChangeNoVoting(); |
||||
SubTest_CanChangeDrawVoting(); |
||||
SubTest_CanChangeFaultyVoting(); |
||||
SubTest_CanChangeDisconnectVoting(); |
||||
SubTest_CanChangeReconnectVoting(); |
||||
} |
||||
|
||||
protected static function SubTest_CanChangeYesVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"yes\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteNo(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "6", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_CanChangeNoVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"no\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteNo(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function SubTest_CanChangeDrawVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"draw\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_EndDraw); |
||||
} |
||||
protected static function SubTest_CanChangeFaultyVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"faulty\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6"); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "6", TEST_EO_End); |
||||
MakeFaultyYesVote(model, "4", VFR_VotingEnded); |
||||
} |
||||
|
||||
protected static function SubTest_CanChangeDisconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"disconnect\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "6", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "9", TEST_EO_Continue); |
||||
// Here we're at 3 "no" votes, 3 "yes" votes out of 7 total; |
||||
// disconnect "6" and "9" for "yes" to win |
||||
SetVoters(model, "2", "4", "5", "8", "10"); |
||||
Issue("Unexpected result after voting users disconnected."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Success); |
||||
} |
||||
|
||||
protected static function SubTest_CanChangeReconnectVoting() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing \"reconnect\" voting where users are allowed to change their vote."); |
||||
model = MakeVotingModel(VP_CanChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteNo(model, "2", TEST_EO_Continue); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_Continue); |
||||
VoteYes(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
// Disconnect 1 3 "yes" voters |
||||
SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "7", TEST_EO_Continue); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteYes(model, "9", TEST_EO_Continue); |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "3", VFR_NotAllowed); |
||||
// Restore 3 "yes" voter |
||||
SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteNo(model, "3", TEST_EO_End); |
||||
} |
||||
|
||||
protected static function Test_All() { |
||||
local VotingModel model; |
||||
|
||||
Context("Testing permissive voting options."); |
||||
model = MakeVotingModel(VP_CanLeaveAndChangeVote); |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "1", TEST_EO_Continue); |
||||
VoteYes(model, "2", TEST_EO_Continue); |
||||
VoteNo(model, "3", TEST_EO_Continue); |
||||
VoteYes(model, "4", TEST_EO_Continue); |
||||
VoteNo(model, "5", TEST_EO_Continue); |
||||
VoteNo(model, "6", TEST_EO_Continue); |
||||
// Disconnect 1 and 5 voters |
||||
SetVoters(model, "2", "3", "4", "6", "7", "8", "9", "10"); |
||||
MakeFaultyNoVote(model, "1", VFR_NotAllowed); |
||||
MakeFaultyNoVote(model, "5", VFR_NotAllowed); |
||||
VoteYes(model, "3", TEST_EO_Continue); |
||||
VoteNo(model, "7", TEST_EO_Continue); |
||||
VoteNo(model, "8", TEST_EO_Continue); |
||||
VoteNo(model, "9", TEST_EO_Continue); |
||||
// Bring back 1, disconnect 3 and 6 |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); |
||||
VoteYes(model, "8", TEST_EO_Continue); |
||||
VoteNo(model, "4", TEST_EO_Continue); |
||||
// Disconnect 10, finishing voting (since now only 9 voters are available) |
||||
SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9"); |
||||
Issue("Unexpected result after voting users disconnected."); |
||||
TEST_ExpectTrue(model.GetStatus() == VPM_Failure); |
||||
} |
||||
|
||||
defaultproperties { |
||||
caseGroup = "Commands" |
||||
caseName = "Voting model" |
||||
} |
@ -0,0 +1,437 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class Voting extends AcediaObject |
||||
dependsOn(VotingModel); |
||||
|
||||
//! Class that describes a single voting option. |
||||
//! |
||||
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and shouldn't be used |
||||
//! separately from it. |
||||
//! You shouldn't allocate its instances directly unless you're working on |
||||
//! the [`Commands_Feature`]'s or related code. |
||||
//! |
||||
//! # Usage |
||||
//! |
||||
//! This class takes care of the voting process by itself, one only needs to call [`Start()`] method. |
||||
//! If you wish to prematurely end voting (e.g. forcing it to end), then call [`Stop()`] method. |
||||
//! |
||||
//! Note that there is no method to handle ending of [`Voting`], since all necessary logic is |
||||
//! expected to be performed internally. |
||||
//! [`Commands_Feature`] does this check lazily (only when user wants to start another voting) |
||||
//! via [`HasEnded()`] method. |
||||
|
||||
/// Records whether end-of-the voting methods were already called. |
||||
var private bool endingHandled; |
||||
/// Records whether voting was forcefully ended |
||||
var private bool forcefullyEnded; |
||||
/// Underlying voting model that does actual vote calculations. |
||||
var private VotingModel model; |
||||
|
||||
/// Fake voters that are only used in debug mode to allow for simpler vote testing. |
||||
var private array<UserID> debugVoters; |
||||
|
||||
/// Text that serves as a template for announcing current vote counts. Don't change this. |
||||
var private const string votesDistributionLine; |
||||
/// Text that serves as a template for announcing player making a new vote. Don't change this. |
||||
var private const string playerVotedLine; |
||||
// [`TextTemplate`]s made from the above `string` templates. |
||||
var private TextTemplate summaryTemplate, playerVotedTemplate; |
||||
|
||||
// TODO: check these limitations |
||||
/// Name of this voting. |
||||
/// |
||||
/// Has to satisfy limitations described in the `BaseText::IsValidName()` |
||||
var private const string preferredName; |
||||
/// Text that should be displayed when voting info is displayed mid-voting. |
||||
/// |
||||
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||
/// "Voting to end trader currently active." line, avoid adding formatting and don't |
||||
/// add comma/exclamation mark at the end. |
||||
var private const string infoLine; |
||||
/// Text that should be displayed when voting starts. |
||||
/// |
||||
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||
/// "Voting to end trader has started" line, avoid adding formatting and don't |
||||
/// add comma/exclamation mark at the end. |
||||
var private const string votingStartedLine; |
||||
/// Text that should be displayed when voting has ended with a success. |
||||
/// |
||||
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||
/// "{$TextPositive Voting to end trader was successful!}" line, coloring it in a positive color and |
||||
/// adding comma/exclamation mark at the end. |
||||
var private const string votingSucceededLine; |
||||
/// Text that should be displayed when voting has ended in a failure. |
||||
/// |
||||
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||
/// "{$TextNegative Voting to end trader has failed!}" line, coloring it in a negative color and |
||||
/// adding comma/exclamation mark at the end. |
||||
var private const string votingFailedLine; |
||||
|
||||
protected function Constructor() { |
||||
summaryTemplate = _.text.MakeTemplate_S(votesDistributionLine); |
||||
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedLine); |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
forcefullyEnded = false; |
||||
endingHandled = false; |
||||
_.memory.Free(model); |
||||
_.memory.Free(summaryTemplate); |
||||
_.memory.Free(playerVotedTemplate); |
||||
model = none; |
||||
summaryTemplate = none; |
||||
playerVotedTemplate = none; |
||||
} |
||||
|
||||
/// Override this to perform necessary logic after voting has succeeded. |
||||
protected function Execute() {} |
||||
|
||||
/// Override this to specify arguments for your voting command. |
||||
/// |
||||
/// This method is for adding arguments only. |
||||
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or [`CommandDataBuilder::Option()`] methods, |
||||
/// otherwise you'll cause unexpected behavior for your mod's users. |
||||
public static function AddInfo(CommandDataBuilder builder) { |
||||
builder.OptionalParams(); |
||||
builder.ParamText(P("message")); |
||||
} |
||||
|
||||
/// Returns name of this voting in the lower case. |
||||
public final static function Text GetPreferredName() { |
||||
local Text result; |
||||
|
||||
result = __().text.FromString(Locs(default.preferredName)); |
||||
if (result.IsValidName()) { |
||||
return result; |
||||
} |
||||
__().memory.Free(result); |
||||
return none; |
||||
} |
||||
|
||||
/// Starts caller [`Voting`] using policies, loaded from the given config. |
||||
public final function Start(BaseText votingConfigName) { |
||||
local int i; |
||||
local MutableText howToVoteHint; |
||||
local array<EPlayer> voters; |
||||
|
||||
if (model != none) { |
||||
return; |
||||
} |
||||
model = VotingModel(_.memory.Allocate(class'VotingModel')); |
||||
model.Initialize(VP_CanChangeVote); |
||||
voters = UpdateVoters(); |
||||
howToVoteHint = MakeHowToVoteHint(); |
||||
for (i = 0; i < voters.length; i += 1) { |
||||
voters[i].Notify(F(votingStartedLine), howToVoteHint); |
||||
voters[i].BorrowConsole().WriteLine(F(votingStartedLine)); |
||||
voters[i].BorrowConsole().WriteLine(howToVoteHint); |
||||
} |
||||
_.memory.FreeMany(voters); |
||||
_.memory.Free(howToVoteHint); |
||||
} |
||||
|
||||
// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint. |
||||
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases aren't properly setup. |
||||
private final function MutableText MakeHowToVoteHint() { |
||||
local Text resolvedAlias; |
||||
local MutableText result; |
||||
|
||||
result = P("Say ").MutableCopy(); |
||||
resolvedAlias = _.alias.ResolveCommand(P("yes")); |
||||
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) { |
||||
result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
||||
} else { |
||||
result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
||||
} |
||||
_.memory.Free(resolvedAlias); |
||||
result.Append(P(" or ")); |
||||
resolvedAlias = _.alias.ResolveCommand(P("no")); |
||||
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) { |
||||
result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative)); |
||||
} else { |
||||
result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative)); |
||||
} |
||||
_.memory.Free(resolvedAlias); |
||||
result.Append(P(" to vote")); |
||||
return result; |
||||
} |
||||
|
||||
/// Forcefully stops [`Voting`]. |
||||
/// |
||||
/// Only works if [`Voting`] has already started, but didn't yet ended (see [`HasEnded()`]). |
||||
public final function Stop() { |
||||
if (model != none) { |
||||
forcefullyEnded = true; |
||||
} |
||||
} |
||||
|
||||
/// Determines whether the [`Voting`] process has concluded. |
||||
/// |
||||
/// Note that this is different from the voting being active, as voting that has not yet started is |
||||
/// also not concluded. |
||||
public final function bool HasEnded() { |
||||
if (model == none) { |
||||
return false; |
||||
} |
||||
return (forcefullyEnded || model.HasEnded()); |
||||
} |
||||
|
||||
/// Returns the current voting status for the specified voter. |
||||
/// |
||||
/// If the voter was previously eligible to vote, cast a vote, but later had their voting rights |
||||
/// revoked, their vote will not count, and this method will return [`PVS_NoVote`]. |
||||
/// |
||||
/// However, if the player regains their voting rights while the voting process is still ongoing, |
||||
/// their previous vote will be automatically restored by the caller [`Voting`]. |
||||
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) { |
||||
if (model != none) { |
||||
return model.GetVote(voter); |
||||
} |
||||
return PVS_NoVote; |
||||
} |
||||
|
||||
/// Specifies the [`UserID`]s that will be added as additional voters in debug mode. |
||||
/// |
||||
/// This method should only be used for debugging purposes and will only function if the game is |
||||
/// running in debug mode. |
||||
public final function SetDebugVoters(array<UserID> newDebugVoters) { |
||||
local int i; |
||||
|
||||
if(!_.environment.IsDebugging()) { |
||||
return; |
||||
} |
||||
_.memory.FreeMany(debugVoters); |
||||
debugVoters.length = 0; |
||||
for (i = 0; i < newDebugVoters.length; i += 1) { |
||||
if (newDebugVoters[i] != none) { |
||||
debugVoters[debugVoters.length] = newDebugVoters[i]; |
||||
newDebugVoters[i].NewRef(); |
||||
} |
||||
} |
||||
_.memory.FreeMany(UpdateVoters()); |
||||
TryEnding(); |
||||
} |
||||
|
||||
/// Adds a new vote by a given [`UserID`]. |
||||
/// |
||||
/// NOTE: this method is intended for use only in debug mode, and is will not do anything otherwise. |
||||
/// This method silently adds a vote using the provided [`UserID`], without any prompt or |
||||
/// notification of updated voting status. |
||||
/// It was added to facilitate testing with fake [`UserID`]s, and is limited to debug mode to |
||||
/// prevent misuse and unintended behavior in production code. |
||||
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) { |
||||
local array<EPlayer> allVoters; |
||||
local VotingModel.VotingResult result; |
||||
|
||||
if (model == none) return VFR_NotAllowed; |
||||
if (voter == none) return VFR_NotAllowed; |
||||
if (!_.environment.IsDebugging()) return VFR_NotAllowed; |
||||
|
||||
allVoters = UpdateVoters(); |
||||
result = model.CastVote(voter, voteForSuccess); |
||||
if (!TryEnding() && result == VFR_Success) { |
||||
AnnounceNewVote(none, allVoters, voteForSuccess); |
||||
} |
||||
_.memory.FreeMany(allVoters); |
||||
return result; |
||||
} |
||||
|
||||
/// Casts a vote for the specified player. |
||||
/// |
||||
/// This method will update the voting status for the specified player and may trigger the end of |
||||
/// the voting process. |
||||
/// After a vote is cast, the updated voting status will be broadcast to all players. |
||||
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) { |
||||
local bool votingContinues; |
||||
local UserID voterID; |
||||
local array<EPlayer> allVoters; |
||||
local VotingModel.VotingResult result; |
||||
|
||||
if (model == none) return VFR_NotAllowed; |
||||
if (voter == none) return VFR_NotAllowed; |
||||
|
||||
voterID = voter.GetUserID(); |
||||
allVoters = UpdateVoters(); |
||||
result = model.CastVote(voterID, voteForSuccess); |
||||
votingContinues = !TryEnding(); |
||||
if (votingContinues) { |
||||
switch (result) { |
||||
case VFR_Success: |
||||
AnnounceNewVote(voter, allVoters, voteForSuccess); |
||||
break; |
||||
case VFR_NotAllowed: |
||||
voter |
||||
.BorrowConsole() |
||||
.WriteLine(F("You are {$TextNegative not allowed} to vote right now.")); |
||||
break; |
||||
case VFR_CannotChangeVote: |
||||
voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}.")); |
||||
break; |
||||
case VFR_VotingEnded: |
||||
voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!")); |
||||
break; |
||||
default: |
||||
} |
||||
} |
||||
_.memory.Free(voterID); |
||||
_.memory.FreeMany(allVoters); |
||||
return result; |
||||
} |
||||
|
||||
/// Prints information about caller [`Voting`] to the given player. |
||||
public final function PrintVotingInfoFor(EPlayer requester) { |
||||
local MutableText summaryPart; |
||||
|
||||
if (requester == none) { |
||||
return; |
||||
} |
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryPart = summaryTemplate.CollectFormattedM(); |
||||
requester |
||||
.BorrowConsole() |
||||
.Write(F(infoLine)) |
||||
.Write(P(". ")) |
||||
.Write(summaryPart) |
||||
.WriteLine(P(".")); |
||||
_.memory.Free(summaryPart); |
||||
} |
||||
|
||||
/// Outputs message about new vote being submitted to all relevant voters. |
||||
private final function AnnounceNewVote(EPlayer voter, array<EPlayer> voters, bool voteForSuccess) { |
||||
local int i; |
||||
local Text voterName; |
||||
local MutableText playerVotedPart, summaryPart; |
||||
|
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryPart = summaryTemplate.CollectFormattedM(); |
||||
|
||||
playerVotedTemplate.Reset(); |
||||
if (voter != none) { |
||||
voterName = voter.GetName(); |
||||
} else { |
||||
voterName = P("DEBUG:FAKER").Copy(); |
||||
} |
||||
playerVotedTemplate.TextArg(P("player_name"), voterName, true); |
||||
_.memory.Free(voterName); |
||||
if (voteForSuccess) { |
||||
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}")); |
||||
} else { |
||||
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}")); |
||||
} |
||||
playerVotedPart = playerVotedTemplate.CollectFormattedM(); |
||||
for (i = 0; i < voters.length; i += 1) { |
||||
voters[i] |
||||
.BorrowConsole() |
||||
.Write(playerVotedPart) |
||||
.Write(P(". ")) |
||||
.Write(summaryPart) |
||||
.WriteLine(P(".")); |
||||
} |
||||
_.memory.Free(playerVotedPart); |
||||
_.memory.Free(summaryPart); |
||||
} |
||||
|
||||
/// Tries to end voting. |
||||
/// |
||||
/// Returns `true` iff this method was called for the first time after the voting concluded. |
||||
private final function bool TryEnding() { |
||||
local MutableText outcomeMessage; |
||||
|
||||
if (model == none) return false; |
||||
if (endingHandled) return false; |
||||
if (!HasEnded()) return false; |
||||
|
||||
endingHandled = true; |
||||
if (model.GetStatus() == VPM_Success) { |
||||
outcomeMessage = _.text.FromFormattedStringM(votingSucceededLine); |
||||
} else { |
||||
outcomeMessage = _.text.FromFormattedStringM(votingFailedLine); |
||||
} |
||||
AnnounceOutcome(outcomeMessage); |
||||
if (model.GetStatus() == VPM_Success) { |
||||
Execute(); |
||||
} |
||||
_.memory.Free(outcomeMessage); |
||||
return true; |
||||
} |
||||
|
||||
/// Updates the inner voting model with current list of players allowed to vote. |
||||
/// Also returns said list. |
||||
private final function array<EPlayer> UpdateVoters() { |
||||
local int i; |
||||
local UserID nextID; |
||||
local array<EPlayer> currentPlayers; |
||||
local array<UserID> potentialVoters; |
||||
|
||||
if (model == none) { |
||||
return currentPlayers; |
||||
} |
||||
for (i = 0; i < debugVoters.length; i += 1) { |
||||
debugVoters[i].NewRef(); |
||||
potentialVoters[potentialVoters.length] = debugVoters[i]; |
||||
} |
||||
currentPlayers = _.players.GetAll(); |
||||
for (i = 0; i < currentPlayers.length; i += 1) { |
||||
nextID = currentPlayers[i].GetUserID(); |
||||
potentialVoters[potentialVoters.length] = nextID; |
||||
} |
||||
model.UpdatePotentialVoters(potentialVoters); |
||||
_.memory.FreeMany(potentialVoters); |
||||
return currentPlayers; |
||||
} |
||||
|
||||
/// Prints given voting outcome message in console and publishes it as a notification. |
||||
private final function AnnounceOutcome(BaseText outcomeMessage) { |
||||
local int i; |
||||
local MutableText summaryLine; |
||||
local array<EPlayer> currentPlayers; |
||||
|
||||
if (model == none) { |
||||
return; |
||||
} |
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryLine = summaryTemplate.CollectFormattedM(); |
||||
currentPlayers = _.players.GetAll(); |
||||
for (i = 0; i < currentPlayers.length; i += 1) { |
||||
currentPlayers[i].BorrowConsole().WriteLine(outcomeMessage); |
||||
currentPlayers[i].BorrowConsole().WriteLine(summaryLine); |
||||
currentPlayers[i].Notify(outcomeMessage, summaryLine); |
||||
} |
||||
_.memory.FreeMany(currentPlayers); |
||||
_.memory.Free(summaryLine); |
||||
} |
||||
|
||||
defaultproperties { |
||||
preferredName = "test" |
||||
infoLine = "Voting for test" |
||||
votingStartedLine = "Test voting has started" |
||||
votingSucceededLine = "{$TextPositive Test voting passed!}" |
||||
votingFailedLine = "{$TextNegative Test voting has failed...}" |
||||
playerVotedLine = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting" |
||||
votesDistributionLine = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}" |
||||
} |
@ -0,0 +1,447 @@
|
||||
/** |
||||
* 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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class VotingModel extends AcediaObject; |
||||
|
||||
//! This class counts votes according to the configured voting policies. |
||||
//! |
||||
//! Its main purpose is to separate the voting logic from the voting interface, making |
||||
//! the implementation simpler and the logic easier to test. |
||||
//! |
||||
//! # Usage |
||||
//! |
||||
//! 1. Allocate an instance of the [`VotingModel`] class. |
||||
//! 2. Call [`Initialize()`] to set the required policies. |
||||
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote. |
||||
//! You can change this set at any time. The method used to recount the votes will depend on |
||||
//! the policies set during the previous [`Initialize()`] call. |
||||
//! 4. Use [`CastVote()`] to add a vote from a user. |
||||
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], check [`GetStatus()`] to |
||||
//! see if the voting has concluded. |
||||
//! Once voting has concluded, the result cannot be changed, so you can release the reference |
||||
//! to the [`VotingModel`] object. |
||||
|
||||
/// Describes how [`VotingModel`] should react when a user performs potentially illegal actions. |
||||
/// |
||||
/// Illegal here means that either corresponding operation won't be permitted or any vote made |
||||
/// would be considered invalid. |
||||
/// |
||||
/// Leaving means simply no longer being in a potential pool of voters, which includes actually |
||||
/// leaving the game and simply losing rights to vote. |
||||
enum VotingPolicies { |
||||
/// Anything that can be considered illegal actions is prohibited. |
||||
/// |
||||
/// Leaving (or losing rights to vote) during voting will make a vote invalid. |
||||
/// |
||||
/// Changing vote is forbidden. |
||||
VP_Restrictive, |
||||
/// Leaving during voting is allowed. Changing a vote is not allowed. |
||||
VP_CanLeave, |
||||
/// Changing one's vote is allowed. If a user leaves during voting, their vote will be invalid. |
||||
VP_CanChangeVote, |
||||
/// Leaving during voting and changing a vote is allowed. Leaving means losing rights to vote. |
||||
/// |
||||
/// Currently, this policy allows all available options, but this may change in the future if |
||||
/// more options are added. |
||||
VP_CanLeaveAndChangeVote |
||||
}; |
||||
|
||||
/// Current state of voting for this model. |
||||
enum VotingModelStatus { |
||||
/// Voting hasn't even started, waiting for [`Initialize()`] call |
||||
VPM_Uninitialized, |
||||
/// Voting is currently in progress |
||||
VPM_InProgress, |
||||
/// Voting has ended with majority for its success |
||||
VPM_Success, |
||||
/// Voting has ended with majority for its failure |
||||
VPM_Failure, |
||||
/// Voting has ended in a draw |
||||
VPM_Draw |
||||
}; |
||||
|
||||
/// A result of user trying to make a vote |
||||
enum VotingResult { |
||||
/// Vote accepted |
||||
VFR_Success, |
||||
/// Voting is not allowed for this particular user |
||||
VFR_NotAllowed, |
||||
/// User already made a vote and changing votes isn't allowed |
||||
VFR_CannotChangeVote, |
||||
/// User has already voted the same way |
||||
VFR_AlreadyVoted, |
||||
/// Voting has already ended and doesn't accept new votes |
||||
VFR_VotingEnded |
||||
}; |
||||
|
||||
/// Checks how given user has voted |
||||
enum PlayerVoteStatus { |
||||
/// User hasn't voted yet |
||||
PVS_NoVote, |
||||
/// User voted for the change |
||||
PVS_VoteFor, |
||||
/// User voted against the change |
||||
PVS_VoteAgainst |
||||
}; |
||||
|
||||
var private VotingModelStatus status; |
||||
|
||||
var private bool policyCanLeave, policyCanChangeVote; |
||||
|
||||
var private array<UserID> votesFor, votesAgainst; |
||||
/// Votes of people that voted before, but then were forbidden to vote |
||||
/// (either because they have left or simply lost the right to vote) |
||||
var private array<UserID> storedVotesFor, storedVotesAgainst; |
||||
/// List of users currently allowed to vote |
||||
var private array<UserID> allowedVoters; |
||||
|
||||
protected function Constructor() { |
||||
status = VPM_Uninitialized; |
||||
policyCanLeave = false; |
||||
policyCanChangeVote = false; |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
_.memory.FreeMany(allowedVoters); |
||||
_.memory.FreeMany(votesFor); |
||||
_.memory.FreeMany(votesAgainst); |
||||
_.memory.FreeMany(storedVotesFor); |
||||
_.memory.FreeMany(storedVotesAgainst); |
||||
allowedVoters.length = 0; |
||||
votesFor.length = 0; |
||||
votesAgainst.length = 0; |
||||
storedVotesFor.length = 0; |
||||
storedVotesAgainst.length = 0; |
||||
} |
||||
|
||||
/// Initializes voting by providing it with a set of policies to follow. |
||||
public final function Initialize(VotingPolicies policies) { |
||||
if (status == VPM_Uninitialized) { |
||||
policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote); |
||||
policyCanChangeVote = |
||||
(policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote); |
||||
} |
||||
status = VPM_InProgress; |
||||
} |
||||
|
||||
/// Returns whether voting has already concluded. |
||||
/// |
||||
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check |
||||
/// whether either of them was enough to conclude the voting result. |
||||
public final function bool HasEnded() { |
||||
return (status != VPM_Uninitialized && status != VPM_InProgress); |
||||
} |
||||
|
||||
/// Returns current status of voting. |
||||
/// |
||||
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check |
||||
/// whether either of them was enough to conclude the voting result. |
||||
public final function VotingModelStatus GetStatus() { |
||||
return status; |
||||
} |
||||
|
||||
/// Changes set of [`User`]s that are allowed to vote. |
||||
/// |
||||
/// Generally you want to provide this method with a list of current players, optionally filtered |
||||
/// from spectators, users not in priviledged group or any other relevant criteria. |
||||
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { |
||||
local int i; |
||||
|
||||
_.memory.FreeMany(allowedVoters); |
||||
allowedVoters.length = 0; |
||||
for (i = 0; i < potentialVoters.length; i += 1) { |
||||
potentialVoters[i].NewRef(); |
||||
allowedVoters[i] = potentialVoters[i]; |
||||
} |
||||
RestoreStoredVoters(potentialVoters); |
||||
FilterCurrentVoters(potentialVoters); |
||||
RecountVotes(); |
||||
} |
||||
|
||||
/// Attempts to add a vote from specified user. |
||||
/// |
||||
/// Adding a vote can fail if [`voter`] isn't allowed to vote or has already voted and policies |
||||
/// forbid changing that vote. |
||||
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) { |
||||
local bool votesSameWay; |
||||
local PlayerVoteStatus currentVote; |
||||
|
||||
if (status != VPM_InProgress) { |
||||
return VFR_VotingEnded; |
||||
} |
||||
if (!IsVotingAllowedFor(voter)) { |
||||
return VFR_NotAllowed; |
||||
} |
||||
currentVote = HasVoted(voter); |
||||
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) |
||||
|| (!voteForSuccess && currentVote == PVS_VoteAgainst); |
||||
if (votesSameWay) { |
||||
return VFR_AlreadyVoted; |
||||
} |
||||
if (!policyCanChangeVote && currentVote != PVS_NoVote) { |
||||
return VFR_CannotChangeVote; |
||||
} |
||||
EraseVote(voter); |
||||
voter.NewRef(); |
||||
if (voteForSuccess) { |
||||
votesFor[votesFor.length] = voter; |
||||
} else { |
||||
votesAgainst[votesAgainst.length] = voter; |
||||
} |
||||
RecountVotes(); |
||||
return VFR_Success; |
||||
} |
||||
|
||||
/// Checks if the provided user is allowed to vote based on the current list of potential voters. |
||||
/// |
||||
/// The right to vote is decided solely by the list of potential voters set using |
||||
/// [`UpdatePotentialVoters()`]. |
||||
/// However, even if a user is on the list of potential voters, they may not be allowed to vote if |
||||
/// they have already cast a vote and the voting policies do not allow vote changes. |
||||
/// |
||||
/// Returns true if the user is allowed to vote, false otherwise. |
||||
public final function bool IsVotingAllowedFor(UserID voter) { |
||||
local int i; |
||||
|
||||
if (voter == none) { |
||||
return false; |
||||
} |
||||
for (i = 0; i < allowedVoters.length; i += 1) { |
||||
if (voter.IsEqual(allowedVoters[i])) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// Returns the current vote status for the given voter. |
||||
/// |
||||
/// If the voter was previously allowed to vote, voted, and had their right to vote revoked, their |
||||
/// vote will only count if policies allow voters to leave mid-vote. |
||||
/// Otherwise, the method will return [`PVS_NoVote`]. |
||||
public final function PlayerVoteStatus GetVote(UserID voter) { |
||||
local int i; |
||||
|
||||
if (voter == none) { |
||||
return PVS_NoVote; |
||||
} |
||||
for (i = 0; i < votesFor.length; i += 1) { |
||||
if (voter.IsEqual(votesFor[i])) { |
||||
return PVS_VoteFor; |
||||
} |
||||
} |
||||
for (i = 0; i < votesAgainst.length; i += 1) { |
||||
if (voter.IsEqual(votesAgainst[i])) { |
||||
return PVS_VoteAgainst; |
||||
} |
||||
} |
||||
if (policyCanLeave) { |
||||
for (i = 0; i < storedVotesFor.length; i += 1) { |
||||
if (voter.IsEqual(storedVotesFor[i])) { |
||||
return PVS_VoteFor; |
||||
} |
||||
} |
||||
for (i = 0; i < storedVotesAgainst.length; i += 1) { |
||||
if (voter.IsEqual(storedVotesAgainst[i])) { |
||||
return PVS_VoteAgainst; |
||||
} |
||||
} |
||||
} |
||||
return PVS_NoVote; |
||||
} |
||||
|
||||
/// Returns amount of current valid votes for the success of this voting. |
||||
public final function int GetVotesFor() { |
||||
if (policyCanLeave) { |
||||
return votesFor.length + storedVotesFor.length; |
||||
} else { |
||||
return votesFor.length; |
||||
} |
||||
} |
||||
|
||||
/// Returns amount of current valid votes against the success of this voting. |
||||
public final function int GetVotesAgainst() { |
||||
if (policyCanLeave) { |
||||
return votesAgainst.length + storedVotesAgainst.length; |
||||
} else { |
||||
return votesAgainst.length; |
||||
} |
||||
} |
||||
|
||||
/// Returns amount of users that are currently allowed to vote in this voting. |
||||
public final function int GetTotalPossibleVotes() { |
||||
if (policyCanLeave) { |
||||
return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length; |
||||
} else { |
||||
return allowedVoters.length; |
||||
} |
||||
} |
||||
|
||||
// Checks if provided user has already voted. |
||||
// Only checks among users that are currently allowed to vote, even if their past vote still counts. |
||||
private final function PlayerVoteStatus HasVoted(UserID voter) { |
||||
local int i; |
||||
|
||||
if (voter == none) { |
||||
return PVS_NoVote; |
||||
} |
||||
for (i = 0; i < votesFor.length; i += 1) { |
||||
if (voter.IsEqual(votesFor[i])) { |
||||
return PVS_VoteFor; |
||||
} |
||||
} |
||||
for (i = 0; i < votesAgainst.length; i += 1) { |
||||
if (voter.IsEqual(votesAgainst[i])) { |
||||
return PVS_VoteAgainst; |
||||
} |
||||
} |
||||
return PVS_NoVote; |
||||
} |
||||
|
||||
private final function RecountVotes() { |
||||
local bool canOverturn, everyoneVoted; |
||||
local int totalPossibleVotes; |
||||
local int totalVotesFor, totalVotesAgainst; |
||||
local int lowerVoteCount, upperVoteCount, undecidedVoteCount; |
||||
|
||||
if (status != VPM_InProgress) { |
||||
return; |
||||
} |
||||
totalVotesFor = GetVotesFor(); |
||||
totalVotesAgainst = GetVotesAgainst(); |
||||
totalPossibleVotes = GetTotalPossibleVotes(); |
||||
lowerVoteCount = Min(totalVotesFor, totalVotesAgainst); |
||||
upperVoteCount = Max(totalVotesFor, totalVotesAgainst); |
||||
undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount); |
||||
everyoneVoted = (undecidedVoteCount <= 0); |
||||
canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount; |
||||
if (everyoneVoted || !canOverturn) { |
||||
if (totalVotesFor > totalVotesAgainst) { |
||||
status = VPM_Success; |
||||
} else if (totalVotesFor < totalVotesAgainst) { |
||||
status = VPM_Failure; |
||||
} else { |
||||
status = VPM_Draw; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private final function EraseVote(UserID voter) { |
||||
local int i; |
||||
|
||||
if (voter == none) { |
||||
return; |
||||
} |
||||
while (i < votesFor.length) { |
||||
if (voter.IsEqual(votesFor[i])) { |
||||
_.memory.Free(votesFor[i]); |
||||
votesFor.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
i = 0; |
||||
while (i < votesAgainst.length) { |
||||
if (voter.IsEqual(votesAgainst[i])) { |
||||
_.memory.Free(votesAgainst[i]); |
||||
votesAgainst.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private final function RestoreStoredVoters(array<UserID> potentialVoters) { |
||||
local int i, j; |
||||
local bool isPotentialVoter; |
||||
|
||||
while (i < storedVotesFor.length) { |
||||
isPotentialVoter = false; |
||||
for (j = 0; j < potentialVoters.length; j += 1) { |
||||
if (storedVotesFor[i].IsEqual(potentialVoters[j])) { |
||||
isPotentialVoter = true; |
||||
break; |
||||
} |
||||
} |
||||
if (isPotentialVoter) { |
||||
votesFor[votesFor.length] = storedVotesFor[i]; |
||||
storedVotesFor.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
i = 0; |
||||
while (i < storedVotesAgainst.length) { |
||||
isPotentialVoter = false; |
||||
for (j = 0; j < potentialVoters.length; j += 1) { |
||||
if (storedVotesAgainst[i].IsEqual(potentialVoters[j])) { |
||||
isPotentialVoter = true; |
||||
break; |
||||
} |
||||
} |
||||
if (isPotentialVoter) { |
||||
votesAgainst[votesAgainst.length] = storedVotesAgainst[i]; |
||||
storedVotesAgainst.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private final function FilterCurrentVoters(array<UserID> potentialVoters) { |
||||
local int i, j; |
||||
local bool isPotentialVoter; |
||||
|
||||
while (i < votesFor.length) { |
||||
isPotentialVoter = false; |
||||
for (j = 0; j < potentialVoters.length; j += 1) { |
||||
if (votesFor[i].IsEqual(potentialVoters[j])) { |
||||
isPotentialVoter = true; |
||||
break; |
||||
} |
||||
} |
||||
if (isPotentialVoter) { |
||||
i += 1; |
||||
} else { |
||||
storedVotesFor[storedVotesFor.length] = votesFor[i]; |
||||
votesFor.Remove(i, 1); |
||||
} |
||||
} |
||||
i = 0; |
||||
while (i < votesAgainst.length) { |
||||
isPotentialVoter = false; |
||||
for (j = 0; j < potentialVoters.length; j += 1) { |
||||
if (votesAgainst[i].IsEqual(potentialVoters[j])) { |
||||
isPotentialVoter = true; |
||||
break; |
||||
} |
||||
} |
||||
if (isPotentialVoter) { |
||||
i += 1; |
||||
} else { |
||||
storedVotesAgainst[storedVotesAgainst.length] = votesAgainst[i]; |
||||
votesAgainst.Remove(i, 1); |
||||
} |
||||
} |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,75 @@
|
||||
class VotingSettings extends FeatureConfig |
||||
perobjectconfig |
||||
config(AcediaVoting); |
||||
|
||||
/// Determines the duration of the voting period, specified in seconds. |
||||
var public config float votingTime; |
||||
|
||||
/// Determines whether spectators are allowed to vote. |
||||
var public config bool allowSpectatorVoting; |
||||
/// Specifies which group(s) of players are allowed to see who makes what vote. |
||||
var public config array<string> allowedToSeeVotesGroup; |
||||
/// Specifies which group(s) of players are allowed to vote. |
||||
var public config array<string> allowedToVoteGroup; |
||||
|
||||
protected function HashTable ToData() { |
||||
local int i; |
||||
local HashTable data; |
||||
local ArrayList arrayOfTexts; |
||||
|
||||
data = __().collections.EmptyHashTable(); |
||||
data.SetFloat(P("votingTime"), votingTime); |
||||
data.SetBool(P("allowSpectatorVoting"), allowSpectatorVoting); |
||||
|
||||
arrayOfTexts = _.collections.EmptyArrayList(); |
||||
for (i = 0; i < allowedToSeeVotesGroup.length; i += 1) { |
||||
arrayOfTexts.AddString(allowedToSeeVotesGroup[i]); |
||||
} |
||||
data.SetItem(P("allowedToSeeVotesGroup"), arrayOfTexts); |
||||
_.memory.Free(arrayOfTexts); |
||||
|
||||
arrayOfTexts = _.collections.EmptyArrayList(); |
||||
for (i = 0; i < allowedToVoteGroup.length; i += 1) { |
||||
arrayOfTexts.AddString(allowedToVoteGroup[i]); |
||||
} |
||||
data.SetItem(P("allowedToVoteGroup"), arrayOfTexts); |
||||
_.memory.Free(arrayOfTexts); |
||||
return data; |
||||
} |
||||
|
||||
protected function FromData(HashTable source) { |
||||
local int i; |
||||
local ArrayList arrayOfTexts; |
||||
|
||||
if (source == none) { |
||||
return; |
||||
} |
||||
votingTime = source.GetFloat(P("votingTime"), 30.0); |
||||
allowSpectatorVoting = source.GetBool(P("allowSpectatorVoting"), false); |
||||
|
||||
allowedToSeeVotesGroup.length = 0; |
||||
arrayOfTexts = source.GetArrayList(P("allowedToSeeVotesGroup")); |
||||
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||
allowedToSeeVotesGroup[allowedToSeeVotesGroup.length] = arrayOfTexts.GetString(i); |
||||
} |
||||
_.memory.Free(arrayOfTexts); |
||||
|
||||
allowedToVoteGroup.length = 0; |
||||
arrayOfTexts = source.GetArrayList(P("allowedToVoteGroup")); |
||||
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||
allowedToVoteGroup[allowedToVoteGroup.length] = arrayOfTexts.GetString(i); |
||||
} |
||||
_.memory.Free(arrayOfTexts); |
||||
} |
||||
|
||||
protected function DefaultIt() { |
||||
votingTime = 30.0; |
||||
allowSpectatorVoting = false; |
||||
allowedToSeeVotesGroup.length = 0; |
||||
allowedToVoteGroup.length = 0; |
||||
allowedToVoteGroup[0] = "everybody"; |
||||
} |
||||
|
||||
defaultproperties { |
||||
configName = "AcediaVoting" |
||||
} |
@ -1,697 +0,0 @@
|
||||
/** |
||||
* 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 |
||||
*------------------------------------------------------------------------------ |
||||
* 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 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 |
||||
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<EPlayer> 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 |
||||
// "<command> <sub_command>"). |
||||
// 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<Parameter> required; |
||||
var array<Parameter> 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<Parameter> required; |
||||
var array<Parameter> 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<SubCommand> subCommands; |
||||
var protected array<Option> options; |
||||
var protected bool requiresTarget; |
||||
}; |
||||
var private Data commandData; |
||||
|
||||
// We do not really ever need to create more than one instance of each class |
||||
// of `Command`, so we will simply store and reuse one created instance. |
||||
var private Command mainInstance; |
||||
|
||||
/** |
||||
* When command is being executed we create several instances of |
||||
* `ConsoleWriter` that can be used for command output. They will also be |
||||
* automatically deallocated once command is executed. |
||||
* DO NOT modify them or deallocate any of them manually. |
||||
* This should make output more convenient and standardized. |
||||
* |
||||
* 1. `publicConsole` - sends messages to all present players; |
||||
* 2. `callerConsole` - sends messages to the player that |
||||
* called the command; |
||||
* 3. `targetConsole` - sends messages to the player that is currently |
||||
* being targeted (different each call of `ExecutedFor()` and |
||||
* `none` during `Executed()` call); |
||||
* 4. `othersConsole` - sends messaged to every player that is |
||||
* neither "caller" or "target". |
||||
*/ |
||||
var protected ConsoleWriter publicConsole, othersConsole; |
||||
var protected ConsoleWriter callerConsole, targetConsole; |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
local CommandDataBuilder dataBuilder; |
||||
dataBuilder = |
||||
CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
||||
BuildData(dataBuilder); |
||||
commandData = dataBuilder.BorrowData(); |
||||
dataBuilder.FreeSelf(); |
||||
dataBuilder = none; |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
local int i; |
||||
local array<SubCommand> subCommands; |
||||
local array<Option> options; |
||||
|
||||
DeallocateConsoles(); |
||||
_.memory.Free(commandData.name); |
||||
_.memory.Free(commandData.summary); |
||||
subCommands = commandData.subCommands; |
||||
for (i = 0; i < options.length; i += 1) |
||||
{ |
||||
_.memory.Free(subCommands[i].name); |
||||
_.memory.Free(subCommands[i].description); |
||||
CleanParameters(subCommands[i].required); |
||||
CleanParameters(subCommands[i].optional); |
||||
subCommands[i].required.length = 0; |
||||
subCommands[i].optional.length = 0; |
||||
} |
||||
commandData.subCommands.length = 0; |
||||
options = commandData.options; |
||||
for (i = 0; i < options.length; i += 1) |
||||
{ |
||||
_.memory.Free(options[i].longName); |
||||
_.memory.Free(options[i].description); |
||||
CleanParameters(options[i].required); |
||||
CleanParameters(options[i].optional); |
||||
options[i].required.length = 0; |
||||
options[i].optional.length = 0; |
||||
} |
||||
commandData.options.length = 0; |
||||
} |
||||
|
||||
private final function CleanParameters(array<Parameter> parameters) |
||||
{ |
||||
local int i; |
||||
|
||||
for (i = 0; i < parameters.length; i += 1) |
||||
{ |
||||
_.memory.Free(parameters[i].displayName); |
||||
_.memory.Free(parameters[i].variableName); |
||||
_.memory.Free(parameters[i].aliasSourceName); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Overload this method to use `builder` to define parameters and options for |
||||
* your command. |
||||
* |
||||
* @param builder Builder that can be used to define your commands parameters |
||||
* and options. Do not deallocate. |
||||
*/ |
||||
protected function BuildData(CommandDataBuilder builder){} |
||||
|
||||
/** |
||||
* Overload this method to perform required actions when |
||||
* your command is called. |
||||
* |
||||
* @param arguments `struct` filled with parameters that your command |
||||
* has been called with. Guaranteed to not be in error state. |
||||
* @param instigator Player that instigated this execution. |
||||
*/ |
||||
protected function Executed(CallData arguments, EPlayer instigator){} |
||||
|
||||
/** |
||||
* Overload this method to perform required actions when your command is called |
||||
* with a given player as a target. If several players have been specified - |
||||
* this method will be called once for each. |
||||
* |
||||
* If your command does not require a target - this method will not be called. |
||||
* |
||||
* @param target Player that this command must perform an action on. |
||||
* @param arguments `struct` filled with parameters that your command |
||||
* has been called with. Guaranteed to not be in error state and contain |
||||
* all the required data. |
||||
* @param instigator Player that instigated this call. |
||||
*/ |
||||
protected function ExecutedFor( |
||||
EPlayer target, |
||||
CallData arguments, |
||||
EPlayer instigator){} |
||||
|
||||
/** |
||||
* Returns an instance of command (of particular class) that is stored |
||||
* "as a singleton" in command's class itself. Do not deallocate it. |
||||
*/ |
||||
public final static function Command GetInstance() |
||||
{ |
||||
if (default.mainInstance == none) { |
||||
default.mainInstance = Command(__().memory.Allocate(default.class)); |
||||
} |
||||
return default.mainInstance; |
||||
} |
||||
|
||||
/** |
||||
* Forces command to process (parse) player's input, producing a structure |
||||
* with parsed data in Acedia's format instead. |
||||
* |
||||
* @see `Execute()` for actually performing command's actions. |
||||
* |
||||
* @param parser Parser that contains command input. |
||||
* @param callerPlayer Player that initiated this command's call, |
||||
* necessary for parsing player list (since it can point at |
||||
* the caller player). |
||||
* @param subCommandName This method can optionally specify sub-command to |
||||
* caller command to use. If this argument's value is `none` - sub-command |
||||
* name will be parsed from the `parser`'s data. |
||||
* @return `CallData` structure that contains all the information about |
||||
* parameters specified in `parser`'s contents. |
||||
* Returned structure contains objects that must be deallocated, |
||||
* which can easily be done by the auxiliary `DeallocateCallData()` method. |
||||
*/ |
||||
public final function CallData ParseInputWith( |
||||
Parser parser, |
||||
EPlayer callerPlayer, |
||||
optional BaseText subCommandName) |
||||
{ |
||||
local array<EPlayer> targetPlayers; |
||||
local CommandParser commandParser; |
||||
local CallData callData; |
||||
|
||||
if (parser == none || !parser.Ok()) |
||||
{ |
||||
callData.parsingError = CET_BadParser; |
||||
return callData; |
||||
} |
||||
// Parse targets and handle errors that can arise here |
||||
if (commandData.requiresTarget) |
||||
{ |
||||
targetPlayers = ParseTargets(parser, callerPlayer); |
||||
if (!parser.Ok()) |
||||
{ |
||||
callData.parsingError = CET_IncorrectTargetList; |
||||
return callData; |
||||
} |
||||
if (targetPlayers.length <= 0) |
||||
{ |
||||
callData.parsingError = CET_EmptyTargetList; |
||||
return callData; |
||||
} |
||||
} |
||||
// Parse parameters themselves |
||||
commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); |
||||
callData = commandParser.ParseWith( |
||||
parser, |
||||
commandData, |
||||
callerPlayer, |
||||
subCommandName); |
||||
callData.targetPlayers = targetPlayers; |
||||
commandParser.FreeSelf(); |
||||
return callData; |
||||
} |
||||
|
||||
/** |
||||
* Executes caller `Command` with data provided by `callData` if it is in |
||||
* a correct state and reports error to `callerPlayer` if |
||||
* `callData` is invalid. |
||||
* |
||||
* @param callData Data about parameters, options, etc. with which |
||||
* caller `Command` is to be executed. |
||||
* @param callerPlayer Player that should be considered responsible for |
||||
* executing this `Command`. |
||||
* @return `true` if command was successfully executed and `false` otherwise. |
||||
* Execution is considered successful if `Execute()` call was made, |
||||
* regardless of whether `Command` can actually perform required action. |
||||
* For example, giving a weapon to a player can fail because he does not |
||||
* have enough space in his inventory, but it will still be considered |
||||
* a successful execution as far as return value is concerned. |
||||
*/ |
||||
public final function bool Execute(CallData callData, EPlayer callerPlayer) |
||||
{ |
||||
local int i; |
||||
local array<EPlayer> targetPlayers; |
||||
|
||||
if (callerPlayer == none) return false; |
||||
if (!callerPlayer.IsExistent()) return false; |
||||
|
||||
// Report or execute |
||||
if (callData.parsingError != CET_None) |
||||
{ |
||||
ReportError(callData, callerPlayer); |
||||
return false; |
||||
} |
||||
targetPlayers = callData.targetPlayers; |
||||
publicConsole = _.console.ForAll(); |
||||
callerConsole = _.console.For(callerPlayer); |
||||
callerConsole |
||||
.Write(P("Executing command `")) |
||||
.Write(commandData.name) |
||||
.Say(P("`")); |
||||
// `othersConsole` should also exist in time for `Executed()` call |
||||
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); |
||||
Executed(callData, callerPlayer); |
||||
_.memory.Free(othersConsole); |
||||
if (commandData.requiresTarget) |
||||
{ |
||||
for (i = 0; i < targetPlayers.length; i += 1) |
||||
{ |
||||
targetConsole = _.console.For(targetPlayers[i]); |
||||
othersConsole = _.console |
||||
.ForAll() |
||||
.ButPlayer(callerPlayer) |
||||
.ButPlayer(targetPlayers[i]); |
||||
ExecutedFor(targetPlayers[i], callData, callerPlayer); |
||||
_.memory.Free(othersConsole); |
||||
_.memory.Free(targetConsole); |
||||
} |
||||
} |
||||
othersConsole = none; |
||||
targetConsole = none; |
||||
DeallocateConsoles(); |
||||
return true; |
||||
} |
||||
|
||||
private final function DeallocateConsoles() |
||||
{ |
||||
if (publicConsole != none && publicConsole.IsAllocated()) { |
||||
_.memory.Free(publicConsole); |
||||
} |
||||
if (callerConsole != none && callerConsole.IsAllocated()) { |
||||
_.memory.Free(callerConsole); |
||||
} |
||||
if (targetConsole != none && targetConsole.IsAllocated()) { |
||||
_.memory.Free(targetConsole); |
||||
} |
||||
if (othersConsole != none && othersConsole.IsAllocated()) { |
||||
_.memory.Free(othersConsole); |
||||
} |
||||
publicConsole = none; |
||||
callerConsole = none; |
||||
targetConsole = none; |
||||
othersConsole = none; |
||||
} |
||||
|
||||
/** |
||||
* Auxiliary method that cleans up all data and deallocates all objects inside |
||||
* provided `callData` structure. |
||||
* |
||||
* @param callData Structure to clean. All stored data will be cleared, |
||||
* meaning that `DeallocateCallData()` method takes ownership of |
||||
* this parameter. |
||||
*/ |
||||
public final static function DeallocateCallData(/* take */ CallData callData) |
||||
{ |
||||
__().memory.Free(callData.subCommandName); |
||||
__().memory.Free(callData.parameters); |
||||
__().memory.Free(callData.options); |
||||
__().memory.Free(callData.errorCause); |
||||
__().memory.FreeMany(callData.targetPlayers); |
||||
if (callData.targetPlayers.length > 0) { |
||||
callData.targetPlayers.length = 0; |
||||
} |
||||
} |
||||
|
||||
// Reports given error to the `callerPlayer`, appropriately picking |
||||
// message color |
||||
private final function ReportError(CallData callData, EPlayer callerPlayer) |
||||
{ |
||||
local Text errorMessage; |
||||
local ConsoleWriter console; |
||||
|
||||
if (callerPlayer == none) return; |
||||
if (!callerPlayer.IsExistent()) return; |
||||
|
||||
// Setup console color |
||||
console = callerPlayer.BorrowConsole(); |
||||
if (callData.parsingError == CET_EmptyTargetList) { |
||||
console.UseColor(_.color.textWarning); |
||||
} |
||||
else { |
||||
console.UseColor(_.color.textFailure); |
||||
} |
||||
// Send message |
||||
errorMessage = PrintErrorMessage(callData); |
||||
console.Say(errorMessage); |
||||
errorMessage.FreeSelf(); |
||||
// Restore console color |
||||
console.ResetColor().Flush(); |
||||
} |
||||
|
||||
private final function Text PrintErrorMessage(CallData callData) |
||||
{ |
||||
local Text result; |
||||
local MutableText builder; |
||||
|
||||
builder = _.text.Empty(); |
||||
switch (callData.parsingError) |
||||
{ |
||||
case CET_BadParser: |
||||
builder.Append(P("Internal error occurred: invalid parser")); |
||||
break; |
||||
case CET_NoSubCommands: |
||||
builder.Append(P("Ill defined command: no subcommands")); |
||||
break; |
||||
case CET_BadSubCommand: |
||||
builder.Append(P("Ill defined sub-command: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_NoRequiredParam: |
||||
builder.Append(P("Missing required parameter: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_NoRequiredParamForOption: |
||||
builder.Append(P("Missing required parameter for option: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnknownOption: |
||||
builder.Append(P("Invalid option specified: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnknownShortOption: |
||||
builder.Append(P("Invalid short option specified")); |
||||
break; |
||||
case CET_RepeatedOption: |
||||
builder.Append(P("Option specified several times: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_UnusedCommandParameters: |
||||
builder.Append(P("Part of command could not be parsed: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_MultipleOptionsWithParams: |
||||
builder.Append(P( "Multiple short options in one declarations" |
||||
@ "require parameters: ")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_IncorrectTargetList: |
||||
builder.Append(P("Target players are incorrectly specified.")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
case CET_EmptyTargetList: |
||||
builder.Append(P("List of target players is empty")) |
||||
.Append(callData.errorCause); |
||||
break; |
||||
default: |
||||
} |
||||
result = builder.Copy(); |
||||
builder.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
// Auxiliary method for parsing list of targeted players. |
||||
// Assumes given parser is not `none` and not in a failed state. |
||||
// If parsing failed, guaranteed to return an empty array. |
||||
private final function array<EPlayer> ParseTargets( |
||||
Parser parser, |
||||
EPlayer callerPlayer) |
||||
{ |
||||
local array<EPlayer> targetPlayers; |
||||
local PlayersParser targetsParser; |
||||
|
||||
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); |
||||
targetsParser.SetSelf(callerPlayer); |
||||
targetsParser.ParseWith(parser); |
||||
if (parser.Ok()) { |
||||
targetPlayers = targetsParser.GetPlayers(); |
||||
} |
||||
targetsParser.FreeSelf(); |
||||
return targetPlayers; |
||||
} |
||||
|
||||
/** |
||||
* Returns name (in lower case) of the caller command class. |
||||
* |
||||
* @return Name (in lower case) of the caller command class. |
||||
* Guaranteed to be not `none`. |
||||
*/ |
||||
public final function Text GetName() |
||||
{ |
||||
if (commandData.name == none) { |
||||
return P("").Copy(); |
||||
} |
||||
return commandData.name.LowerCopy(); |
||||
} |
||||
|
||||
/** |
||||
* Returns group name (in lower case) of the caller command class. |
||||
* |
||||
* @return Group name (in lower case) of the caller command class. |
||||
* Guaranteed to be not `none`. |
||||
*/ |
||||
public final function Text GetGroupName() |
||||
{ |
||||
if (commandData.group == none) { |
||||
return P("").Copy(); |
||||
} |
||||
return commandData.group.LowerCopy(); |
||||
} |
||||
|
||||
/** |
||||
* Returns `Command.Data` struct that describes caller `Command`. |
||||
* |
||||
* @return `Command.Data` that describes caller `Command`. Returned struct |
||||
* contains `Text` references that are used internally by the `Command` |
||||
* and not their copies. |
||||
* Generally this is undesired approach and leaves `Command` more |
||||
* vulnerable to modification, but copying all the data inside would not |
||||
* only introduce a largely pointless computational overhead, but also |
||||
* would require some cumbersome logic. This might change in the future, |
||||
* so deallocating any objects in the returned `struct` would lead to |
||||
* undefined behavior. |
||||
*/ |
||||
public final function Data BorrowData() |
||||
{ |
||||
return commandData; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,84 +0,0 @@
|
||||
/** |
||||
* Config object for `Commands_Feature`. |
||||
* Copyright 2021-2022 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 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<string> allowedPlayers; |
||||
|
||||
protected function HashTable ToData() |
||||
{ |
||||
local int i; |
||||
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; |
||||
|
||||
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(); |
||||
} |
||||
|
||||
protected function DefaultIt() |
||||
{ |
||||
useChatInput = true; |
||||
useMutateInput = true; |
||||
chatCommandPrefix = "!"; |
||||
allowedPlayers.length = 0; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
configName = "AcediaSystem" |
||||
useChatInput = true |
||||
useMutateInput = true |
||||
chatCommandPrefix = "!" |
||||
} |
@ -1,627 +0,0 @@
|
||||
/** |
||||
* 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-2022 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 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. |
||||
*/ |
||||
|
||||
// Delimiters that always separate command name from it's parameters |
||||
var private array<Text> commandDelimiters; |
||||
// Registered commands, recorded as (<command_name>, <command_instance>) pairs. |
||||
// Keys should be deallocated when their entry is removed. |
||||
var private HashTable registeredCommands; |
||||
// `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs |
||||
// to allow quick fetch of commands belonging to a single group |
||||
var private HashTable groupedCommands; |
||||
|
||||
// When this flag is set to true, mutate input becomes available |
||||
// despite `useMutateInput` flag to allow to unlock server in case of an error |
||||
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`. |
||||
var private /*config*/ bool useChatInput; |
||||
// 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 "!". |
||||
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<string> 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(); |
||||
RegisterCommand(class'ACommandHelp'); |
||||
RegisterCommand(class'ACommandNotify'); |
||||
// Macro selector |
||||
commandDelimiters[0] = _.text.FromString("@"); |
||||
// Key selector |
||||
commandDelimiters[1] = _.text.FromString("#"); |
||||
// Player array (possibly JSON array) |
||||
commandDelimiters[2] = _.text.FromString("["); |
||||
// Negation of the selector |
||||
commandDelimiters[3] = _.text.FromString("!"); |
||||
} |
||||
|
||||
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; |
||||
commandDelimiters.length = 0; |
||||
} |
||||
|
||||
protected function SwapConfig(FeatureConfig config) |
||||
{ |
||||
local Commands newConfig; |
||||
|
||||
newConfig = Commands(config); |
||||
if (newConfig == none) { |
||||
return; |
||||
} |
||||
_.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(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* `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; |
||||
|
||||
if (!IsEnabled()) |
||||
{ |
||||
autoConfig = GetAutoEnabledConfig(); |
||||
EnableMe(autoConfig); |
||||
__().memory.Free(autoConfig); |
||||
} |
||||
feature = Commands_Feature(GetEnabledInstance()); |
||||
if ( !feature.emergencyEnabledMutate |
||||
&& !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) |
||||
{ |
||||
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() |
||||
{ |
||||
local Commands_Feature instance; |
||||
|
||||
instance = Commands_Feature(GetEnabledInstance()); |
||||
if (instance != none) { |
||||
return instance.useChatInput; |
||||
} |
||||
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() |
||||
{ |
||||
local Commands_Feature instance; |
||||
|
||||
instance = Commands_Feature(GetEnabledInstance()); |
||||
if (instance != none) { |
||||
return instance.useMutateInput; |
||||
} |
||||
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() |
||||
{ |
||||
local Commands_Feature instance; |
||||
|
||||
instance = Commands_Feature(GetEnabledInstance()); |
||||
if (instance != none && instance.chatCommandPrefix != none) { |
||||
return instance.chatCommandPrefix.Copy(); |
||||
} |
||||
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<Command> commandClass) |
||||
{ |
||||
local Text commandName, groupName; |
||||
local ArrayList groupArray; |
||||
local Command newCommandInstance, existingCommandInstance; |
||||
|
||||
if (commandClass == none) return; |
||||
if (registeredCommands == none) return; |
||||
|
||||
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) |
||||
{ |
||||
_.logger.Auto(errCommandDuplicate) |
||||
.ArgClass(existingCommandInstance.class) |
||||
.Arg(commandName) |
||||
.ArgClass(commandClass); |
||||
_.memory.Free(groupName); |
||||
_.memory.Free(newCommandInstance); |
||||
_.memory.Free(existingCommandInstance); |
||||
return; |
||||
} |
||||
// Otherwise record new command |
||||
// `commandName` used as a key, do not deallocate it |
||||
registeredCommands.SetItem(commandName, newCommandInstance); |
||||
// Add to grouped collection |
||||
groupArray = groupedCommands.GetArrayList(groupName); |
||||
if (groupArray == none) { |
||||
groupArray = _.collections.EmptyArrayList(); |
||||
} |
||||
groupArray.AddItem(newCommandInstance); |
||||
groupedCommands.SetItem(groupName, groupArray); |
||||
_.memory.Free(groupArray); |
||||
_.memory.Free(groupName); |
||||
_.memory.Free(commandName); |
||||
_.memory.Free(newCommandInstance); |
||||
} |
||||
|
||||
/** |
||||
* 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<Command> commandClass) |
||||
{ |
||||
local int i; |
||||
local CollectionIterator iter; |
||||
local Command nextCommand; |
||||
local Text nextCommandName; |
||||
local array<Text> commandGroup; |
||||
local array<Text> keysToRemove; |
||||
|
||||
if (commandClass == none) return; |
||||
if (registeredCommands == none) return; |
||||
|
||||
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); |
||||
continue; |
||||
} |
||||
keysToRemove[keysToRemove.length] = nextCommandName; |
||||
commandGroup[commandGroup.length] = nextCommand.GetGroupName(); |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
iter.FreeSelf(); |
||||
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<Command> commandClass, |
||||
BaseText commandGroup) |
||||
{ |
||||
local int i; |
||||
local ArrayList groupArray; |
||||
local Command nextCommand; |
||||
|
||||
groupArray = groupedCommands.GetArrayList(commandGroup); |
||||
if (groupArray == none) { |
||||
return; |
||||
} |
||||
while (i < groupArray.GetLength()) |
||||
{ |
||||
nextCommand = Command(groupArray.GetItem(i)); |
||||
if (nextCommand != none && nextCommand.class == commandClass) { |
||||
groupArray.RemoveIndex(i); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
if (groupArray.GetLength() == 0) { |
||||
groupedCommands.RemoveItem(commandGroup); |
||||
} |
||||
_.memory.Free(groupArray); |
||||
} |
||||
|
||||
/** |
||||
* 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) |
||||
{ |
||||
local Text commandNameLowerCase; |
||||
local Command commandInstance; |
||||
|
||||
if (commandName == none) return none; |
||||
if (registeredCommands == none) return none; |
||||
|
||||
commandNameLowerCase = commandName.LowerCopy(); |
||||
commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); |
||||
commandNameLowerCase.FreeSelf(); |
||||
return commandInstance; |
||||
} |
||||
|
||||
/** |
||||
* Returns array of names of all available commands. |
||||
* |
||||
* @return Array of names of all available (registered) commands. |
||||
*/ |
||||
public final function array<Text> GetCommandNames() |
||||
{ |
||||
local array<Text> emptyResult; |
||||
|
||||
if (registeredCommands != none) { |
||||
return registeredCommands.GetTextKeys(); |
||||
} |
||||
return emptyResult; |
||||
} |
||||
|
||||
/** |
||||
* Returns array of names of all available commands belonging to the group |
||||
* `groupName`. |
||||
* |
||||
* @return Array of names of all available (registered) commands, belonging to |
||||
* the group `groupName`. |
||||
*/ |
||||
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) |
||||
{ |
||||
local int i; |
||||
local ArrayList groupArray; |
||||
local Command nextCommand; |
||||
local array<Text> result; |
||||
|
||||
if (groupedCommands == none) return result; |
||||
groupArray = groupedCommands.GetArrayList(groupName); |
||||
if (groupArray == none) return result; |
||||
|
||||
for (i = 0; i < groupArray.GetLength(); i += 1) |
||||
{ |
||||
nextCommand = Command(groupArray.GetItem(i)); |
||||
if (nextCommand != none) { |
||||
result[result.length] = nextCommand.GetName(); |
||||
} |
||||
_.memory.Free(nextCommand); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns all available command groups' names. |
||||
* |
||||
* @return Array of all available command groups' names. |
||||
*/ |
||||
public final function array<Text> GetGroupsNames() |
||||
{ |
||||
local array<Text> emptyResult; |
||||
|
||||
if (groupedCommands != none) { |
||||
return groupedCommands.GetTextKeys(); |
||||
} |
||||
return emptyResult; |
||||
} |
||||
|
||||
/** |
||||
* Handles user input: finds appropriate command and passes the rest of |
||||
* the arguments to it for further processing. |
||||
* |
||||
* @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) |
||||
{ |
||||
local Parser wrapper; |
||||
|
||||
if (input == none) { |
||||
return; |
||||
} |
||||
wrapper = input.Parse(); |
||||
HandleInputWith(wrapper, callerPlayer); |
||||
wrapper.FreeSelf(); |
||||
} |
||||
|
||||
/** |
||||
* 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; |
||||
} |
||||
callPair = ParseCommandCallPairWith(parser); |
||||
commandInstance = GetCommand(callPair.commandName); |
||||
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); |
||||
commandInstance.DeallocateCallData(callData); |
||||
} |
||||
_.memory.Free(callPair.commandName); |
||||
_.memory.Free(callPair.subCommandName); |
||||
} |
||||
|
||||
// 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 |
||||
{ |
||||
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.") |
||||
} |
@ -1,559 +0,0 @@
|
||||
/** |
||||
* Object for parsing what converting textual description of a group of |
||||
* players into array of `EPlayer`s. Depends on the game context. |
||||
* Copyright 2021-2022 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 PlayersParser extends AcediaObject |
||||
dependson(Parser); |
||||
|
||||
/** |
||||
* # `PlayersParser` |
||||
* |
||||
* This parser is supposed to parse player set definitions as they |
||||
* are used in commands. |
||||
* Basic use is to specify one of the selectors: |
||||
* 1. Key selector: "#<integer>" (examples: "#1", "#5"). |
||||
* This one is used to specify players by their key, assigned to |
||||
* them when they enter the game. This type of selectors can be used |
||||
* when players have hard to type names. |
||||
* 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". |
||||
* "@", "@me", and "@self" are identical and can be used to |
||||
* specify player that called the command. |
||||
* "@admin" can be used to specify all admins in the game at once. |
||||
* "@all" specifies all current players. |
||||
* In future it is planned to make macros extendable by allowing to |
||||
* bind more names to specific groups of players. |
||||
* 3. Name selectors: quoted strings and any other types of string that |
||||
* do not start with either "#" or "@". |
||||
* These specify name prefixes: any player with specified prefix |
||||
* will be considered to match such selector. |
||||
* |
||||
* Negated selectors: "!<selector>". Specifying "!" in front of selector |
||||
* will select all players that do not match it instead. |
||||
* |
||||
* Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". |
||||
* Specified selectors are process in order: from left to right. |
||||
* First selector works as usual and selects a set of players. |
||||
* All the following selectors either |
||||
* expand that list (additive ones, without "!" prefix) |
||||
* or remove specific players from the list (the ones with "!" prefix). |
||||
* Examples of that: |
||||
* *. "[@admin, !@self]" - selects all admins, except the one who called |
||||
* the command (whether he is admin or not). |
||||
* *. "[dkanus, 'mate']" - will select players "dkanus" and "mate". |
||||
* Order also matters, since: |
||||
* *. "[@admin, !@admin]" - won't select anyone, since it will first |
||||
* add all the admins and then remove them. |
||||
* *. "[!@admin, @admin]" - will select everyone, since it will first |
||||
* select everyone who is not an admin and then adds everyone else. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* 1. Allocate `PlayerParser`; |
||||
* 2. Set caller player through `SetSelf()` method to make "@" and "@me" |
||||
* selectors usable; |
||||
* 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that |
||||
* starts with proper players selector; |
||||
* 4. Call `GetPlayers()` to obtain selected players array. |
||||
* |
||||
* ## Implementation |
||||
* |
||||
* When created, `PlayersParser` takes a snapshot (array) of current |
||||
* players on the server. Then `currentSelection` is decided based on whether |
||||
* first selector is positive (initial selection is taken as empty array) or |
||||
* negative (initial selection is taken as full snapshot). |
||||
* After that `PlayersParser` simply goes through specified selectors |
||||
* (in case more than one is specified) and adds or removes appropriate players |
||||
* in `currentSelection`, assuming that `playersSnapshot` is a current full |
||||
* array of players. |
||||
*/ |
||||
|
||||
// Player for which "@", "@me", and "@self" macros will refer |
||||
var private EPlayer selfPlayer; |
||||
// Copy of the list of current players at the moment of allocation of |
||||
// this `PlayersParser`. |
||||
var private array<EPlayer> playersSnapshot; |
||||
// Players, selected according to selectors we have parsed so far |
||||
var private array<EPlayer> currentSelection; |
||||
// Have we parsed our first selector? |
||||
// We need this to know whether to start with the list of |
||||
// all players (if first selector removes them) or |
||||
// with empty list (if first selector adds them). |
||||
var private bool parsedFirstSelector; |
||||
// Will be equal to a single-element array [","], used for parsing |
||||
var private array<Text> selectorDelimiters; |
||||
|
||||
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; |
||||
var const int TOPEN_BRACKET, TCLOSE_BRACKET; |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
// No need to deallocate `currentSelection`, |
||||
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer` |
||||
_.memory.Free(selfPlayer); |
||||
_.memory.FreeMany(playersSnapshot); |
||||
selfPlayer = none; |
||||
parsedFirstSelector = false; |
||||
playersSnapshot.length = 0; |
||||
currentSelection.length = 0; |
||||
} |
||||
|
||||
/** |
||||
* Set a player who will be referred to by "@", "@me" and "@self" macros. |
||||
* |
||||
* @param newSelfPlayer Player who will be referred to by "@", "@me" and |
||||
* "@self" macros. Passing `none` will make it so no one is |
||||
* referred by them. |
||||
*/ |
||||
public final function SetSelf(EPlayer newSelfPlayer) |
||||
{ |
||||
_.memory.Free(selfPlayer); |
||||
selfPlayer = none; |
||||
if (newSelfPlayer != none) { |
||||
selfPlayer = EPlayer(newSelfPlayer.Copy()); |
||||
} |
||||
} |
||||
|
||||
// Insert a new player into currently selected list of players |
||||
// (`currentSelection`) such that there will be no duplicates. |
||||
// `none` values are auto-discarded. |
||||
private final function InsertPlayer(EPlayer toInsert) |
||||
{ |
||||
local int i; |
||||
|
||||
if (toInsert == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < currentSelection.length; i += 1) |
||||
{ |
||||
if (currentSelection[i] == toInsert) { |
||||
return; |
||||
} |
||||
} |
||||
currentSelection[currentSelection.length] = toInsert; |
||||
} |
||||
|
||||
// Adds all the players with specified key (`key`) to the current selection. |
||||
private final function AddByKey(int key) |
||||
{ |
||||
local int i; |
||||
|
||||
for (i = 0; i < playersSnapshot.length; i += 1) |
||||
{ |
||||
if (playersSnapshot[i].GetIdentity().GetKey() == key) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Removes all the players with specified key (`key`) from |
||||
// the current selection. |
||||
private final function RemoveByKey(int key) |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < currentSelection.length) |
||||
{ |
||||
if (currentSelection[i].GetIdentity().GetKey() == key) { |
||||
currentSelection.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Adds all the players with specified name (`name`) to the current selection. |
||||
private final function AddByName(BaseText name) |
||||
{ |
||||
local int i; |
||||
local Text nextPlayerName; |
||||
|
||||
if (name == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < playersSnapshot.length; i += 1) |
||||
{ |
||||
nextPlayerName = playersSnapshot[i].GetName(); |
||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
nextPlayerName.FreeSelf(); |
||||
} |
||||
} |
||||
|
||||
// Removes all the players with specified name (`name`) from |
||||
// the current selection. |
||||
private final function RemoveByName(BaseText name) |
||||
{ |
||||
local int i; |
||||
local Text nextPlayerName; |
||||
|
||||
while (i < currentSelection.length) |
||||
{ |
||||
nextPlayerName = currentSelection[i].GetName(); |
||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||
currentSelection.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
nextPlayerName.FreeSelf(); |
||||
} |
||||
} |
||||
|
||||
// Adds all the admins to the current selection. |
||||
private final function AddAdmins() |
||||
{ |
||||
local int i; |
||||
|
||||
for (i = 0; i < playersSnapshot.length; i += 1) |
||||
{ |
||||
if (playersSnapshot[i].IsAdmin()) { |
||||
InsertPlayer(playersSnapshot[i]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Removes all the admins from the current selection. |
||||
private final function RemoveAdmins() |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < currentSelection.length) |
||||
{ |
||||
if (currentSelection[i].IsAdmin()) { |
||||
currentSelection.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Add all the players specified by `macroText` (from macro "@<macroText>"). |
||||
// Does nothing if there is no such macro. |
||||
private final function AddByMacro(BaseText macroText) |
||||
{ |
||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) |
||||
{ |
||||
AddAdmins(); |
||||
return; |
||||
} |
||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) |
||||
{ |
||||
currentSelection = playersSnapshot; |
||||
return; |
||||
} |
||||
if ( macroText.IsEmpty() |
||||
|| macroText.Compare(T(TSELF), SCASE_INSENSITIVE) |
||||
|| macroText.Compare(T(TME), SCASE_INSENSITIVE)) |
||||
{ |
||||
InsertPlayer(selfPlayer); |
||||
} |
||||
} |
||||
|
||||
// Removes all the players specified by `macroText` |
||||
// (from macro "@<macroText>"). |
||||
// Does nothing if there is no such macro. |
||||
private final function RemoveByMacro(BaseText macroText) |
||||
{ |
||||
local int i; |
||||
|
||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) |
||||
{ |
||||
RemoveAdmins(); |
||||
return; |
||||
} |
||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) |
||||
{ |
||||
currentSelection.length = 0; |
||||
return; |
||||
} |
||||
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) |
||||
{ |
||||
while (i < currentSelection.length) |
||||
{ |
||||
if (currentSelection[i] == selfPlayer) { |
||||
currentSelection.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Parses one selector from `parser`, while accordingly modifying current |
||||
// player selection list. |
||||
private final function ParseSelector(Parser parser) |
||||
{ |
||||
local bool additiveSelector; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
if (!parser.Match(T(TNOT)).Ok()) |
||||
{ |
||||
additiveSelector = true; |
||||
parser.RestoreState(confirmedState); |
||||
} |
||||
// Determine whether we stars with empty or full player list |
||||
if (!parsedFirstSelector) |
||||
{ |
||||
parsedFirstSelector = true; |
||||
if (additiveSelector) { |
||||
currentSelection.length = 0; |
||||
} |
||||
else { |
||||
currentSelection = playersSnapshot; |
||||
} |
||||
} |
||||
// Try all selector types |
||||
confirmedState = parser.GetCurrentState(); |
||||
if (parser.Match(T(TKEY)).Ok()) |
||||
{ |
||||
ParseKeySelector(parser, additiveSelector); |
||||
return; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
if (parser.Match(T(TMACRO)).Ok()) |
||||
{ |
||||
ParseMacroSelector(parser, additiveSelector); |
||||
return; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
ParseNameSelector(parser, additiveSelector); |
||||
} |
||||
|
||||
// Parse key selector (assuming "#" is already consumed), while accordingly |
||||
// modifying current player selection list. |
||||
private final function ParseKeySelector(Parser parser, bool additiveSelector) |
||||
{ |
||||
local int key; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
if (!parser.MInteger(key).Ok()) return; |
||||
|
||||
if (additiveSelector) { |
||||
AddByKey(key); |
||||
} |
||||
else { |
||||
RemoveByKey(key); |
||||
} |
||||
} |
||||
|
||||
// Parse macro selector (assuming "@" is already consumed), while accordingly |
||||
// modifying current player selection list. |
||||
private final function ParseMacroSelector(Parser parser, bool additiveSelector) |
||||
{ |
||||
local MutableText macroName; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
macroName = ParseLiteral(parser); |
||||
if (!parser.Ok()) |
||||
{ |
||||
_.memory.Free(macroName); |
||||
return; |
||||
} |
||||
if (additiveSelector) { |
||||
AddByMacro(macroName); |
||||
} |
||||
else { |
||||
RemoveByMacro(macroName); |
||||
} |
||||
_.memory.Free(macroName); |
||||
} |
||||
|
||||
// Parse name selector, while accordingly modifying current player |
||||
// selection list. |
||||
private final function ParseNameSelector(Parser parser, bool additiveSelector) |
||||
{ |
||||
local MutableText playerName; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return; |
||||
if (!parser.Ok()) return; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
playerName = ParseLiteral(parser); |
||||
if (!parser.Ok() || playerName.IsEmpty()) |
||||
{ |
||||
_.memory.Free(playerName); |
||||
return; |
||||
} |
||||
if (additiveSelector) { |
||||
AddByName(playerName); |
||||
} |
||||
else { |
||||
RemoveByName(playerName); |
||||
} |
||||
_.memory.Free(playerName); |
||||
} |
||||
|
||||
// Reads a string that can either be a body of name selector |
||||
// (some player's name prefix) or of a macro selector (what comes after "@"). |
||||
// This is different from `parser.MString()` because it also uses |
||||
// "," as a separator. |
||||
private final function MutableText ParseLiteral(Parser parser) |
||||
{ |
||||
local MutableText literal; |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return none; |
||||
if (!parser.Ok()) return none; |
||||
|
||||
confirmedState = parser.GetCurrentState(); |
||||
if (!parser.MStringLiteral(literal).Ok()) |
||||
{ |
||||
parser.RestoreState(confirmedState); |
||||
parser.MUntilMany(literal, selectorDelimiters, true); |
||||
} |
||||
return literal; |
||||
} |
||||
|
||||
/** |
||||
* Returns players parsed by the last `ParseWith()` or `Parse()` call. |
||||
* If neither were yet called - returns an empty array. |
||||
* |
||||
* @return players parsed by the last `ParseWith()` or `Parse()` call. |
||||
*/ |
||||
public final function array<EPlayer> GetPlayers() |
||||
{ |
||||
local int i; |
||||
local array<EPlayer> result; |
||||
|
||||
for (i = 0; i < currentSelection.length; i += 1) |
||||
{ |
||||
if (currentSelection[i].IsExistent()) { |
||||
result[result.length] = EPlayer(currentSelection[i].Copy()); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Parses players from `parser` according to the currently present players. |
||||
* |
||||
* Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||
* |
||||
* @param parser `Parser` from which to parse player list. |
||||
* It's state will be set to failed in case the parsing fails. |
||||
* @return `true` if parsing was successful and `false` otherwise. |
||||
*/ |
||||
public final function bool ParseWith(Parser parser) |
||||
{ |
||||
local Parser.ParserState confirmedState; |
||||
|
||||
if (parser == none) return false; |
||||
if (!parser.Ok()) return false; |
||||
if (parser.HasFinished()) return false; |
||||
|
||||
Reset(); |
||||
confirmedState = parser.Skip().GetCurrentState(); |
||||
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) |
||||
{ |
||||
ParseSelector(parser.RestoreState(confirmedState)); |
||||
if (parser.Ok()) { |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
while (parser.Ok() && !parser.HasFinished()) |
||||
{ |
||||
confirmedState = parser.Skip().GetCurrentState(); |
||||
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) { |
||||
return true; |
||||
} |
||||
parser.RestoreState(confirmedState); |
||||
if (parsedFirstSelector) { |
||||
parser.Match(T(TCOMMA)).Skip(); |
||||
} |
||||
ParseSelector(parser); |
||||
parser.Skip(); |
||||
} |
||||
parser.Fail(); |
||||
return false; |
||||
} |
||||
|
||||
// Resets this object to initial state before parsing and update |
||||
// `playersSnapshot` to contain current players. |
||||
private final function Reset() |
||||
{ |
||||
parsedFirstSelector = false; |
||||
currentSelection.length = 0; |
||||
_.memory.FreeMany(playersSnapshot); |
||||
playersSnapshot.length = 0; |
||||
playersSnapshot = _.players.GetAll(); |
||||
selectorDelimiters.length = 0; |
||||
selectorDelimiters[0] = T(TCOMMA); |
||||
selectorDelimiters[1] = T(TCLOSE_BRACKET); |
||||
} |
||||
|
||||
/** |
||||
* Parses players from `toParse` according to the currently present players. |
||||
* |
||||
* Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||
* |
||||
* @param toParse `Text` from which to parse player list. |
||||
* @return `true` if parsing was successful and `false` otherwise. |
||||
*/ |
||||
public final function bool Parse(BaseText toParse) |
||||
{ |
||||
local bool wasSuccessful; |
||||
local Parser parser; |
||||
|
||||
if (toParse == none) { |
||||
return false; |
||||
} |
||||
parser = _.text.Parse(toParse); |
||||
wasSuccessful = ParseWith(parser); |
||||
parser.FreeSelf(); |
||||
return wasSuccessful; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
TSELF = 0 |
||||
stringConstants(0) = "self" |
||||
TADMIN = 1 |
||||
stringConstants(1) = "admin" |
||||
TALL = 2 |
||||
stringConstants(2) = "all" |
||||
TNOT = 3 |
||||
stringConstants(3) = "!" |
||||
TKEY = 4 |
||||
stringConstants(4) = "#" |
||||
TMACRO = 5 |
||||
stringConstants(5) = "@" |
||||
TCOMMA = 6 |
||||
stringConstants(6) = "," |
||||
TOPEN_BRACKET = 7 |
||||
stringConstants(7) = "[" |
||||
TCLOSE_BRACKET = 8 |
||||
stringConstants(8) = "]" |
||||
TME = 9 |
||||
stringConstants(9) = "me" |
||||
} |
@ -1,53 +0,0 @@
|
||||
/** |
||||
* Mock command class for testing. |
||||
* Copyright 2021 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 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")); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
Loading…
Reference in new issue