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