496 lines
16 KiB
Ucode
496 lines
16 KiB
Ucode
/**
|
|
* Author: dkanus
|
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
|
|
* License: GPL
|
|
* Copyright 2021-2023 Anton Tarasenko
|
|
*------------------------------------------------------------------------------
|
|
* This file is part of Acedia.
|
|
*
|
|
* Acedia is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*\
|
|
* Acedia is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
class 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) {
|
|
newSelfPlayer.NewRef();
|
|
selfPlayer = newSelfPlayer;
|
|
}
|
|
}
|
|
|
|
/// 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()) {
|
|
currentSelection[i].NewRef();
|
|
result[result.length] = currentSelection[i];
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
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"
|
|
} |