/**
* Object for parsing what converting textual description of a group of
* players into array of `EPlayer`s. Depends on the game context.
* 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 .
*/
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: "#" (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: "!". Specifying "!" in front of selector
* will select all players that do not match it instead.
*
* Grouped selectors: "['', '', ... '']".
* 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.
*/
// 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 playersSnapshot;
// Players, selected according to selectors we have parsed so far
var private array 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 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);
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(Text 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(Text 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 "@").
// Does nothing if there is no such macro.
private final function AddByMacro(Text 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 "@").
// Does nothing if there is no such macro.
private final function RemoveByMacro(Text 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())
{
_.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 GetPlayers()
{
local int i;
local array 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;
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(Text 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"
}