Compare commits

..

No commits in common. 'e8ae6fd8d16dc51d22ad4f5525c5399a447e9f69' and '70c41a5926fe78978cf6ca9236cb2ee4ada7b43f' have entirely different histories.

  1. 4
      config/AcediaAliases_Commands.ini
  2. 117
      config/AcediaCommands.ini
  3. 45
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc
  4. 227
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc
  5. 9
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc
  6. 10
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandSideEffects.uc
  7. 137
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc
  8. 568
      sources/BaseAPI/API/Commands/Command.uc
  9. 1568
      sources/BaseAPI/API/Commands/CommandAPI.uc
  10. 755
      sources/BaseAPI/API/Commands/CommandDataBuilder.uc
  11. 249
      sources/BaseAPI/API/Commands/CommandList.uc
  12. 350
      sources/BaseAPI/API/Commands/CommandParser.uc
  13. 66
      sources/BaseAPI/API/Commands/CommandPermissions.uc
  14. 59
      sources/BaseAPI/API/Commands/CommandRegistrationJob.uc
  15. 182
      sources/BaseAPI/API/Commands/Commands.uc
  16. 818
      sources/BaseAPI/API/Commands/Commands_Feature.uc
  17. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Signal.uc
  18. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Slot.uc
  19. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Signal.uc
  20. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Slot.uc
  21. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Signal.uc
  22. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Slot.uc
  23. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Signal.uc
  24. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Slot.uc
  25. 39
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Signal.uc
  26. 38
      sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Slot.uc
  27. 251
      sources/BaseAPI/API/Commands/PlayersParser.uc
  28. 351
      sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc
  29. 306
      sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc
  30. 142
      sources/BaseAPI/API/Commands/Tools/CommandsTool.uc
  31. 177
      sources/BaseAPI/API/Commands/Tools/ItemCard.uc
  32. 119
      sources/BaseAPI/API/Commands/Tools/VotingsTool.uc
  33. 563
      sources/BaseAPI/API/Commands/Voting/TEST_Voting.uc
  34. 860
      sources/BaseAPI/API/Commands/Voting/Voting.uc
  35. 243
      sources/BaseAPI/API/Commands/Voting/VotingModel.uc
  36. 132
      sources/BaseAPI/API/Commands/Voting/VotingPermissions.uc
  37. 75
      sources/BaseAPI/API/Commands/Voting/VotingSettings.uc
  38. 4
      sources/BaseAPI/API/Unflect/UnflectApi.uc
  39. 5
      sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc
  40. 2
      sources/Chat/ChatAPI.uc
  41. 25
      sources/Data/Database/DBAPI.uc
  42. 5
      sources/Features/FeatureConfig.uc
  43. 33
      sources/InfoQueryHandler/InfoQueryHandler.uc
  44. 13
      sources/Players/EPlayer.uc
  45. 87
      sources/Text/TextAPI.uc
  46. 7
      sources/Users/ACommandUserGroups.uc
  47. 53
      sources/Users/Users_Feature.uc

4
config/AcediaAliases_Commands.ini

@ -1,8 +1,8 @@
; This config file allows you to configure command aliases. ; This config file allows you to configure command aliases.
; Remember that aliases are case-insensitive. ; Remember that aliases are case-insensitive.
[AcediaCore.CommandAliasSource] [AcediaCore.CommandAliasSource]
record=(alias="yes",value="vote.yes") record=(alias="yes",value="vote yes")
record=(alias="no",value="vote.no") record=(alias="no",value="vote no")
[help CommandAliases] [help CommandAliases]
Alias="hlp" Alias="hlp"

117
config/AcediaCommands.ini

@ -1,117 +0,0 @@
[default Commands]
autoEnable=true
;= Setting this to `true` enables players to input commands with "mutate"
;= console command.
;= Default is `true`.
useMutateInput=true
;= Setting this to `true` enables players to input commands right in the chat
;= by prepending them with [`chatCommandPrefix`].
;= Default is `true`.
useChatInput=true
;= Chat messages, prepended by this prefix will be treated as commands.
;= Default is "!". Empty values are also treated as "!".
chatCommandPrefix=!
;= Allows to specify which user groups are used in determining command/votings
;= permission.
;= They must be specified in the order of importance: from the group with
;= highest level of permissions to the lowest. When determining player's
;= permission to use a certain command/voting, his group with the highest
;= available permissions will be used.
commandGroup=admin
commandGroup=moderator
commandGroup=trusted
commandGroup=all
;= Add a specified `CommandList` to the specified user group
addCommandList=(name="default",for="all")
addCommandList=(name="moderator",for="moderator")
addCommandList=(name="admin",for="admin")
addCommandList=(name="debug",for="admin")
;= Allows to specify a name for a certain command class
;=
;= NOTE:By default command choses that name by itself and its not recommended
;= to override it. You should only use this setting in case there is naming
;= conflict between commands from different packages.
;=renamingRule=(rename=class'ACommandHelp',to="lol")
;= Allows to specify a name for a certain voting class
;=
;= NOTE:By default voting choses that name by itself and its not recommended
;= to override it. You should only use this setting in case there is naming
;= conflict between votings from different packages.
;=votingRenamingRule=(rename=class'Voting',to="lol")
;= `CommandList` describes a set of commands and votings that can be made
;= available to users inside Commands feature
;=
;= Optionally, permission configs can be specified for commands and votings,
;= allowing server admins to create command lists for different groups player
;= with the same commands, but different permissions.
[default CommandList]
;= Allows to specify if this list should only be added when server is running
;= in debug mode.
;= `true` means yes, `false` means that list will always be available.
debugOnly=false
;= Adds a command of specified class with a "default" permissions config
command=class'ACommandHelp'
command=class'ACommandVote'
;= Adds a voting of specified class with a "default" permissions config
voting=class'Voting'
;= Adds a command of specified class with specified permissions config
;=commandWith=(cmd=,config="")
;= Adds a voting of specified class with specified permissions config
;=commandWith=(vtn=,config="")
[debug CommandList]
debugOnly=true
command=class'ACommandFakers'
[moderator CommandList]
command=class'ACommandNotify'
[admin CommandList]
command=class'ACommandSideEffects'
;= `VotingPermissions` describe use permission settings for a voting
[default VotingPermissions]
;= Determines the duration of the voting period, specified in seconds.
;= Zero or negative values mean unlimited voting period.
votingTime=30
;= Determines how draw will be interpreted.
;= `true` means draw counts as a vote's success, `false` means draw counts as a vote's failure.
drawEqualsSuccess=false
;= Determines whether spectators are allowed to vote.
allowSpectatorVoting=false
;= Specifies which group(s) of players are allowed to see who makes what vote.
allowedToVoteGroup=all
;= Specifies which group(s) of players are allowed to see who makes what vote.
allowedToSeeVotesGroup=all
;= Specifies which group(s) of players are allowed to forcibly end voting.
allowedToForceGroup=admin
allowedToForceGroup=moderator
[anonymous VotingPermissions]
votingTime=30
drawEqualsSuccess=false
allowSpectatorVoting=false
allowedToVoteGroup=all
allowedToSeeVotesGroup=admin
allowedToSeeVotesGroup=moderator
allowedToForceGroup=admin
allowedToForceGroup=moderator
[moderator VotingPermissions]
votingTime=60
drawEqualsSuccess=false
allowSpectatorVoting=false
allowedToVoteGroup=admin
allowedToVoteGroup=moderator
allowedToSeeVotesGroup=admin
allowedToForceGroup=admin
[admin VotingPermissions]
votingTime=60
drawEqualsSuccess=false
allowSpectatorVoting=true
allowedToVoteGroup=admin
allowedToSeeVotesGroup=admin
allowedToForceGroup=admin

45
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc

@ -24,12 +24,8 @@ class ACommandFakers extends Command
var private array<UserID> fakers; var private array<UserID> fakers;
protected static function StaticFinalizer() {
__().memory.FreeMany(default.fakers);
default.fakers.length = 0;
}
protected function BuildData(CommandDataBuilder builder) { protected function BuildData(CommandDataBuilder builder) {
builder.Name(P("fakers"));
builder.Group(P("debug")); builder.Group(P("debug"));
builder.Summary(P("Adds fake voters for testing \"vote\" command.")); builder.Summary(P("Adds fake voters for testing \"vote\" command."));
builder.Describe(P("Displays current fake voters.")); builder.Describe(P("Displays current fake voters."));
@ -44,11 +40,7 @@ protected function BuildData(CommandDataBuilder builder) {
builder.ParamBoolean(P("vote_for")); builder.ParamBoolean(P("vote_for"));
} }
protected function Executed( protected function Executed(CallData arguments, EPlayer instigator) {
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
if (arguments.subCommandName.IsEmpty()) { if (arguments.subCommandName.IsEmpty()) {
DisplayCurrentFakers(); DisplayCurrentFakers();
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) { } else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) {
@ -60,8 +52,14 @@ protected function Executed(
} }
} }
public final static function /*borrow*/ array<UserID> BorrowDebugVoters() { public final function UpdateFakersForVoting() {
return default.fakers; local Voting currentVoting;
currentVoting = GetCurrentVoting();
if (currentVoting != none) {
currentVoting.SetDebugVoters(fakers);
}
_.memory.Free(currentVoting);
} }
private final function CastVote(int fakerID, bool voteFor) { private final function CastVote(int fakerID, bool voteFor) {
@ -73,7 +71,7 @@ private final function CastVote(int fakerID, bool voteFor) {
.WriteLine(P("Faker number is out of bounds.")); .WriteLine(P("Faker number is out of bounds."));
return; return;
} }
currentVoting = _.commands.GetCurrentVoting(); currentVoting = GetCurrentVoting();
if (currentVoting == none) { if (currentVoting == none) {
callerConsole callerConsole
.UseColor(_.color.TextFailure) .UseColor(_.color.TextFailure)
@ -88,7 +86,6 @@ private final function ChangeAmount(int newAmount) {
local int i; local int i;
local Text nextIDName; local Text nextIDName;
local UserID nextID; local UserID nextID;
local Voting currentVoting;
if (newAmount < 0) { if (newAmount < 0) {
callerConsole callerConsole
@ -113,12 +110,19 @@ private final function ChangeAmount(int newAmount) {
} }
fakers.length = newAmount; fakers.length = newAmount;
} }
default.fakers = fakers; UpdateFakersForVoting();
currentVoting = _.commands.GetCurrentVoting(); }
if (currentVoting != none) {
currentVoting.SetDebugVoters(default.fakers); private function Voting GetCurrentVoting() {
_.memory.Free(currentVoting); 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() { private function DisplayCurrentFakers() {
@ -131,7 +135,7 @@ private function DisplayCurrentFakers() {
callerConsole.WriteLine(P("No fakers!")); callerConsole.WriteLine(P("No fakers!"));
return; return;
} }
currentVoting =_.commands.GetCurrentVoting(); currentVoting = GetCurrentVoting();
for (i = 0; i < fakers.length; i += 1) { for (i = 0; i < fakers.length; i += 1) {
nextNumber = _.text.FromIntM(i); nextNumber = _.text.FromIntM(i);
callerConsole callerConsole
@ -159,5 +163,4 @@ private function DisplayCurrentFakers() {
} }
defaultproperties { defaultproperties {
preferredName = "fakers"
} }

227
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandHelp.uc

@ -1,6 +1,6 @@
/** /**
* Command for displaying help information about registered Acedia's commands. * Command for displaying help information about registered Acedia's commands.
* Copyright 2021-2023 Anton Tarasenko * Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -18,8 +18,7 @@
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class ACommandHelp extends Command class ACommandHelp extends Command
dependson(LoggerAPI) dependson(LoggerAPI);
dependson(CommandAPI);
/** /**
* # `ACommandHelp` * # `ACommandHelp`
@ -73,8 +72,6 @@ class ACommandHelp extends Command
// that uses sub-command names as keys and returns `ArrayList` of aliases. // that uses sub-command names as keys and returns `ArrayList` of aliases.
var private HashTable commandToAliasesMap; var private HashTable commandToAliasesMap;
var private User callerUser;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS; var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE; var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT; var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
@ -82,8 +79,7 @@ var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO; var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET; var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP; var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN; var public const int TALIASES_FOR, TEMPTY, TDOT;
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;
protected function Constructor() protected function Constructor()
{ {
@ -100,13 +96,13 @@ protected function Constructor()
protected function Finalizer() protected function Finalizer()
{ {
super.Finalizer();
_.memory.Free(commandToAliasesMap); _.memory.Free(commandToAliasesMap);
commandToAliasesMap = none; commandToAliasesMap = none;
} }
protected function BuildData(CommandDataBuilder builder) protected function BuildData(CommandDataBuilder builder)
{ {
builder.Name(P("help"));
builder.Group(P("core")); builder.Group(P("core"));
builder.Summary(P("Displays detailed information about available commands.")); builder.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams(); builder.OptionalParams();
@ -124,18 +120,13 @@ protected function BuildData(CommandDataBuilder builder)
builder.ParamTextList(P("groups")); builder.ParamTextList(P("groups"));
} }
protected function Executed( protected function Executed(Command.CallData callData, EPlayer callerPlayer)
Command.CallData callData, {
EPlayer callerPlayer,
CommandPermissions permissions
) {
local bool printedSomething;
local HashTable parameters, options; local HashTable parameters, options;
local ArrayList commandsToDisplay, commandGroupsToDisplay; local ArrayList commandsToDisplay, commandGroupsToDisplay;
callerUser = callerPlayer.GetIdentity(); parameters = callData.parameters;
parameters = callData.parameters; options = callData.options;
options = callData.options;
// Print command list if "--list" option was specified // Print command list if "--list" option was specified
if (options.HasKey(P("list"))) if (options.HasKey(P("list")))
{ {
@ -144,7 +135,6 @@ protected function Executed(
commandGroupsToDisplay, commandGroupsToDisplay,
options.HasKey(P("aliases"))); options.HasKey(P("aliases")));
_.memory.Free(commandGroupsToDisplay); _.memory.Free(commandGroupsToDisplay);
printedSomething = true;
} }
// Help pages. // Help pages.
// Only need to print them if: // Only need to print them if:
@ -154,11 +144,9 @@ protected function Executed(
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands"))) if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{ {
commandsToDisplay = parameters.GetArrayList(P("commands")); commandsToDisplay = parameters.GetArrayList(P("commands"));
DisplayCommandHelpPages(commandsToDisplay, printedSomething); DisplayCommandHelpPages(commandsToDisplay);
_.memory.Free(commandsToDisplay); _.memory.Free(commandsToDisplay);
} }
_.memory.Free(callerUser);
callerUser = none;
} }
// If instance of the `Aliases_Feature` is passed as an argument (allowing this // If instance of the `Aliases_Feature` is passed as an argument (allowing this
@ -259,11 +247,17 @@ private final function DisplayCommandLists(
ArrayList commandGroupsToDisplay, ArrayList commandGroupsToDisplay,
bool displayAliases) bool displayAliases)
{ {
local int i; local int i;
local array<Text> commandNames, groupsNames; local array<Text> commandNames, groupsNames;
local Commands_Feature commandsFeature;
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) {
return;
}
if (commandGroupsToDisplay == none) { if (commandGroupsToDisplay == none) {
groupsNames = _.commands.GetGroupsNames(); groupsNames = commandsFeature.GetGroupsNames();
} }
else else
{ {
@ -277,7 +271,7 @@ private final function DisplayCommandLists(
if (groupsNames[i] == none) { if (groupsNames[i] == none) {
continue; continue;
} }
commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]); commandNames = commandsFeature.GetCommandNamesInGroup(groupsNames[i]);
if (commandNames.length > 0) if (commandNames.length > 0)
{ {
callerConsole.UseColorOnce(_.color.TextSubHeader); callerConsole.UseColorOnce(_.color.TextSubHeader);
@ -288,45 +282,41 @@ private final function DisplayCommandLists(
callerConsole.WriteLine(groupsNames[i]); callerConsole.WriteLine(groupsNames[i]);
} }
PrintCommandsNamesArray( PrintCommandsNamesArray(
commandsFeature,
commandNames, commandNames,
displayAliases); displayAliases);
_.memory.FreeMany(commandNames); _.memory.FreeMany(commandNames);
} else {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
callerConsole.Write(groupsNames[i]);
callerConsole.WriteLine(T(TEMPTY_GROUP_END));
callerConsole.ResetColor();
} }
} }
_.memory.FreeMany(groupsNames); _.memory.FreeMany(groupsNames);
commandsFeature.FreeSelf();
} }
private final function PrintCommandsNamesArray( private final function PrintCommandsNamesArray(
array<Text> commandsNamesArray, Commands_Feature commandsFeature,
bool displayAliases array<Text> commandsNamesArray,
) { bool displayAliases)
local int i; {
local Command.Data nextData; local int i;
local CommandAPI.CommandConfigInfo nextCommandPair; local Command nextCommand;
local Command.Data nextData;
for (i = 0; i < commandsNamesArray.length; i += 1) for (i = 0; i < commandsNamesArray.length; i += 1)
{ {
nextCommandPair = _.commands.ResolveCommandForUser( nextCommand = commandsFeature.GetCommand(commandsNamesArray[i]);
commandsNamesArray[i], if (nextCommand == none) {
callerUser); continue;
if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) { }
nextData = nextCommandPair.instance.BorrowData(); nextData = nextCommand.BorrowData();
callerConsole callerConsole
.UseColorOnce(_.color.textEmphasis) .UseColorOnce(_.color.textEmphasis)
.Write(commandsNamesArray[i]) .Write(nextData.name)
.Write(T(TCOLON_SPACE)) .Write(T(TCOLON_SPACE))
.WriteLine(nextData.summary); .WriteLine(nextData.summary);
if (displayAliases) { if (displayAliases) {
PrintCommandAliases(commandsNamesArray[i]); PrintCommandAliases(nextData.name);
}
} }
_.memory.Free(nextCommandPair.instance); _.memory.Free(nextCommand);
} }
} }
@ -407,47 +397,46 @@ private final function PrintAliasesArray(
callerConsole.WriteBlock(); callerConsole.WriteBlock();
} }
private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) { private final function DisplayCommandHelpPages(ArrayList commandList)
local int i; {
local Text nextUserProvidedName; local int i;
local MutableText referredSubcommand; local bool printedSomething;
local CommandAPI.CommandConfigInfo nextPair; local Text nextUserProvidedName;
local MutableText referredSubcommand;
local Command nextCommand;
// If arguments were empty - at least display our own help page // If arguments were empty - at least display our own help page
if (commandList == none) { if (commandList == none)
nextPair.instance = self; {
PrintHelpPageFor(usedName, none, nextPair); PrintHelpPageFor(BorrowData().name, none, BorrowData());
return; return;
} }
// Otherwise - print help for specified commands // Otherwise - print help for specified commands
for (i = 0; i < commandList.GetLength(); i += 1) { for (i = 0; i < commandList.GetLength(); i += 1)
{
nextUserProvidedName = commandList.GetText(i); nextUserProvidedName = commandList.GetText(i);
nextPair = GetCommandFromUserProvidedName( nextCommand = GetCommandFromUserProvidedName(
nextUserProvidedName, nextUserProvidedName,
/*out*/ referredSubcommand); referredSubcommand);
if (nextPair.instance != none && !nextPair.usageForbidden) { if (nextCommand != none)
{
if (printedSomething) { if (printedSomething) {
callerConsole.WriteLine(T(TSEPARATOR)); callerConsole.WriteLine(T(TSEPARATOR));
} }
PrintHelpPageFor( PrintHelpPageFor(
nextUserProvidedName, nextUserProvidedName,
referredSubcommand, referredSubcommand,
nextPair); nextCommand.BorrowData());
printedSomething = true; printedSomething = true;
} else if (nextPair.instance != none) {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TNO_COMMAND_BEGIN));
callerConsole.Write(nextUserProvidedName);
callerConsole.WriteLine(T(TNO_COMMAND_END));
callerConsole.ResetColor();
} }
_.memory.Free(nextPair.instance); _.memory.Free(nextCommand);
_.memory.Free(nextUserProvidedName); _.memory.Free(nextUserProvidedName);
_.memory.Free(referredSubcommand); _.memory.Free(referredSubcommand);
// `referredSubcommand` is passed as an `out` parameter on // `referredSubcommand` is passed as an `out` parameter on
// every iteration, so we need to prevent the possibility of its value // every iteration, so we need to prevent the possibility of its value
// being used. // being used.
// NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten. // NOTE: `nextCommand` and `nextUserProvidedName` are just
// rewritten.
referredSubcommand = none; referredSubcommand = none;
} }
} }
@ -457,49 +446,56 @@ private final function DisplayCommandHelpPages(ArrayList commandList, bool print
// is passed) and is used to return name of the subcommand for returned // is passed) and is used to return name of the subcommand for returned
// `Command` that is specified by `nextUserProvidedName` (only relevant for // `Command` that is specified by `nextUserProvidedName` (only relevant for
// aliases that refer to a particular subcommand). // aliases that refer to a particular subcommand).
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName( private final function Command GetCommandFromUserProvidedName(
BaseText nextUserProvidedName, BaseText nextUserProvidedName,
out MutableText referredSubcommand) out MutableText referredSubcommand)
{ {
local CommandAPI.CommandConfigInfo result; local Command result;
local Text commandAliasValue; local Text commandAliasValue;
local MutableText parsedCommandName; local Commands_Feature commandsFeature;
local MutableText parsedCommandName;
// Clear `out` parameter no matter what // Clear `out` parameter no matter what
if (referredSubcommand != none) { if (referredSubcommand != none)
{
referredSubcommand.FreeSelf(); referredSubcommand.FreeSelf();
referredSubcommand = none; referredSubcommand = none;
} }
// Try accessing (check availability of) `Commands_Feature`
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) {
return none;
}
// Try getting command using `nextUserProvidedName` as a literal name // Try getting command using `nextUserProvidedName` as a literal name
result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser); result = commandsFeature.GetCommand(nextUserProvidedName);
if (result.instance != none) { if (result != none)
{
commandsFeature.FreeSelf();
return result; return result;
} }
// On failure - try resolving it as an alias // On failure - try resolving it as an alias
commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName); commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand); ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser); result = commandsFeature.GetCommand(parsedCommandName);
if ( result.instance == none
|| !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
_.memory.Free(result.instance);
return result;
}
// Empty subcommand name from the alias is essentially no subcommand name // Empty subcommand name from the alias is essentially no subcommand name
if (referredSubcommand != none && referredSubcommand.IsEmpty()) { if (referredSubcommand != none && referredSubcommand.IsEmpty())
{
referredSubcommand.FreeSelf(); referredSubcommand.FreeSelf();
referredSubcommand = none; referredSubcommand = none;
} }
_.memory.Free2(commandAliasValue, parsedCommandName); _.memory.Free(commandAliasValue);
_.memory.Free(parsedCommandName);
commandsFeature.FreeSelf();
return result; return result;
} }
private final function PrintHelpPageFor( private final function PrintHelpPageFor(
BaseText commandAlias, BaseText commandAlias,
BaseText referredSubcommand, BaseText referredSubcommand,
CommandAPI.CommandConfigInfo commandPair Command.Data commandData)
) { {
local Text commandNameLowerCase, commandNameUpperCase; local Text commandNameLowerCase, commandNameUpperCase;
// Get capitalized command name // Get capitalized command name
commandNameUpperCase = commandAlias.UpperCopy(); commandNameUpperCase = commandAlias.UpperCopy();
// Print header: name + basic info // Print header: name + basic info
@ -507,7 +503,7 @@ private final function PrintHelpPageFor(
.Write(commandNameUpperCase) .Write(commandNameUpperCase)
.UseColor(_.color.textDefault); .UseColor(_.color.textDefault);
commandNameUpperCase.FreeSelf(); commandNameUpperCase.FreeSelf();
if (commandPair.instance.BorrowData().requiresTarget) { if (commandData.requiresTarget) {
callerConsole.WriteLine(T(TCMD_WITH_TARGET)); callerConsole.WriteLine(T(TCMD_WITH_TARGET));
} }
else { else {
@ -515,27 +511,31 @@ private final function PrintHelpPageFor(
} }
// Print commands and options // Print commands and options
commandNameLowerCase = commandAlias.LowerCopy(); commandNameLowerCase = commandAlias.LowerCopy();
PrintCommands(commandPair, commandNameLowerCase, referredSubcommand); PrintCommands(commandData, commandNameLowerCase, referredSubcommand);
commandNameLowerCase.FreeSelf(); commandNameLowerCase.FreeSelf();
PrintOptions(commandPair.instance.BorrowData()); PrintOptions(commandData);
// Clean up // Clean up
callerConsole.ResetColor().Flush(); callerConsole.ResetColor().Flush();
} }
private final function PrintCommands( private final function PrintCommands(
CommandAPI.CommandConfigInfo commandPair, Command.Data data,
BaseText commandName, BaseText commandName,
BaseText referredSubcommand BaseText referredSubcommand)
) { {
local int i; local int i;
local array<Command.SubCommand> subCommands; local array<SubCommand> subCommands;
subCommands = commandPair.instance.BorrowData().subCommands; subCommands = data.subCommands;
for (i = 0; i < subCommands.length; i += 1) { for (i = 0; i < subCommands.length; i += 1)
if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) { {
if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) { if ( referredSubcommand == none
PrintSubCommand(subCommands[i], commandName, referredSubcommand != none); || referredSubcommand.Compare(subCommands[i].name))
} {
PrintSubCommand(
subCommands[i],
commandName,
referredSubcommand != none);
} }
} }
} }
@ -719,13 +719,4 @@ defaultproperties
stringConstants(21) = "" stringConstants(21) = ""
TDOT = 22 TDOT = 22
stringConstants(22) = "." stringConstants(22) = "."
TNO_COMMAND_BEGIN = 23
stringConstants(23) = "Command `"
TNO_COMMAND_END = 24
stringConstants(24) = "` not found!"
TEMPTY_GROUP_BEGIN = 25
stringConstants(25) = "No commands in group \""
TEMPTY_GROUP_END = 26
stringConstants(26) = "\"!"
preferredName = "help"
} }

9
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc

@ -23,6 +23,7 @@ class ACommandNotify extends Command
dependsOn(ChatApi); dependsOn(ChatApi);
protected function BuildData(CommandDataBuilder builder) { protected function BuildData(CommandDataBuilder builder) {
builder.Name(P("notify"));
builder.Group(P("core")); builder.Group(P("core"));
builder.Summary(P("Notifies players with provided message.")); builder.Summary(P("Notifies players with provided message."));
builder.ParamText(P("message")); builder.ParamText(P("message"));
@ -42,12 +43,7 @@ protected function BuildData(CommandDataBuilder builder) {
builder.ParamText(P("channel_name")); builder.ParamText(P("channel_name"));
} }
protected function ExecutedFor( protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) {
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local Text title, message, plainTitle, plainMessage; local Text title, message, plainTitle, plainMessage;
plainMessage = arguments.parameters.GetText(P("message")); plainMessage = arguments.parameters.GetText(P("message"));
@ -65,5 +61,4 @@ protected function ExecutedFor(
} }
defaultproperties { defaultproperties {
preferredName = "notify"
} }

10
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandSideEffects.uc

@ -36,7 +36,8 @@ protected function Finalizer() {
} }
protected function BuildData(CommandDataBuilder builder) { protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("debug")); builder.Name(P("sideeffects"));
builder.Group(P("core"));
builder.Summary(P("Displays information about current side effects.")); builder.Summary(P("Displays information about current side effects."));
builder.Describe(P("This command allows to display current side effects, optionally filtering" builder.Describe(P("This command allows to display current side effects, optionally filtering"
@ "them by specified package names.")); @ "them by specified package names."));
@ -53,11 +54,7 @@ protected function BuildData(CommandDataBuilder builder) {
builder.Describe(P("Display verbose information about each side effect.")); builder.Describe(P("Display verbose information about each side effect."));
} }
protected function Executed( protected function Executed(CallData arguments, EPlayer instigator) {
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local UserID playerID; local UserID playerID;
local array<SideEffect> relevantSideEffects; local array<SideEffect> relevantSideEffects;
local ArrayList packagesList, storedSideEffectsList; local ArrayList packagesList, storedSideEffectsList;
@ -193,5 +190,4 @@ private function ShowInfoFor(UserID playerID, int sideEffectIndex) {
} }
defaultproperties { defaultproperties {
preferredName = "sideeffects"
} }

137
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc

@ -19,93 +19,62 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class ACommandVote extends Command class ACommandVote extends Command;
dependson(CommandAPI)
dependson(VotingModel);
var private CommandDataBuilder dataBuilder; var private CommandDataBuilder dataBuilder;
protected function Constructor() { protected function Constructor() {
ResetVotingInfo(); ResetVotingInfo();
_.commands.OnVotingAdded(self).connect = AddVotingInfo;
_.commands.OnVotingRemoved(self).connect = HandleRemovedVoting;
_.chat.OnVoiceMessage(self).connect = VoteWithVoice;
} }
protected function Finalizer() { protected function Finalizer() {
super.Finalizer(); super.Finalizer();
_.memory.Free(dataBuilder); _.memory.Free(dataBuilder);
dataBuilder = none; dataBuilder = none;
_.commands.OnVotingAdded(self).Disconnect();
_.commands.OnVotingRemoved(self).Disconnect();
_.chat.OnVoiceMessage(self).Disconnect();
} }
protected function BuildData(CommandDataBuilder builder) { protected function BuildData(CommandDataBuilder builder) {
builder.Name(P("vote"));
builder.Group(P("core")); builder.Group(P("core"));
builder.Summary(P("Allows players to initiate any available voting." builder.Summary(P("Allows players to initiate any available voting."
@ "Voting options themselves are specified as sub-commands.")); @ "Votings themselves are added as sub-commands."));
builder.Describe(P("Default command simply displaces information about current vote.")); builder.Describe(P("Default command simply displaces information about current vote."));
dataBuilder.SubCommand(P("yes")); dataBuilder.SubCommand(P("yes"));
builder.Describe(P("Vote `yes` on the current vote.")); builder.Describe(P("Vote `yes` on the current vote."));
dataBuilder.SubCommand(P("no")); dataBuilder.SubCommand(P("no"));
builder.Describe(P("Vote `no` on the current vote.")); builder.Describe(P("Vote `no` on the current vote."));
builder.Option(P("force"));
builder.Describe(P("Tries to force voting to end immediately with the desired result."));
} }
protected function Executed( protected function Executed(CallData arguments, EPlayer instigator) {
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local bool forcingVoting;
local VotingModel.ForceEndingType forceType;
local Voting currentVoting; local Voting currentVoting;
local Commands_Feature feature;
forcingVoting = arguments.options.HasKey(P("force")); feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
currentVoting = _.commands.GetCurrentVoting(); 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()) { if (arguments.subCommandName.IsEmpty()) {
DisplayInfoAboutVoting(instigator, currentVoting); DisplayInfoAboutVoting(instigator, currentVoting);
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) { } else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, true); CastVote(currentVoting, instigator, true);
forceType = FET_Success;
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) { } else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, false); CastVote(currentVoting, instigator, false);
forceType = FET_Failure;
} else if (StartVoting(arguments, currentVoting, instigator)) {
_.memory.Free(currentVoting);
currentVoting = _.commands.GetCurrentVoting();
forceType = FET_Success;
} else { } else {
forcingVoting = false; StartVoting(arguments.subCommandName, feature, currentVoting, instigator);
}
if (currentVoting != none && !currentVoting.HasEnded() && forcingVoting) {
if (currentVoting.ForceEnding(instigator, forceType) == FEO_Forbidden) {
callerConsole
.WriteLine(F("You {$TextNegative aren't allowed} to forcibly end current voting"));
}
}
_.memory.Free(currentVoting);
}
private final function VoteWithVoice(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
local Voting currentVoting;
currentVoting = _.commands.GetCurrentVoting();
if (message == BIVM_AckYes) {
CastVote(currentVoting, sender, true);
}
if (message == BIVM_AckNo) {
CastVote(currentVoting, sender, false);
} }
_.memory.Free(feature);
_.memory.Free(currentVoting); _.memory.Free(currentVoting);
} }
/// Adds sub-command information about given voting with a given name. /// Adds sub-command information about given voting with a given name.
public final function AddVotingInfo(class<Voting> processClass, Text processName) { public final function AddVotingInfo(BaseText processName, class<Voting> processClass) {
if (processName == none) return; if (processName == none) return;
if (processClass == none) return; if (processClass == none) return;
if (dataBuilder == none) return; if (dataBuilder == none) return;
@ -115,19 +84,6 @@ public final function AddVotingInfo(class<Voting> processClass, Text processName
commandData = dataBuilder.BorrowData(); commandData = dataBuilder.BorrowData();
} }
public final function HandleRemovedVoting(class<Voting> votingClass) {
local int i;
local array<Text> votingsNames;
ResetVotingInfo();
// Rebuild the whole voting data
votingsNames = _.commands.GetAllVotingsNames();
for (i = 0; i < votingsNames.length; i += 1) {
AddVotingInfo(_.commands.GetVotingClass(votingsNames[i]), votingsNames[i]);
}
_.memory.FreeMany(votingsNames);
}
/// Clears all sub-command information added from [`Voting`]s. /// Clears all sub-command information added from [`Voting`]s.
public final function ResetVotingInfo() { public final function ResetVotingInfo() {
_.memory.Free(dataBuilder); _.memory.Free(dataBuilder);
@ -153,54 +109,41 @@ private final function CastVote(Voting currentVoting, EPlayer voter, bool voteFo
} }
// Assumes all arguments aren't `none`. // Assumes all arguments aren't `none`.
private final function bool StartVoting( private final function StartVoting(
CallData arguments, BaseText votingName,
Commands_Feature feature,
Voting currentVoting, Voting currentVoting,
EPlayer instigator EPlayer instigator
) { ) {
local Command fakersCommand;
local Voting newVoting; local Voting newVoting;
local User callerUser; local Commands_Feature.StartVotingResult result;
local CommandAPI.VotingConfigInfo pair;
local CommandAPI.StartVotingResult result;
callerUser = instigator.GetIdentity(); result = feature.StartVoting(votingName);
pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser); // Handle errors
_.memory.Free(callerUser); if (result == SVR_UnknownVoting) {
if (pair.votingClass == none) {
callerConsole callerConsole
.UseColor(_.color.TextFailure) .UseColor(_.color.TextFailure)
.Write(P("Unknown voting option \"")) .Write(P("Unknown voting option \""))
.Write(arguments.subCommandName) .Write(votingName)
.WriteLine(P("\"")); .WriteLine(P("\""));
return false; return;
} } else if (result == SVR_AlreadyInProgress) {
if (pair.usageForbidden) {
callerConsole
.UseColor(_.color.TextFailure)
.Write(P("You aren't allowed to start \""))
.Write(arguments.subCommandName)
.WriteLine(P("\" voting"));
return false;
}
result = _.commands.StartVoting(pair, arguments.parameters);
Log("Result:" @ result);
// Handle errors.
// `SVR_UnknownVoting` is impossible, since we've already checked that
// `pair.votingClass != none`)
if (result == SVR_AlreadyInProgress) {
callerConsole callerConsole
.UseColor(_.color.TextWarning) .UseColor(_.color.TextWarning)
.WriteLine(P("Another voting is already in progress!")); .WriteLine(P("Another voting is already in progress!"));
return false; return;
} }
if (result == SVR_NoVoters) { // Inform new voting about fake voters, in case we're debugging
callerConsole if (currentVoting == none && _.environment.IsDebugging()) {
.UseColor(_.color.TextWarning) fakersCommand = feature.GetCommand(P("fakers"));
.WriteLine(P("There are no players eligible for that voting.")); if (fakersCommand != none && fakersCommand.class == class'ACommandFakers') {
return false; ACommandFakers(fakersCommand).UpdateFakersForVoting();
}
_.memory.Free(fakersCommand);
} }
// Cast a vote from instigator // Cast a vote from instigator
newVoting = _.commands.GetCurrentVoting(); newVoting = feature.GetCurrentVoting();
if (newVoting != none) { if (newVoting != none) {
newVoting.CastVote(instigator, true); newVoting.CastVote(instigator, true);
} else { } else {
@ -208,13 +151,9 @@ private final function bool StartVoting(
.UseColor(_.color.TextFailure) .UseColor(_.color.TextFailure)
.WriteLine(P("Voting should be available, but it isn't." .WriteLine(P("Voting should be available, but it isn't."
@ "This is unexpected, something broke terribly.")); @ "This is unexpected, something broke terribly."));
_.memory.Free(newVoting);
return false;
} }
_.memory.Free(newVoting); _.memory.Free(newVoting);
return true;
} }
defaultproperties { defaultproperties {
preferredName = "vote"
} }

568
sources/BaseAPI/API/Commands/Command.uc

@ -54,97 +54,92 @@ class Command extends AcediaObject
//! //!
//! # Implementation //! # Implementation
//! //!
//! The idea of `Command`'s implementation is simple: command is basically the //! The idea of `Command`'s implementation is simple: command is basically the `Command.Data` struct
//! `Command.Data` struct that is filled via `CommandDataBuilder`. //! that is filled via `CommandDataBuilder`.
//! Whenever command is called it uses `CommandParser` to parse user's input //! Whenever command is called it uses `CommandParser` to parse user's input based on its
//! based on its `Command.Data` and either report error (in case of failure) or //! `Command.Data` and either report error (in case of failure) or pass make
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success). //! `Executed()`/`ExecutedFor()` calls (in case of success).
//! //!
//! When command is called is decided by `Commands_Feature` that tracks possible //! When command is called is decided by `Commands_Feature` that tracks possible user inputs
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for //! (and provides `HandleInput()`/`HandleInputWith()` methods for adding custom command inputs).
//! adding custom command inputs). That feature basically parses first part of //! That feature basically parses first part of the command: its name (not the subcommand's names)
//! the command: its name (not the subcommand's names) and target players //! and target players (using `PlayersParser`, but only if command is targeted).
//! (using `PlayersParser`, but only if command is targeted).
//! //!
//! Majority of the command-related code either serves to build `Command.Data` //! Majority of the command-related code either serves to build `Command.Data` or to parse command
//! or to parse command input by using it (`CommandParser`). //! input by using it (`CommandParser`).
/// Possible errors that can arise when parsing command parameters from user /// Possible errors that can arise when parsing command parameters from user input
/// input
enum ErrorType { enum ErrorType {
/// No error // No error
CET_None, CET_None,
/// Bad parser was provided to parse user input (this should not be possible) // Bad parser was provided to parse user input (this should not be possible)
CET_BadParser, CET_BadParser,
/// Sub-command name was not specified or was incorrect // Sub-command name was not specified or was incorrect
/// (this should not be possible) // (this should not be possible)
CET_NoSubCommands, CET_NoSubCommands,
/// Specified sub-command does not exist // Specified sub-command does not exist
/// (only relevant when it is enforced for parser, e.g. by an alias) // (only relevant when it is enforced for parser, e.g. by an alias)
CET_BadSubCommand, CET_BadSubCommand,
/// Required param for command / option was not specified // Required param for command / option was not specified
CET_NoRequiredParam, CET_NoRequiredParam,
CET_NoRequiredParamForOption, CET_NoRequiredParamForOption,
/// Unknown option key was specified // Unknown option key was specified
CET_UnknownOption, CET_UnknownOption,
/// Unknown short option key was specified
CET_UnknownShortOption, CET_UnknownShortOption,
/// Same option appeared twice in one command call // Same option appeared twice in one command call
CET_RepeatedOption, CET_RepeatedOption,
/// Part of user's input could not be interpreted as a part of // Part of user's input could not be interpreted as a part of
/// command's call // command's call
CET_UnusedCommandParameters, CET_UnusedCommandParameters,
/// In one short option specification (e.g. '-lah') several options require // In one short option specification (e.g. '-lah') several options require parameters:
/// parameters: this introduces ambiguity and is not allowed // this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams, CET_MultipleOptionsWithParams,
/// Targets are specified incorrectly (for targeted commands only) // (For targeted commands only)
// Targets are specified incorrectly (or none actually specified)
CET_IncorrectTargetList, CET_IncorrectTargetList,
// No targets are specified (for targeted commands only)
CET_EmptyTargetList CET_EmptyTargetList
}; };
/// Structure that contains all the information about how `Command` was called. /// Structure that contains all the information about how `Command` was called.
struct CallData { struct CallData {
/// Targeted players (if applicable) // Targeted players (if applicable)
var public array<EPlayer> targetPlayers; var public array<EPlayer> targetPlayers;
/// Specified sub-command and parameters/options // Specified sub-command and parameters/options
var public Text subCommandName; var public Text subCommandName;
/// Provided parameters and specified options // Provided parameters and specified options
var public HashTable parameters; var public HashTable parameters;
var public HashTable options; var public HashTable options;
/// Errors that occurred during command call processing are described by // Errors that occurred during command call processing are described by
/// error type. // error type and optional error textual name of the object
// (parameter, option, etc.) that caused it.
var public ErrorType parsingError; var public ErrorType parsingError;
/// Optional error textual name of the object (parameter, option, etc.)
/// that caused it.
var public Text errorCause; var public Text errorCause;
}; };
/// Possible types of parameters. /// Possible types of parameters.
enum ParameterType { enum ParameterType {
/// Parses into `BoolBox` // Parses into `BoolBox`
CPT_Boolean, CPT_Boolean,
/// Parses into `IntBox` // Parses into `IntBox`
CPT_Integer, CPT_Integer,
/// Parses into `FloatBox` // Parses into `FloatBox`
CPT_Number, CPT_Number,
/// Parses into `Text` // Parses into `Text`
CPT_Text, CPT_Text,
/// Special parameter that consumes the rest of the input into `Text` // Special parameter that consumes the rest of the input into `Text`
CPT_Remainder, CPT_Remainder,
/// Parses into `HashTable` // Parses into `HashTable`
CPT_Object, CPT_Object,
/// Parses into `ArrayList` // Parses into `ArrayList`
CPT_Array, CPT_Array,
/// Parses into any JSON value // Parses into any JSON value
CPT_JSON, CPT_JSON,
/// Parses into an array of specified players // Parses into an array of specified players
CPT_Players CPT_Players
}; };
/// Possible forms a boolean variable can be used as. /// Possible forms a boolean variable can be used as.
/// Boolean parameter can define it's preferred format, which will be used for /// Boolean parameter can define it's preferred format, which will be used for help page generation.
/// help page generation.
enum PreferredBooleanFormat { enum PreferredBooleanFormat {
PBF_TrueFalse, PBF_TrueFalse,
PBF_EnableDisable, PBF_EnableDisable,
@ -154,117 +149,91 @@ enum PreferredBooleanFormat {
// Defines a singular command parameter // Defines a singular command parameter
struct Parameter { struct Parameter {
/// Display name (for the needs of help page displaying) // Display name (for the needs of help page displaying)
var Text displayName; var Text displayName;
/// Type of value this parameter would store // Type of value this parameter would store
var ParameterType type; var ParameterType type;
/// Does it take only a singular value or can it contain several of them, // Does it take only a singular value or can it contain several of them,
/// written in a list // written in a list
var bool allowsList; var bool allowsList;
/// Variable name that will be used as a key to store parameter's value // Variable name that will be used as a key to store parameter's value
var Text variableName; var Text variableName;
/// (For `CPT_Boolean` type variables only) - preferred boolean format, // (For `CPT_Boolean` type variables only) - preferred boolean format,
/// used in help pages // used in help pages
var PreferredBooleanFormat booleanFormat; var PreferredBooleanFormat booleanFormat;
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some // `CPT_Text` can be attempted to be auto-resolved as an alias from some source during parsing.
/// source during parsing. // For command to attempt that, this field must be not-`none` and contain the name of
/// For command to attempt that, this field must be not-`none` and contain // the alias source (either "weapon", "color", "feature", "entity" or some kind of custom alias
/// the name of the alias source (either "weapon", "color", "feature", // source name).
/// "entity" or some kind of custom alias source name). //
/// // Only relevant when given value is prefixed with "$" character.
/// Only relevant when given value is prefixed with "$" character.
var Text aliasSourceName; var Text aliasSourceName;
}; };
/// Defines a sub-command of a this command // Defines a sub-command of a this command (specified as "<command> <sub_command>").
/// (specified as "<command> <sub_command>"). //
/// // Using sub-command is not optional, but if none defined (in `BuildData()`) / specified by
/// Using sub-command is not optional, but if none defined // the player - an empty (`name.IsEmpty()`) one is automatically created / used.
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
/// one is automatically created / used.
struct SubCommand { struct SubCommand {
/// Name of the sub command. Cannot be `none`. // Cannot be `none`
var Text name; var Text name;
/// Human-readable description of the subcommand. Can be `none`. // Can be `none`
var Text description; var Text description;
/// List of required parameters of this [`Command`].
var array<Parameter> required; var array<Parameter> required;
/// List of optional parameters of this [`Command`].
var array<Parameter> optional; var array<Parameter> optional;
}; };
/// Defines command's option (options are specified by "--long" or "-l"). // Defines command's option (options are specified by "--long" or "-l").
/// Options are independent from sub-commands. // Options are independent from sub-commands.
struct Option { struct Option {
/// [`Option`]'s short name, i.e. a single letter "f" that can be specified
/// in, e.g. "-laf" type option listings
var BaseText.Character shortName; var BaseText.Character shortName;
/// [`Option`]'s full name, e.g. "--force".
var Text longName; var Text longName;
/// Human-readable description of the option. Can be `none`.
var Text description; var Text description;
/// List of required parameters of this [`Command::Option`]. // Option can also have their own parameters
var array<Parameter> required; var array<Parameter> required;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> optional; var array<Parameter> optional;
}; };
/// Structure that defines what sub-commands and options command has // Structure that defines what sub-commands and options command has
/// (and what parameters they take) // (and what parameters they take)
struct Data { struct Data {
/// Command group this command belongs to // 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; var protected Text group;
/// Short summary of what command does (recommended to // Short summary of what command does (recommended to
/// keep it to 80 characters) // keep it to 80 characters)
var protected Text summary; var protected Text summary;
/// Available subcommands.
var protected array<SubCommand> subCommands; var protected array<SubCommand> subCommands;
/// Available options, common to all subcommands.
var protected array<Option> options; var protected array<Option> options;
/// `true` iff related [`Command`] targets players.
var protected bool requiresTarget; var protected bool requiresTarget;
}; };
var protected Data commandData; var protected Data commandData;
/// Setting variable that defines a name that will be chosen for command by
/// default.
var protected const string preferredName;
/// Name that was used to register this command.
var protected Text usedName;
/// Settings variable that defines a class to be used for this [`Command`]'s
/// permissions config
var protected const class<CommandPermissions> permissionsConfigClass;
// We do not really ever need to create more than one instance of each class // 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. // of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance; var private Command mainInstance;
/// When command is being executed we create several instances of
/// `ConsoleWriter` that can be used for command output. // When command is being executed we create several instances of `ConsoleWriter` that can be used
/// They will also be automatically deallocated once command is executed. // for command output. They will also be automatically deallocated once command is executed.
/// //
/// DO NOT modify them or deallocate any of them manually. // DO NOT modify them or deallocate any of them manually.
/// //
/// This should make output more convenient and standardized. // This should make output more convenient and standardized.
/// //
/// 1. `publicConsole` - sends messages to all present players; // 1. `publicConsole` - sends messages to all present players;
/// 2. `callerConsole` - sends messages to the player that called the command; // 2. `callerConsole` - sends messages to the player that called the command;
/// 3. `targetConsole` - sends messages to the player that is currently being // 3. `targetConsole` - sends messages to the player that is currently being targeted
/// targeted (different each call of `ExecutedFor()` and `none` during // (different each call of `ExecutedFor()` and `none` during `Executed()` call);
/// `Executed()` call); // 4. `othersConsole` - sends messaged to every player that is neither "caller" or "target".
/// 4. `othersConsole` - sends messaged to every player that is neither
/// "caller" or "target".
var protected ConsoleWriter publicConsole, othersConsole; var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole; var protected ConsoleWriter callerConsole, targetConsole;
protected function Constructor() { protected function Constructor() {
local CommandDataBuilder dataBuilder; local CommandDataBuilder dataBuilder;
if (permissionsConfigClass != none) {
permissionsConfigClass.static.Initialize();
}
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
// Let user fill-in the rest
BuildData(dataBuilder); BuildData(dataBuilder);
commandData = dataBuilder.BorrowData(); commandData = dataBuilder.BorrowData();
dataBuilder.FreeSelf(); dataBuilder.FreeSelf();
@ -277,10 +246,8 @@ protected function Finalizer() {
local array<Option> options; local array<Option> options;
DeallocateConsoles(); DeallocateConsoles();
_.memory.Free(usedName); _.memory.Free(commandData.name);
_.memory.Free(commandData.summary); _.memory.Free(commandData.summary);
usedName = none;
commandData.summary = none;
subCommands = commandData.subCommands; subCommands = commandData.subCommands;
for (i = 0; i < options.length; i += 1) { for (i = 0; i < options.length; i += 1) {
_.memory.Free(subCommands[i].name); _.memory.Free(subCommands[i].name);
@ -303,55 +270,43 @@ protected function Finalizer() {
commandData.options.length = 0; commandData.options.length = 0;
} }
/// Initializes command, providing it with a specific name. private final function CleanParameters(array<Parameter> parameters) {
/// local int i;
/// Argument cannot be `none`, otherwise initialization fails.
/// [`Command`] can only be successfully initialized once.
public final function bool Initialize(BaseText commandName) {
if (commandName == none) return false;
if (usedName != none) return false;
usedName = commandName.LowerCopy(); for (i = 0; i < parameters.length; i += 1) {
return true; _.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 /// Overload this method to use `builder` to define parameters and options for your command.
/// your command.
protected function BuildData(CommandDataBuilder builder){} protected function BuildData(CommandDataBuilder builder){}
/// Overload this method to perform required actions when your command is /// Overload this method to perform required actions when your command is called.
/// called. ///
/// [`arguments`] is a `struct` filled with parameters that your command has been called with.
/// Guaranteed to not be in error state.
/// ///
/// [`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. /// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call. protected function Executed(CallData arguments, EPlayer instigator){}
protected function Executed(
CallData arguments, /// Overload this method to perform required actions when your command is called with a given player
EPlayer instigator, /// as a target.
CommandPermissions permissions) {}
/// 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 /// If several players have been specified - this method will be called once for each.
/// for each.
/// ///
/// If your command does not require a target - this method will not be called. /// 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. /// [`target`] is a player that this command must perform an action on.
/// [`arguments`] is a `struct` filled with parameters that your command has /// [`arguments`] is a `struct` filled with parameters that your command has been called with.
/// been called with. Guaranteed to not be in error state. /// Guaranteed to not be in error state.
///
/// [`instigator`] is a player that instigated this execution. /// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call. protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) {}
protected function ExecutedFor(
EPlayer target, /// Returns an instance of command (of particular class) that is stored "as a singleton" in
CallData arguments, /// command's class itself. Do not deallocate it.
EPlayer instigator,
CommandPermissions permissions) {}
/// 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() { public final static function Command GetInstance() {
if (default.mainInstance == none) { if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class)); default.mainInstance = Command(__().memory.Allocate(default.class));
@ -359,19 +314,18 @@ public final static function Command GetInstance() {
return default.mainInstance; return default.mainInstance;
} }
/// Forces command to process (parse) player's input, producing a structure with /// Forces command to process (parse) player's input, producing a structure with parsed data in
/// parsed data in Acedia's format instead. /// Acedia's format instead.
/// ///
/// Use `Execute()` for actually performing command's actions. /// Use `Execute()` for actually performing command's actions.
/// ///
/// [`subCommandName`] can be optionally specified to use as sub-command. /// [`subCommandName`] can be optionally specified to use as sub-command.
/// If this argument's value is `none` - sub-command name will be parsed from /// If this argument's value is `none` - sub-command name will be parsed from the `parser`'s data.
/// the `parser`'s data.
/// ///
/// Returns `CallData` structure that contains all the information about /// Returns `CallData` structure that contains all the information about parameters specified in
/// parameters specified in `parser`'s contents. /// `parser`'s contents.
/// Returned structure contains objects that must be deallocated, which can /// Returned structure contains objects that must be deallocated, which can easily be done by
/// easily be done by the auxiliary `DeallocateCallData()` method. /// the auxiliary `DeallocateCallData()` method.
public final function CallData ParseInputWith( public final function CallData ParseInputWith(
Parser parser, Parser parser,
EPlayer callerPlayer, EPlayer callerPlayer,
@ -409,27 +363,16 @@ public final function CallData ParseInputWith(
return callData; return callData;
} }
/// Executes caller `Command` with data provided by `callData` if it is in /// Executes caller `Command` with data provided by `callData` if it is in a correct state and
/// a correct state and reports error to `callerPlayer` if `callData` is /// reports error to `callerPlayer` if `callData` is invalid.
/// invalid.
/// ///
/// Returns `true` if command was successfully executed and `false` otherwise. /// Returns `true` if command was successfully executed and `false` otherwise.
/// Execution is considered successful if `Execute()` call was made, regardless /// Execution is considered successful if `Execute()` call was made, regardless of whether `Command`
/// of whether `Command` can actually perform required action. /// can actually perform required action.
/// For example, giving a weapon to a player can fail because he does not have /// For example, giving a weapon to a player can fail because he does not have enough space in his
/// enough space in his inventory, but it will still be considered a successful /// inventory, but it will still be considered a successful execution as far as return value is
/// execution as far as return value is concerned. /// concerned.
/// public final function bool Execute(CallData callData, EPlayer callerPlayer) {
/// [`permissions`] argument is supposed to specify permissions with which this
/// command runs.
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
/// being `none` should mean running with minimal priviledges.
public final function bool Execute(
CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local int i; local int i;
local array<EPlayer> targetPlayers; local array<EPlayer> targetPlayers;
@ -446,11 +389,11 @@ public final function bool Execute(
callerConsole = _.console.For(callerPlayer); callerConsole = _.console.For(callerPlayer);
callerConsole callerConsole
.Write(P("Executing command `")) .Write(P("Executing command `"))
.Write(usedName) .Write(commandData.name)
.Say(P("`")); .Say(P("`"));
// `othersConsole` should also exist in time for `Executed()` call // `othersConsole` should also exist in time for `Executed()` call
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
Executed(callData, callerPlayer, permissions); Executed(callData, callerPlayer);
_.memory.Free(othersConsole); _.memory.Free(othersConsole);
if (commandData.requiresTarget) { if (commandData.requiresTarget) {
for (i = 0; i < targetPlayers.length; i += 1) { for (i = 0; i < targetPlayers.length; i += 1) {
@ -459,7 +402,7 @@ public final function bool Execute(
.ForAll() .ForAll()
.ButPlayer(callerPlayer) .ButPlayer(callerPlayer)
.ButPlayer(targetPlayers[i]); .ButPlayer(targetPlayers[i]);
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions); ExecutedFor(targetPlayers[i], callData, callerPlayer);
_.memory.Free(othersConsole); _.memory.Free(othersConsole);
_.memory.Free(targetConsole); _.memory.Free(targetConsole);
} }
@ -470,209 +413,6 @@ public final function bool Execute(
return true; return true;
} }
/// 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;
}
}
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);
}
}
/// Returns name (in lower case) of the caller command class.
public final static function Text GetPreferredName() {
return __().text.FromString(Locs(default.preferredName));
}
/// Returns name (in lower case) of the caller command class.
public final static function string GetPreferredName_S() {
return Locs(default.preferredName);
}
/// Returns name (in lower case) of the caller command class.
public final function Text GetName() {
if (usedName == none) {
return P("").Copy();
}
return usedName.LowerCopy();
}
/// Returns name (in lower case) of the caller command class.
public final function string GetName_S() {
if (usedName == none) {
return "";
}
return _.text.IntoString(/*take*/ usedName.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 group name (in lower case) of the caller command class.
public final function string GetGroupName_S() {
if (commandData.group == none) {
return "";
}
return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig(BaseText configName) {
if (configName == none) return none;
if (default.permissionsConfigClass == none) return none;
// This creates default config if it is missing
default.permissionsConfigClass.static.NewConfig(configName);
return CommandPermissions(default.permissionsConfigClass.static
.GetConfigInstance(configName));
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig_S(string configName) {
local MutableText wrapper;
local CommandPermissions result;
wrapper = __().text.FromStringM(configName);
result = LoadConfig(wrapper);
__().memory.Free(wrapper);
return result;
}
/// Returns subcommands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
local int i, j;
local bool addSubCommand;
local array<string> forbiddenCommands;
local array<Text> result;
forbiddenCommands = permissions.forbiddenSubCommands;
if (permissions != none) {
forbiddenCommands = permissions.forbiddenSubCommands;
}
for (i = 0; i < commandData.subCommands.length; i += 1) {
addSubCommand = true;
for (j = 0; j < forbiddenCommands.length; j += 1) {
if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
addSubCommand = false;
break;
}
}
if (addSubCommand) {
result[result.length] = commandData.subCommands[i].name.LowerCopy();
}
}
return result;
}
/// Returns sub commands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
return _.text.IntoStrings(GetSubCommands(permissions));
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed as either argument, returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing subcommand is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed(
BaseText subCommand,
CommandPermissions permissions
) {
if (subCommand == none) return true;
if (permissions == none) return true;
return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed for permissions, always returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing sub command is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed_S(
string subCommand,
CommandPermissions permissions
) {
local int i;
local array<string> forbiddenCommands;
if (permissions == none) {
return true;
}
forbiddenCommands = permissions.forbiddenSubCommands;
for (i = 0; i < forbiddenCommands.length; i += 1) {
if (subCommand ~= forbiddenCommands[i]) {
return false;
}
}
return true;
}
/// 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;
}
private final function DeallocateConsoles() { private final function DeallocateConsoles() {
if (publicConsole != none && publicConsole.IsAllocated()) { if (publicConsole != none && publicConsole.IsAllocated()) {
_.memory.Free(publicConsole); _.memory.Free(publicConsole);
@ -692,8 +432,20 @@ private final function DeallocateConsoles() {
othersConsole = none; othersConsole = none;
} }
/// Reports given error to the `callerPlayer`, appropriately picking /// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
/// message color 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) { private final function ReportError(CallData callData, EPlayer callerPlayer) {
local Text errorMessage; local Text errorMessage;
local ConsoleWriter console; local ConsoleWriter console;
@ -799,7 +551,35 @@ private final function array<EPlayer> ParseTargets(Parser parser, EPlayer caller
return targetPlayers; 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 { defaultproperties {
preferredName = ""
permissionsConfigClass = none
} }

1568
sources/BaseAPI/API/Commands/CommandAPI.uc

File diff suppressed because it is too large Load Diff

755
sources/BaseAPI/API/Commands/CommandDataBuilder.uc

File diff suppressed because it is too large Load Diff

249
sources/BaseAPI/API/Commands/CommandList.uc

@ -1,249 +0,0 @@
/**
* Config class for storing map lists.
* 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 CommandList extends AcediaConfig
perObjectConfig
config(AcediaCommands);
//! `CommandList` describes a set of commands and votings that can be made
//! available to users inside Commands feature
//!
//! Optionally, permission configs can be specified for commands and votings,
//! allowing server admins to create command lists for different groups player
//! with the same commands, but different permissions.
// For storing `class<Command>` - `string` pairs in the config
struct CommandConfigStoragePair {
var public class<Command> cmd;
var public string config;
};
// For storing `class` - `string` pairs in the config
struct VotingConfigStoragePair {
var public class<Voting> vtn;
var public string config;
};
// For returning `class` - `Text` pairs into other Acedia classes
struct EntityConfigPair {
var public class<AcediaObject> class;
var public Text config;
};
/// Allows to specify if this list should only be added when server is running
/// in debug mode.
/// `true` means yes, `false` means that list will always be available.
var public config bool debugOnly;
/// Adds a command of specified class with a "default" permissions config.
var public config array< class<Command> > command;
/// Adds a command of specified class with specified permissions config
var public config array<CommandConfigStoragePair> commandWith;
/// Adds a voting of specified class with a "default" permissions config
var public config array< class<Voting> > voting;
/// Adds a voting of specified class with specified permissions config
var public config array<VotingConfigStoragePair> votingWith;
public final function array<EntityConfigPair> GetCommandData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < command.length; i += 1) {
if (command[i] != none) {
nextPair.class = command[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < commandWith.length; i += 1) {
if (commandWith[i].cmd != none) {
nextPair.class = commandWith[i].cmd;
if (commandWith[i].config != "") {
nextPair.config = _.text.FromString(commandWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
public final function array<EntityConfigPair> GetVotingData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < voting.length; i += 1) {
if (voting[i] != none) {
nextPair.class = voting[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < votingWith.length; i += 1) {
if (votingWith[i].vtn != none) {
nextPair.class = votingWith[i].vtn;
if (votingWith[i].config != "") {
nextPair.config = _.text.FromString(votingWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
protected function HashTable ToData() {
local int i;
local ArrayList entityArray;
local HashTable result, innerPair;
result = _.collections.EmptyHashTable();
result.SetBool(P("debugOnly"), debugOnly);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < command.length; i += 1) {
entityArray.AddString(string(command[i]));
}
result.SetItem(P("commands"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < voting.length; i += 1) {
entityArray.AddString(string(voting[i]));
}
result.SetItem(P("votings"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < commandWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("command"), string(commandWith[i].cmd));
innerPair.SetString(P("config"), commandWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("commandsWithConfig"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < votingWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("voting"), string(votingWith[i].vtn));
innerPair.SetString(P("config"), votingWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("votingsWithConfig"), entityArray);
_.memory.Free(entityArray);
return result;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList entityArray;
local HashTable innerPair;
local class<Command> nextCommandClass;
local class<Voting> nextVotingClass;
local CommandConfigStoragePair nextCommandPair;
local VotingConfigStoragePair nextVotingPair;
if (source == none) {
return;
}
debugOnly = source.GetBool(P("debugOnly"));
command.length = 0;
entityArray = source.GetArrayList(P("commands"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextCommandClass = class<Command>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextCommandClass != none) {
command[command.length] = nextCommandClass;
}
}
}
_.memory.Free(entityArray);
voting.length = 0;
entityArray = source.GetArrayList(P("votings"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextVotingClass = class<Voting>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextVotingClass != none) {
voting[voting.length] = nextVotingClass;
}
}
}
_.memory.Free(entityArray);
commandWith.length = 0;
entityArray = source.GetArrayList(P("commandsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextCommandPair.cmd =
class<Command>(_.memory.LoadClass_S(innerPair.GetString(P("command"))));
nextCommandPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextCommandPair.cmd != none) {
commandWith[commandWith.length] = nextCommandPair;
}
}
}
_.memory.Free(entityArray);
votingWith.length = 0;
entityArray = source.GetArrayList(P("votingsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextVotingPair.vtn =
class<Voting>(_.memory.LoadClass_S(innerPair.GetString(P("voting"))));
nextVotingPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextVotingPair.vtn != none) {
votingWith[votingWith.length] = nextVotingPair;
}
}
}
_.memory.Free(entityArray);
}
protected function DefaultIt() {
debugOnly = false;
command.length = 0;
commandWith.length = 0;
voting.length = 0;
votingWith.length = 0;
command[0] = class'ACommandHelp';
command[1] = class'ACommandVote';
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
debugOnly = false
command(0) = class'ACommandHelp'
command(1) = class'ACommandVote'
}

350
sources/BaseAPI/API/Commands/CommandParser.uc

@ -23,60 +23,51 @@ class CommandParser extends AcediaObject
dependson(Command); dependson(Command);
//! Class specialized for parsing user input of the command's call into //! Class specialized for parsing user input of the command's call into `Command.CallData` structure
//![ `Command.CallData`] structure with the information about all parsed //! with the information about all parsed arguments.
//! arguments.
//! //!
//! [`CommandParser`] is not made to parse the whole input: //! `CommandParser` is not made to parse the whole input:
//! //!
//! * Command's name needs to be parsed and resolved as an alias before using //! * Command's name needs to be parsed and resolved as an alias before using this parser -
//! this parser - it won't do this hob for you; //! it won't do this hob for you;
//! * List of targeted players must also be parsed using [`PlayersParser`] - //! * List of targeted players must also be parsed using `PlayersParser` - `CommandParser` won't do
//! [`CommandParser`] won't do this for you; //! this for you;
//! * Optionally one can also decide on the referred subcommand and pass it into //! * Optionally one can also decide on the referred subcommand and pass it into `ParseWith()`
//! [`ParseWith()`] method. If subcommand's name is not passed - //! method. If subcommand's name is not passed - `CommandParser` will try to parse it itself.
//! [`CommandParser`] will try to parse it itself.
//! This feature is used to add support for subcommand aliases. //! This feature is used to add support for subcommand aliases.
//! //!
//! However, above steps are handled by [`Commands_Feature`] and one only needs to //! However, above steps are handled by `Commands_Feature` and one only needs to call that feature's
//! call that feature's [`HandleInput()`] methods to pass user input with command //! `HandleInput()` methods to pass user input with command call line there.
//! call line there.
//! //!
//! # Usage //! # Usage
//! //!
//! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with: //! Allocate `CommandParser` and call `ParseWith()` method, providing it with:
//! //!
//! 1. [`Parser`], filled with command call input; //! 1. `Parser`, filled with command call input;
//! 2. Command's data that describes subcommands, options and their parameters //! 2. Command's data that describes subcommands, options and their parameters for the command,
//! for the command, which call we are parsing; //! which call we are parsing;
//! 3. (Optionally) [`EPlayer`] reference to the player that initiated //! 3. (Optionally) `EPlayer` reference to the player that initiated the command call;
//! the command call; //! 4. (Optionally) Subcommand to be used - this will prevent `CommandParser` from parsing
//! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`] //! subcommand name itself. Used for implementing aliases that refer to a particular subcommand.
//! from parsing subcommand name itself. Used for implementing aliases that
//! refer to a particular subcommand.
//! //!
//! # Implementation //! # Implementation
//! //!
//! [`CommandParser`] stores both its state and command data, relevant to parsing, //! `CommandParser` stores both its state and command data, relevant to parsing, as its member
//! as its member variables during the whole parsing process, instead of passing //! variables during the whole parsing process, instead of passing that data around in every single
//! that data around in every single method. //! method.
//! //!
//! We will give a brief overview of how around 20 parsing methods below are //! We will give a brief overview of how around 20 parsing methods below are interconnected.
//! interconnected.
//! //!
//! The only public method [`ParseWith()`] is used to start parsing and it uses //! The only public method `ParseWith()` is used to start parsing and it uses `PickSubCommand()` to
//! [`PickSubCommand()`] to first try and figure out what sub command is //! first try and figure out what sub command is intended by user's input.
//! intended by user's input.
//! //!
//! Main bulk of the work is done by [`ParseParameterArrays()`] method, for //! Main bulk of the work is done by `ParseParameterArrays()` method, for simplicity broken into two
//! simplicity broken into two [`ParseRequiredParameterArray()`] and //! `ParseRequiredParameterArray()` and `ParseOptionalParameterArray()` methods that can parse
//! [`ParseOptionalParameterArray()`] methods that can parse
//! parameters for both command itself and it's options. //! parameters for both command itself and it's options.
//! //!
//! They go through arrays of required and optional parameters, calling //! They go through arrays of required and optional parameters, calling `ParseParameter()` for each
//! [`ParseParameter()`] for each parameters, which in turn can make several //! parameters, which in turn can make several calls of `ParseSingleValue()` to parse parameters'
//! calls of [`ParseSingleValue()`] to parse parameters' values: it is called //! values: it is called once for single-valued parameters, but possibly several times for list
//! once for single-valued parameters, but possibly several times for list
//! parameters that can contain several values. //! parameters that can contain several values.
//! //!
//! So main parsing method looks something like: //! So main parsing method looks something like:
@ -89,25 +80,22 @@ class CommandParser extends AcediaObject
//! } //! }
//! ``` //! ```
//! //!
//! [`ParseSingleValue()`] is essentially that redirects it's method call to //! `ParseSingleValue()` is essentially that redirects it's method call to another, more specific,
//! another, more specific, parsing method based on the parameter type. //! parsing method based on the parameter type.
//! //!
//! Finally, to allow users to specify options at any point in command, we call //! Finally, to allow users to specify options at any point in command, we call
//! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`] //! `TryParsingOptions()` at the beginning of every `ParseSingleValue()` (the only parameter that
//! (the only parameter that has higher priority than options is //! has higher priority than options is `CPT_Remainder`), since option definition can appear at any
//! [`CPT_Remainder`]), since option definition can appear at any place between //! place between parameters. We also call `TryParsingOptions()` *after* we've parsed all command's
//! parameters. We also call `TryParsingOptions()` *after* we've parsed all //! parameters, since that case won't be detected by parsing them *before* every parameter.
//! command's parameters, since that case won't be detected by parsing them
//! *before* every parameter.
//! //!
//! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes //! `TryParsingOptions()` itself simply tries to detect "-" and "--" prefixes (filtering out
//! (filtering out negative numeric values) and then redirect the call to either //! negative numeric values) and then redirect the call to either of more specialized methods:
//! of more specialized methods: [`ParseLongOption()`] or //! `ParseLongOption()` or `ParseShortOption()`, that can in turn make another
//! [`ParseShortOption()`], that can in turn make another //! `ParseParameterArrays()` call, if specified option has parameters.
//! [`ParseParameterArrays()`] call, if specified option has parameters.
//! //!
//! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option //! NOTE: `ParseParameterArrays()` can only nest in itself once, since option declaration always
//! declaration always interrupts previous option's parameter list. //! interrupts previous option's parameter list.
//! Rest of the methods perform simple auxiliary functions. //! Rest of the methods perform simple auxiliary functions.
// Describes which parameters we are currently parsing, classifying them // Describes which parameters we are currently parsing, classifying them
@ -119,7 +107,7 @@ class CommandParser extends AcediaObject
// * Still parsing required *parameter* "integer list"; // * Still parsing required *parameter* "integer list";
// * But no more integers are *necessary* for successful parsing. // * But no more integers are *necessary* for successful parsing.
// //
// Therefore we consider parameter "necessary" if the lack of it will // Therefore we consider parameter "necessary" if the lack of it will\
// result in failed parsing and "extra" otherwise. // result in failed parsing and "extra" otherwise.
enum ParsingTarget { enum ParsingTarget {
// We are in the process of parsing required parameters, that must all // We are in the process of parsing required parameters, that must all
@ -135,31 +123,31 @@ enum ParsingTarget {
}; };
// Parser filled with user input. // Parser filled with user input.
var private Parser commandParser; var private Parser commandParser;
// Data for sub-command specified by both command we are parsing // Data for sub-command specified by both command we are parsing
// and user's input; determined early during parsing. // and user's input; determined early during parsing.
var private Command.SubCommand pickedSubCommand; var private Command.SubCommand pickedSubCommand;
// Options available for the command we are parsing. // Options available for the command we are parsing.
var private array<Command.Option> availableOptions; var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process, // Result variable we are filling during the parsing process,
// should be `none` outside of [`self.ParseWith()`] method call. // should be `none` outside of `self.ParseWith()` method call.
var private Command.CallData nextResult; var private Command.CallData nextResult;
// Parser for player parameters, setup with a caller for current parsing // Parser for player parameters, setup with a caller for current parsing
var private PlayersParser currentPlayersParser; var private PlayersParser currentPlayersParser;
// Current [`ParsingTarget`], see it's enum description for more details // Current `ParsingTarget`, see it's enum description for more details
var private ParsingTarget currentTarget; var private ParsingTarget currentTarget;
// `true` means we are parsing parameters for a command's option and // `true` means we are parsing parameters for a command's option and
// `false` means we are parsing command's own parameters // `false` means we are parsing command's own parameters
var private bool currentTargetIsOption; var private bool currentTargetIsOption;
// If we are parsing parameters for an option (`currentTargetIsOption == true`) // If we are parsing parameters for an option (`currentTargetIsOption == true`)
// this variable will store that option's data. // this variable will store that option's data.
var private Command.Option targetOption; var private Command.Option targetOption;
// Last successful state of [`commandParser`]. // Last successful state of `commandParser`.
var Parser.ParserState confirmedState; var Parser.ParserState confirmedState;
// Options we have so far encountered during parsing, necessary since we want // Options we have so far encountered during parsing, necessary since we want
// to forbid specifying th same option more than once. // to forbid specifying th same option more than once.
var private array<Command.Option> usedOptions; var private array<Command.Option> usedOptions;
// Literals that can be used as boolean values // Literals that can be used as boolean values
var private array<string> booleanTrueEquivalents; var private array<string> booleanTrueEquivalents;
@ -171,61 +159,6 @@ protected function Finalizer() {
Reset(); Reset();
} }
/// 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;
}
// Zero important variables // Zero important variables
private final function Reset() { private final function Reset() {
local Command.CallData blankCallData; local Command.CallData blankCallData;
@ -253,11 +186,11 @@ private final function DeclareError(Command.ErrorType type, optional BaseText ca
// Assumes `commandParser != none`, is in successful state. // Assumes `commandParser != none`, is in successful state.
// //
// Picks a sub command based on it's contents (parser's pointer must be before // Picks a sub command based on it's contents (parser's pointer must be before where subcommand's
// where subcommand's name is specified). // name is specified).
// //
// If [`specifiedSubCommand`] is not `none` - will always use that value instead // If `specifiedSubCommand` is not `none` - will always use that value instead of parsing it from
// of parsing it from [`commandParser`]. // `commandParser`.
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) { private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) {
local int i; local int i;
local MutableText candidateSubCommandName; local MutableText candidateSubCommandName;
@ -291,11 +224,64 @@ private final function PickSubCommand(Command.Data commandData, BaseText specifi
} }
} }
// We will only reach here if we did not match any sub commands, // We will only reach here if we did not match any sub commands,
// meaning that whatever consumed by[ `candidateSubCommandName`] probably // meaning that whatever consumed by `candidateSubCommandName` probably
// has a different meaning. // has a different meaning.
commandParser.RestoreState(confirmedState); 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` // Assumes `commandParser` is not `none`
// Declares an error if `commandParser` still has any input left // Declares an error if `commandParser` still has any input left
private final function AssertNoTrailingInput() { private final function AssertNoTrailingInput() {
@ -310,8 +296,7 @@ private final function AssertNoTrailingInput() {
} }
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// Parses given required and optional parameters along with any possible option // Parses given required and optional parameters along with any possible option declarations.
// declarations.
// Returns `HashTable` filled with (variable, parsed value) pairs. // Returns `HashTable` filled with (variable, parsed value) pairs.
// Failure is equal to `commandParser` entering into a failed state. // Failure is equal to `commandParser` entering into a failed state.
private final function HashTable ParseParameterArrays( private final function HashTable ParseParameterArrays(
@ -369,8 +354,8 @@ private final function ParseRequiredParameterArray(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses given optional parameters along with any possible option declarations // Parses given optional parameters along with any possible option declarations into given
// into given `parsedParameters` hash table. // `parsedParameters` hash table.
private final function ParseOptionalParameterArray( private final function ParseOptionalParameterArray(
HashTable parsedParameters, HashTable parsedParameters,
array<Command.Parameter> optionalParameters array<Command.Parameter> optionalParameters
@ -399,11 +384,10 @@ private final function ParseOptionalParameterArray(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses one given parameter along with any possible option declarations into // Parses one given parameter along with any possible option declarations into given
// given `parsedParameters` `HashTable`. // `parsedParameters` `HashTable`.
// //
// Returns `true` if we've successfully parsed given parameter without any // Returns `true` if we've successfully parsed given parameter without any errors.
// errors.
private final function bool ParseParameter( private final function bool ParseParameter(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -439,12 +423,10 @@ private final function bool ParseParameter(
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses a single value for a given parameter (e.g. one integer for integer or // Parses a single value for a given parameter (e.g. one integer for integer or integer list
// integer list parameter types) along with any possible option declarations // parameter types) along with any possible option declarations into given `parsedParameters`.
// into given `parsedParameters`.
// //
// Returns `true` if we've successfully parsed a single value without // Returns `true` if we've successfully parsed a single value without any errors.
// any errors.
private final function bool ParseSingleValue( private final function bool ParseSingleValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -555,8 +537,7 @@ private final function bool ParseIntegerValue(
} }
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single number (float) value into given `parsedParameters` // Parses a single number (float) value into given `parsedParameters` hash table.
// hash table.
private final function bool ParseNumberValue( private final function bool ParseNumberValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -610,8 +591,8 @@ private final function bool ParseTextValue(
return true; return true;
} }
// Resolves alias and returns it, along with the resolved value, if parameter // Resolves alias and returns it, along with the resolved value, if parameter was specified to be
// was specified to be auto-resolved. // auto-resolved.
// Returns `none` otherwise. // Returns `none` otherwise.
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) { private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) {
local HashTable result; local HashTable result;
@ -647,8 +628,8 @@ private final function HashTable AutoResolveAlias(MutableText textValue, Text al
// Assumes `commandParser` and `parsedParameters` are not `none`. // Assumes `commandParser` and `parsedParameters` are not `none`.
// //
// Parses a single `Text` value into given `parsedParameters` hash table, // Parses a single `Text` value into given `parsedParameters` hash table, consuming all remaining
// consuming all remaining contents. // contents.
private final function bool ParseRemainderValue( private final function bool ParseRemainderValue(
HashTable parsedParameters, HashTable parsedParameters,
Command.Parameter expectedParameter Command.Parameter expectedParameter
@ -767,15 +748,14 @@ private final function RecordParameter(
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// //
// Tries to parse an option declaration (along with all of it's parameters) with // Tries to parse an option declaration (along with all of it's parameters) with `commandParser`.
// `commandParser`.
// //
// Returns `true` on success and `false` otherwise. // Returns `true` on success and `false` otherwise.
// //
// In case of failure to detect option declaration also reverts state of // In case of failure to detect option declaration also reverts state of `commandParser` to that
// `commandParser` to that before `TryParsingOptions()` call. // before `TryParsingOptions()` call.
// However, if option declaration was present, but invalid (or had invalid // However, if option declaration was present, but invalid (or had invalid parameters) parser
// parameters) parser will be left in a failed state. // will be left in a failed state.
private final function bool TryParsingOptions() { private final function bool TryParsingOptions() {
local int temporaryInt; local int temporaryInt;
@ -815,12 +795,12 @@ private final function bool TryParsingOptions() {
// Assumes `commandParser` is not `none`. // Assumes `commandParser` is not `none`.
// //
// Tries to parse a long option name along with all of it's possible parameters // Tries to parse a long option name along with all of it's possible parameters with
// with `commandParser`. // `commandParser`.
// //
// Returns `true` on success and `false` otherwise. At the point this method is // Returns `true` on success and `false` otherwise. At the point this method is called, option
// called, option declaration is already assumed to be detected and any failure // declaration is already assumed to be detected and any failure implies parsing error
// implies parsing error (ending in failed `Command.CallData`). // (ending in failed `Command.CallData`).
private final function bool ParseLongOption() { private final function bool ParseLongOption() {
local int i, optionIndex; local int i, optionIndex;
local MutableText optionName; local MutableText optionName;
@ -852,16 +832,17 @@ private final function bool ParseLongOption() {
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
// //
// Tries to parse a short option name along with all of it's possible parameters // Tries to parse a short option name along with all of it's possible parameters with
// with `commandParser`. // `commandParser`.
// //
// Returns `true` on success and `false` otherwise. At the point this // Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected // method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `Command.CallData`). // and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseShortOption() { private final function bool ParseShortOption()
local int i; {
local bool pickedOptionWithParameters; local int i;
local MutableText optionsList; local bool pickedOptionWithParameters;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true); commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok()) { if (!commandParser.Ok()) {
@ -883,28 +864,24 @@ private final function bool ParseShortOption() {
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
// //
// Auxiliary method that adds option by it's short version's character // Auxiliary method that adds option by it's short version's character `optionCharacter`.
// `optionCharacter`.
// //
// It also accepts `optionSourceList` that describes short option expression // It also accepts `optionSourceList` that describes short option expression (e.g. "-rtV") from
// (e.g. "-rtV") from // which it originated for error reporting and `forbidOptionWithParameters` that, when set to
// which it originated for error reporting and `forbidOptionWithParameters` // `true`, forces this method to cause the `CET_MultipleOptionsWithParams` error if new option has
// that, when set to `true`, forces this method to cause the // non-empty parameters.
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters.
// //
// Method returns `true` if added option had non-empty parameters and `false` // Method returns `true` if added option had non-empty parameters and `false` otherwise.
// otherwise.
// //
// Any parsing failure inside this method always causes // Any parsing failure inside this method always causes `nextError.DeclareError()` call, so you
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()` // can use `nextResult.IsSuccessful()` to check if method has failed.
// to check if method has failed.
private final function bool AddOptionByCharacter( private final function bool AddOptionByCharacter(
BaseText.Character optionCharacter, BaseText.Character optionCharacter,
BaseText optionSourceList, BaseText optionSourceList,
bool forbidOptionWithParameters bool forbidOptionWithParameters
) { ) {
local int i; local int i;
local bool optionHasParameters; local bool optionHasParameters;
// Prevent same option appearing twice // Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1) { for (i = 0; i < usedOptions.length; i += 1) {
@ -938,8 +915,7 @@ private final function bool AddOptionByCharacter(
} }
// Auxiliary method for parsing option's parameters (including empty ones). // Auxiliary method for parsing option's parameters (including empty ones).
// Automatically fills `nextResult` with parsed parameters (or `none` if option // Automatically fills `nextResult` with parsed parameters (or `none` if option has no parameters).
// has no parameters).
// Assumes `commandParser` and `nextResult` are not `none`. // Assumes `commandParser` and `nextResult` are not `none`.
private final function bool ParseOptionParameters(Command.Option pickedOption) { private final function bool ParseOptionParameters(Command.Option pickedOption) {
local HashTable optionParameters; local HashTable optionParameters;
@ -970,13 +946,13 @@ private final function bool ParseOptionParameters(Command.Option pickedOption) {
} }
defaultproperties { defaultproperties {
booleanTrueEquivalents(0) = "true" booleanTrueEquivalents(0) = "true"
booleanTrueEquivalents(1) = "enable" booleanTrueEquivalents(1) = "enable"
booleanTrueEquivalents(2) = "on" booleanTrueEquivalents(2) = "on"
booleanTrueEquivalents(3) = "yes" booleanTrueEquivalents(3) = "yes"
booleanFalseEquivalents(0) = "false" booleanFalseEquivalents(0) = "false"
booleanFalseEquivalents(1) = "disable" booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off" booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no" booleanFalseEquivalents(3) = "no"
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.") errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
} }

66
sources/BaseAPI/API/Commands/CommandPermissions.uc

@ -1,66 +0,0 @@
/**
* 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 CommandPermissions extends AcediaConfig
perobjectconfig
config(AcediaCommands)
abstract;
var public config array<string> forbiddenSubCommands;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList forbiddenList;
data = _.collections.EmptyHashTable();
forbiddenList = _.collections.EmptyArrayList();
for (i = 0; i < forbiddenSubCommands.length; i += 1) {
forbiddenList.AddString(Locs(forbiddenSubCommands[i]));
}
data.SetItem(P("forbiddenSubCommands"), forbiddenList);
_.memory.Free(forbiddenList);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList forbiddenList;
if (source == none) return;
forbiddenList = source.GetArrayList(P("forbiddenSubCommands"));
if (forbiddenList == none) return;
forbiddenSubCommands.length = 0;
for (i = 0; i < forbiddenList.GetLength(); i += 1) {
forbiddenSubCommands[i] = forbiddenList.GetString(i);
}
_.memory.Free(forbiddenList);
}
protected function DefaultIt() {
forbiddenSubCommands.length = 0;
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
}

59
sources/BaseAPI/API/Commands/CommandRegistrationJob.uc

@ -19,63 +19,36 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class CommandRegistrationJob extends SchedulerJob class CommandRegistrationJob extends SchedulerJob;
dependson(CommandAPI);
var private CommandAPI.AsyncTask nextItem; var private class<Command> nextCommand;
// Expecting 300 units of work, this gives us registering 20 commands per tick
const ADDING_COMMAND_COST = 15;
// Adding voting option is approximately the same as adding a command's
// single sub-command - we'll estimate it as 1/3rd of the full value
const ADDING_VOTING_COST = 5;
// Authorizing is relatively cheap, whether it's commands or voting
const AUTHORIZING_COST = 1;
protected function Constructor() { protected function Constructor() {
nextItem = _.commands._popPending(); nextCommand = _.commands._popPending();
} }
protected function Finalizer() { protected function Finalizer() {
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName); nextCommand = none;
nextItem.entityClass = none;
nextItem.entityName = none;
nextItem.userGroup = none;
nextItem.configName = none;
} }
public function bool IsCompleted() { public function bool IsCompleted() {
return (nextItem.entityName == none); return (nextCommand == none);
} }
public function DoWork(int allottedWorkUnits) { public function DoWork(int allottedWorkUnits) {
while (allottedWorkUnits > 0 && nextItem.entityName != none) { local int i, iterationsAmount;
if (nextItem.type == CAJT_AddCommand) {
allottedWorkUnits -= ADDING_COMMAND_COST; // Expected 300 units per tick, to register 20 commands per tick use about 10
_.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName); iterationsAmount = Max(allottedWorkUnits / 10, 1);
_.memory.Free(nextItem.entityName); for (i = 0; i < iterationsAmount; i += 1) {
} else if (nextItem.type == CAJT_AddVoting) { _.commands.RegisterCommand(nextCommand);
allottedWorkUnits -= ADDING_VOTING_COST; nextCommand = _.commands._popPending();
_.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName); if (nextCommand == none) {
_.memory.Free(nextItem.entityName); break;
} else if (nextItem.type == CAJT_AuthorizeCommand) {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeCommandUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeVotingUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} }
nextItem = _.commands._popPending();
} }
} }
defaultproperties { defaultproperties
{
} }

182
sources/BaseAPI/API/Commands/Commands.uc

@ -21,210 +21,40 @@
*/ */
class Commands extends FeatureConfig class Commands extends FeatureConfig
perobjectconfig perobjectconfig
config(AcediaCommands); config(AcediaSystem);
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandSetGroupPair {
/// Name of the command set to add
var public string name;
/// Name of the group, for which to add this set
var public string for;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> rename;
/// Name to use for that class
var public string to;
};
/// Setting this to `true` enables players to input commands with "mutate"
/// console command.
/// Default is `true`.
var public config bool useMutateInput;
/// Setting this to `true` enables players to input commands right in the chat
/// by prepending them with [`chatCommandPrefix`].
/// Default is `true`.
var public config bool useChatInput; var public config bool useChatInput;
/// Chat messages, prepended by this prefix will be treated as commands. var public config bool useMutateInput;
/// Default is "!". Empty values are also treated as "!".
var public config string chatCommandPrefix; var public config string chatCommandPrefix;
/// Allows to specify which user groups are used in determining command/votings
/// permission.
/// They must be specified in the order of importance: from the group with
/// highest level of permissions to the lowest. When determining player's
/// permission to use a certain command/voting, his group with the highest
/// available permissions will be used.
var public config array<string> commandGroup;
/// Add a specified `CommandList` to the specified user group
var public config array<CommandSetGroupPair> addCommandList;
/// Allows to specify a name for a certain command class
///
/// NOTE:By default command choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between commands from different packages.
var public config array<RenamingRulePair> renamingRule;
/// Allows to specify a name for a certain voting class
///
/// NOTE:By default voting choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between votings from different packages.
var public config array<RenamingRulePair> votingRenamingRule;
protected function HashTable ToData() { protected function HashTable ToData() {
local int i;
local HashTable data; local HashTable data;
local ArrayList innerList;
local HashTable innerPair;
data = __().collections.EmptyHashTable(); data = __().collections.EmptyHashTable();
data.SetBool(P("useChatInput"), useChatInput, true); data.SetBool(P("useChatInput"), useChatInput, true);
data.SetBool(P("useMutateInput"), useMutateInput, true); data.SetBool(P("useMutateInput"), useMutateInput, true);
data.SetString(P("chatCommandPrefix"), chatCommandPrefix); data.SetString(P("chatCommandPrefix"), chatCommandPrefix);
// Serialize `commandGroup`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < commandGroup.length; i += 1) {
innerList.AddString(commandGroup[i]);
}
data.SetItem(P("commandGroups"), innerList);
_.memory.Free(innerList);
// Serialize `addCommandSet`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("name"), addCommandList[i].name);
innerPair.SetString(P("for"), addCommandList[i].for);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("commandSets"), innerList);
_.memory.Free(innerList);
// Serialize `renamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(renamingRule[i].rename));
innerPair.SetString(P("to"), renamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("renamingRules"), innerList);
_.memory.Free(innerList);
// Serialize `votingRenamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename));
innerPair.SetString(P("to"), votingRenamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("votingRenamingRules"), innerList);
_.memory.Free(innerList);
return data; return data;
} }
protected function FromData(HashTable source) { protected function FromData(HashTable source) {
local int i;
local ArrayList innerList;
local HashTable innerPair;
local CommandSetGroupPair nextCommandSetGroupPair;
local RenamingRulePair nextRenamingRule;
local class<AcediaObject> nextClass;
if (source == none) { if (source == none) {
return; return;
} }
useChatInput = source.GetBool(P("useChatInput")); useChatInput = source.GetBool(P("useChatInput"));
useMutateInput = source.GetBool(P("useMutateInput")); useMutateInput = source.GetBool(P("useMutateInput"));
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!");
// De-serialize `commandGroup`
commandGroup.length = 0;
innerList = source.GetArrayList(P("commandGroups"));
if (innerList != none) {
for (i = 0; i < commandGroup.length; i += 1) {
commandGroup[i] = innerList.GetString(i);
}
_.memory.Free(innerList);
}
// De-serialize `addCommandSet`
addCommandList.length = 0;
innerList = source.GetArrayList(P("commandSets"));
if (innerList != none) {
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextCommandSetGroupPair.name = innerPair.GetString(P("name"));
nextCommandSetGroupPair.for = innerPair.GetString(P("for"));
addCommandList[addCommandList.length] = nextCommandSetGroupPair;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `renamingRule`
renamingRule.length = 0;
innerList = source.GetArrayList(P("renamingRules"));
if (innerList != none) {
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
renamingRule[renamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `votingRenamingRule`
votingRenamingRule.length = 0;
innerList = source.GetArrayList(P("votingRenamingRules"));
if (innerList != none) {
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
votingRenamingRule[votingRenamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
} }
protected function DefaultIt() { protected function DefaultIt() {
local CommandSetGroupPair defaultPair;
useChatInput = true; useChatInput = true;
useMutateInput = true; useMutateInput = true;
chatCommandPrefix = "!"; chatCommandPrefix = "!";
commandGroup[0] = "admin";
commandGroup[1] = "moderator";
commandGroup[2] = "trusted";
addCommandList.length = 0;
defaultPair.name = "default";
defaultPair.for = "all";
addCommandList[0] = defaultPair;
renamingRule.length = 0;
} }
defaultproperties { defaultproperties {
configName = "AcediaCommands" configName = "AcediaSystem"
useChatInput = true
useMutateInput = true
chatCommandPrefix = "!"
} }

818
sources/BaseAPI/API/Commands/Commands_Feature.uc

@ -19,9 +19,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class Commands_Feature extends Feature class Commands_Feature extends Feature;
dependson(CommandAPI)
dependson(Commands);
//! This feature manages commands that automatically parse their arguments into standard Acedia //! This feature manages commands that automatically parse their arguments into standard Acedia
//! collections. //! collections.
@ -40,81 +38,83 @@ class Commands_Feature extends Feature
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that //! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input. //! enforces connecting to the "mutate" input.
/// Auxiliary struct for passing name of the command to call with pre-specified /// 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. /// sub-command name.
/// ///
/// Normally sub-command name is parsed by the command itself, however command /// Normally sub-command name is parsed by the command itself, however command aliases can try to
/// aliases can try to enforce one. /// enforce one.
struct CommandCallPair { struct CommandCallPair {
var MutableText commandName; var MutableText commandName;
/// Not `none` in case it is enforced by an alias /// In case it is enforced by an alias
var MutableText subCommandName; var MutableText subCommandName;
}; };
/// Auxiliary struct that stores all the information needed to load /// Describes possible outcomes of starting a voting by its name
/// a certain command enum StartVotingResult {
struct EntityLoadInfo { /// Voting was successfully started
/// Command class to load. SVR_Success,
var public class<AcediaObject> entityClass; /// Voting wasn't started because another one was still in progress
/// Name to load that command class under. SVR_AlreadyInProgress,
var public Text name; /// Voting wasn't started because voting with that name hasn't been registered
/// Groups that are authorized to use that command. SVR_UnknownVoting
var public array<Text> authorizedGroups;
/// Groups that are authorized to use that command.
var public array<Text> groupsConfig;
};
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandListGroupPair {
/// Name of the command set to add
var public Text commandListName;
/// Name of the group, for which to add this set
var public Text permissionGroup;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> class;
/// Name to use for that class
var public Text newName;
}; };
/// Tools that provide functionality of managing registered commands and votings
var private CommandAPI.CommandFeatureTools tools;
/// Delimiters that always separate command name from it's parameters /// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters; var private array<Text> commandDelimiters;
/// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
/// When this flag is set to true, mutate input becomes available despite /// Keys should be deallocated when their entry is removed.
/// [`useMutateInput`] flag to allow to unlock server in case of an error 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; 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; 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; 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 private /*config*/ Text chatCommandPrefix;
var public /*config*/ array<string> commandGroup;
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet; var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable;
var public /*config*/ array<Commands.RenamingRulePair> renamingRule; var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved;
var public /*config*/ array<Commands.RenamingRulePair> votingRenamingRule;
// Converted version of `commandGroup`
var private array<Text> permissionGroupOrder;
/// Converted version of `addCommandSet`
var private array<CommandListGroupPair> usedCommandLists;
/// Converted version of `renamingRule` and `votingRenamingRule`
var private array<RenamingRulePair> commandRenamingRules;
var private array<RenamingRulePair> votingRenamingRules;
// Name, under which `ACommandHelp` is registered
var private Text helpCommandName;
var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList;
var LoggerAPI.Definition infoCommandAdded, infoVotingAdded;
protected function OnEnabled() { protected function OnEnabled() {
helpCommandName = P("help"); registeredCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
RegisterCommand(class'ACommandNotify');
RegisterCommand(class'ACommandVote');
RegisterCommand(class'ACommandSideEffects');
if (_.environment.IsDebugging()) {
RegisterCommand(class'ACommandFakers');
}
RegisterVotingClass(class'Voting');
// Macro selector // Macro selector
commandDelimiters[0] = _.text.FromString("@"); commandDelimiters[0] = _.text.FromString("@");
// Key selector // Key selector
@ -137,16 +137,6 @@ protected function OnEnabled() {
_.logger.Auto(errServerAPIUnavailable); _.logger.Auto(errServerAPIUnavailable);
} }
} }
LoadConfigArrays();
// `SetPermissionGroupOrder()` must be called *after* loading configs
tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool'));
tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool'));
tools.commands.SetPermissionGroupOrder(permissionGroupOrder);
tools.votings.SetPermissionGroupOrder(permissionGroupOrder);
_.commands._reloadFeature();
// Uses `CommandAPI`, so must be done after `_reloadFeature()` call
LoadCommands();
LoadVotings();
} }
protected function OnDisabled() { protected function OnDisabled() {
@ -156,27 +146,18 @@ protected function OnDisabled() {
if (useMutateInput && __server() != none) { if (useMutateInput && __server() != none) {
__server().unreal.mutator.OnMutate(self).Disconnect(); __server().unreal.mutator.OnMutate(self).Disconnect();
} }
useChatInput = false; useChatInput = false;
useMutateInput = false; useMutateInput = false;
_.memory.Free3(tools.commands, tools.votings, chatCommandPrefix); _.memory.Free3(registeredCommands, groupedCommands, chatCommandPrefix);
tools.commands = none; registeredCommands = none;
tools.votings = none; groupedCommands = none;
chatCommandPrefix = none; chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
commandDelimiters.length = 0; commandDelimiters.length = 0;
ReleaseNameVotingsArray(/*out*/ registeredVotings);
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
FreeUsedCommandSets();
FreeRenamingRules();
_.commands._reloadFeature();
} }
protected function SwapConfig(FeatureConfig config) { protected function SwapConfig(FeatureConfig config)
{
local Commands newConfig; local Commands newConfig;
newConfig = Commands(config); newConfig = Commands(config);
@ -187,14 +168,10 @@ protected function SwapConfig(FeatureConfig config) {
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
useChatInput = newConfig.useChatInput; useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput; useMutateInput = newConfig.useMutateInput;
commandGroup = newConfig.commandGroup;
addCommandSet = newConfig.addCommandList;
renamingRule = newConfig.renamingRule;
votingRenamingRule = newConfig.votingRenamingRule;
} }
/// This method allows to forcefully enable `Command_Feature` along with /// This method allows to forcefully enable `Command_Feature` along with "mutate" input in case
/// "mutate" input in case something goes wrong. /// something goes wrong.
/// ///
/// `Command_Feature` is a critical command to have running on your server and, /// `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 /// if disabled by accident, there will be no way of starting it again without
@ -211,7 +188,7 @@ public final static function EmergencyEnable() {
} }
feature = Commands_Feature(GetEnabledInstance()); feature = Commands_Feature(GetEnabledInstance());
noWayToInputCommands = !feature.emergencyEnabledMutate noWayToInputCommands = !feature.emergencyEnabledMutate
&& !feature.IsUsingMutateInput() &&!feature.IsUsingMutateInput()
&& !feature.IsUsingChatInput(); && !feature.IsUsingChatInput();
if (noWayToInputCommands) { if (noWayToInputCommands) {
default.emergencyEnabledMutate = true; default.emergencyEnabledMutate = true;
@ -252,8 +229,7 @@ public final static function bool IsUsingMutateInput() {
return false; return false;
} }
/// Returns prefix that will indicate that chat message is intended to be /// Returns prefix that will indicate that chat message is intended to be a command. By default "!".
/// a command. By default "!".
/// ///
/// If `Commands_Feature` is disabled, always returns `none`. /// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetChatPrefix() { public final static function Text GetChatPrefix() {
@ -266,34 +242,309 @@ public final static function Text GetChatPrefix() {
return none; return none;
} }
/// Returns name, under which [`ACommandHelp`] is registered. /// 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.
/// ///
/// If `Commands_Feature` is disabled, always returns `none`. /// `none` iff no voting is currently active.
public final static function Text GetHelpCommandName() { public final function Voting GetCurrentVoting() {
local Commands_Feature instance; if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
if (currentVoting != none) {
currentVoting.NewRef();
}
return currentVoting;
}
instance = Commands_Feature(GetEnabledInstance()); /// `true` if voting under the given name (case-insensitive) is already registered.
if (instance != none && instance.helpCommandName != none) { public final function bool IsVotingRegistered(BaseText processName) {
return instance.helpCommandName.Copy(); local int i;
for (i = 0; i < registeredVotings.length; i += 1) {
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) {
return true;
}
} }
return none; 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. /// Executes command based on the input.
/// ///
/// Takes [`commandLine`] as input with command's call, finds appropriate /// Takes [`commandLine`] as input with command's call, finds appropriate registered command
/// registered command instance and executes it with parameters specified in /// instance and executes it with parameters specified in the [`commandLine`].
/// the [`commandLine`].
/// ///
/// [`callerPlayer`] has to be specified and represents instigator of this /// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
/// command that will receive appropriate result/error messages. /// appropriate result/error messages.
/// ///
/// Returns `true` iff command was successfully executed. /// Returns `true` iff command was successfully executed.
/// ///
/// # Errors /// # Errors
/// ///
/// Doesn't log any errors, but can complain about errors in name or parameters /// Doesn't log any errors, but can complain about errors in name or parameters to
/// to the [`callerPlayer`] /// the [`callerPlayer`]
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
local bool result; local bool result;
local Parser wrapper; local Parser wrapper;
@ -309,23 +560,21 @@ public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
/// Executes command based on the input. /// Executes command based on the input.
/// ///
/// Takes [`commandLine`] as input with command's call, finds appropriate /// Takes [`commandLine`] as input with command's call, finds appropriate registered command
/// registered command instance and executes it with parameters specified in /// instance and executes it with parameters specified in the [`commandLine`].
/// the [`commandLine`].
/// ///
/// [`callerPlayer`] has to be specified and represents instigator of this /// [`callerPlayer`] has to be specified and represents instigator of this command that will receive
/// command that will receive appropriate result/error messages. /// appropriate result/error messages.
/// ///
/// Returns `true` iff command was successfully executed. /// Returns `true` iff command was successfully executed.
/// ///
/// # Errors /// # Errors
/// ///
/// Doesn't log any errors, but can complain about errors in name or parameters /// Doesn't log any errors, but can complain about errors in name or parameters to
/// to the [`callerPlayer`] /// the [`callerPlayer`]
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local bool errorOccured; local bool errorOccured;
local User identity; local Command commandInstance;
local CommandAPI.CommandConfigInfo commandPair;
local Command.CallData callData; local Command.CallData callData;
local CommandCallPair callPair; local CommandCallPair callPair;
@ -333,25 +582,34 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer)
if (callerPlayer == none) return false; if (callerPlayer == none) return false;
if (!parser.Ok()) return false; if (!parser.Ok()) return false;
identity = callerPlayer.GetIdentity();
callPair = ParseCommandCallPairWith(parser); callPair = ParseCommandCallPairWith(parser);
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity); commandInstance = GetCommand(callPair.commandName);
if (commandPair.instance == none || commandPair.usageForbidden) { if (commandInstance == none && callerPlayer != none && callerPlayer.IsExistent()) {
if (callerPlayer != none && callerPlayer.IsExistent()) { callerPlayer
callerPlayer .BorrowConsole()
.BorrowConsole() .Flush()
.Flush() .Say(F("{$TextFailure Command not found!}"));
.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);
} }
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) { return none;
callData =
commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config);
commandPair.instance.DeallocateCallData(callData);
}
_.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity);
return errorOccured;
} }
// Parses command's name into `CommandCallPair` - sub-command is filled in case // Parses command's name into `CommandCallPair` - sub-command is filled in case
@ -419,290 +677,44 @@ private function HandleMutate(string command, PlayerController sendingPlayer) {
parser.FreeSelf(); parser.FreeSelf();
} }
private final function LoadConfigArrays() { private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup) {
local int i; local int i;
local CommandListGroupPair nextCommandSetGroupPair; local ArrayList groupArray;
local Command nextCommand;
for (i = 0; i < commandGroup.length; i += 1) {
permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
}
for (i = 0; i < addCommandSet.length; i += 1) {
nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name);
nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for);
usedCommandLists[i] = nextCommandSetGroupPair;
}
FreeRenamingRules();
commandRenamingRules = LoadRenamingRules(renamingRule);
votingRenamingRules = LoadRenamingRules(votingRenamingRule);
}
private final function array<RenamingRulePair> LoadRenamingRules(
array<Commands.RenamingRulePair> inputRules) {
local int i, j;
local RenamingRulePair nextRule;
local array<RenamingRulePair> result;
// Clear away duplicates
for (i = 0; i < inputRules.length; i += 1) {
j = i + 1;
while (j < inputRules.length) {
if (inputRules[i].rename == inputRules[j].rename) {
_.logger.Auto(warnDuplicateRenaming)
.ArgClass(inputRules[i].rename)
.Arg(_.text.FromString(inputRules[i].to))
.Arg(_.text.FromString(inputRules[j].to));
inputRules.Remove(j, 1);
} else {
j += 1;
}
}
}
// Translate rules
for (i = 0; i < inputRules.length; i += 1) {
nextRule.class = inputRules[i].rename;
nextRule.newName = _.text.FromString(inputRules[i].to);
if (nextRule.class == class'ACommandHelp') {
_.memory.Free(helpCommandName);
helpCommandName = nextRule.newName.Copy();
}
result[result.length] = nextRule;
}
return result;
}
private final function LoadCommands() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> commandClassesToLoad;
commandClassesToLoad = CollectAllCommandClasses();
// Load command names to use, according to preferred names and name rules
for (i = 0; i < commandClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < commandRenamingRules.length; j += 1) {
if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) {
nextName = commandRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Command>(commandClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
commandClassesToLoad[i].name = nextName;
}
// Actually load commands
for (i = 0; i < commandClassesToLoad.length; i += 1) {
_.commands.AddCommandAsync(
class<Command>(commandClassesToLoad[i].entityClass),
commandClassesToLoad[i].name);
for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeCommandUsageAsync(
commandClassesToLoad[i].name,
commandClassesToLoad[i].authorizedGroups[j],
commandClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoCommandAdded)
.ArgClass(commandClassesToLoad[i].entityClass)
.Arg(/*take*/ commandClassesToLoad[i].name);
}
for (i = 0; i < commandClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(commandClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(commandClassesToLoad[i].groupsConfig);
}
}
private final function LoadVotings() { groupArray = groupedCommands.GetArrayList(commandGroup);
local int i, j; if (groupArray == none) {
local Text nextName; return;
local array<EntityLoadInfo> votingClassesToLoad;
votingClassesToLoad = CollectAllVotingClasses();
// Load voting names to use, according to preferred names and name rules
for (i = 0; i < votingClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < votingRenamingRules.length; j += 1) {
if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) {
nextName = votingRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Voting>(votingClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
votingClassesToLoad[i].name = nextName;
}
// Actually load votings
for (i = 0; i < votingClassesToLoad.length; i += 1) {
_.commands.AddVotingAsync(
class<Voting>(votingClassesToLoad[i].entityClass),
votingClassesToLoad[i].name);
for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeVotingUsageAsync(
votingClassesToLoad[i].name,
votingClassesToLoad[i].authorizedGroups[j],
votingClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoVotingAdded)
.ArgClass(votingClassesToLoad[i].entityClass)
.Arg(/*take*/ votingClassesToLoad[i].name);
}
for (i = 0; i < votingClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(votingClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(votingClassesToLoad[i].groupsConfig);
}
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllCommandClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetCommandData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
} }
return result; while (i < groupArray.GetLength()) {
} nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none && nextCommand.class == commandClass) {
// Guaranteed to not return `none` items in the array groupArray.RemoveIndex(i);
private final function array<EntityLoadInfo> CollectAllVotingClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetVotingData(),
usedCommandLists[i].permissionGroup);
} else { } else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy()); i += 1;
} }
_.memory.Free(nextCommand);
} }
return result; if (groupArray.GetLength() == 0) {
} groupedCommands.RemoveItem(commandGroup);
// Adds `newCommands` into `infoArray`, adding `commandsGroup` to
// their array `authorizedGroups`
//
// Assumes that all arguments aren't `none`.
//
// Guaranteed to not add `none` commands from `newCommands`.
//
// Assumes that items from `infoArray` all have `name` field set to `none`,
// will also leave them as `none`.
private final function MergeEntityClassArrays(
out array<EntityLoadInfo> infoArray,
/*take*/ array<CommandList.EntityConfigPair> newCommands,
Text commandsGroup
) {
local int i, infoToEditIndex;
local EntityLoadInfo infoToEdit;
local array<Text> editedArray;
for (i = 0; i < newCommands.length; i += 1) {
if (newCommands[i].class == none) {
continue;
}
// Search for an existing record of class `newCommands[i]` in
// `infoArray`. If found, copy to `infoToEdit` and index into
// `infoToEditIndex`, else `infoToEditIndex` will hold the next unused
// index in `infoArray`.
infoToEditIndex = 0;
while (infoToEditIndex < infoArray.length) {
if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) {
infoToEdit = infoArray[infoToEditIndex];
break;
}
infoToEditIndex += 1;
}
// Update data inside `infoToEdit`.
infoToEdit.entityClass = newCommands[i].class;
editedArray = infoToEdit.authorizedGroups;
editedArray[editedArray.length] = commandsGroup.Copy();
infoToEdit.authorizedGroups = editedArray;
editedArray = infoToEdit.groupsConfig;
editedArray[editedArray.length] = newCommands[i].config; // moving here
infoToEdit.groupsConfig = editedArray;
// Update `infoArray` with the modified record.
infoArray[infoToEditIndex] = infoToEdit;
// Forget about data `authorizedGroups` and `groupsConfig`:
//
// 1. Their references have already been moved into `infoArray` and
// don't need to be released;
// 2. If we don't find corresponding struct inside `infoArray` on
// the next iteration, we'll override `commandClass`,
// but not `authorizedGroups`/`groupsConfig`, so we'll just reset them
// to empty now.
// (`name` field is expected to be `none` during this method)
infoToEdit.authorizedGroups.length = 0;
infoToEdit.groupsConfig.length = 0;
} }
_.memory.Free(groupArray);
} }
private final function FreeUsedCommandSets() { private final function ReleaseNameVotingsArray(out array<NamedVoting> toRelease) {
local int i; local int i;
for (i = 0; i < usedCommandLists.length; i += 1) { for (i = 0; i < toRelease.length; i += 1) {
_.memory.Free(usedCommandLists[i].commandListName); _.memory.Free(toRelease[i].processName);
_.memory.Free(usedCommandLists[i].permissionGroup); toRelease[i].processName = none;
} }
usedCommandLists.length = 0; toRelease.length = 0;
}
private final function FreeRenamingRules() {
local int i;
for (i = 0; i < commandRenamingRules.length; i += 1) {
_.memory.Free(commandRenamingRules[i].newName);
}
commandRenamingRules.length = 0;
for (i = 0; i < votingRenamingRules.length; i += 1) {
_.memory.Free(votingRenamingRules[i].newName);
}
votingRenamingRules.length = 0;
}
public final function CommandAPI.CommandFeatureTools _borrowTools() {
return tools;
} }
defaultproperties { defaultproperties {
configClass = class'Commands' 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.") errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.") 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.")
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.") 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.")
infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".")
infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".")
} }

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Signal.uc

@ -1,39 +0,0 @@
/**
* 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 CommandsAPI_OnCommandAdded_Signal extends Signal;
public final function bool Emit(class<Command> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandAdded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandAdded_Slot.uc

@ -1,38 +0,0 @@
/**
* 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 CommandsAPI_OnCommandAdded_Slot extends Slot;
delegate connect(class<Command> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Signal.uc

@ -1,39 +0,0 @@
/**
* 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 CommandsAPI_OnCommandRemoved_Signal extends Signal;
public final function bool Emit(class<Command> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandRemoved_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnCommandRemoved_Slot.uc

@ -1,38 +0,0 @@
/**
* 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 CommandsAPI_OnCommandRemoved_Slot extends Slot;
delegate connect(class<Command> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Signal.uc

@ -1,39 +0,0 @@
/**
* 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 CommandsAPI_OnVotingAdded_Signal extends Signal;
public final function bool Emit(class<Voting> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingAdded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingAdded_Slot.uc

@ -1,38 +0,0 @@
/**
* 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 CommandsAPI_OnVotingAdded_Slot extends Slot;
delegate connect(class<Voting> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Signal.uc

@ -1,39 +0,0 @@
/**
* 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 CommandsAPI_OnVotingEnded_Signal extends Signal;
public final function bool Emit(bool success, HashTable arguments) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingEnded_Slot(nextSlot).connect(success, arguments);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingEnded_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingEnded_Slot.uc

@ -1,38 +0,0 @@
/**
* 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 CommandsAPI_OnVotingEnded_Slot extends Slot;
delegate connect(bool success, HashTable arguments) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

39
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Signal.uc

@ -1,39 +0,0 @@
/**
* 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 CommandsAPI_OnVotingRemoved_Signal extends Signal;
public final function bool Emit(class<Voting> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingRemoved_Slot'
}

38
sources/BaseAPI/API/Commands/Events/CommandsAPI_OnVotingRemoved_Slot.uc

@ -1,38 +0,0 @@
/**
* 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 CommandsAPI_OnVotingRemoved_Slot extends Slot;
delegate connect(class<Voting> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

251
sources/BaseAPI/API/Commands/PlayersParser.uc

@ -27,75 +27,71 @@ class PlayersParser extends AcediaObject
//! //!
//! Basic use is to specify one of the selectors: //! Basic use is to specify one of the selectors:
//! 1. Key selector: "#<integer>" (examples: "#1", "#5"). //! 1. Key selector: "#<integer>" (examples: "#1", "#5").
//! This one is used to specify players by their key, assigned to them when //! This one is used to specify players by their key, assigned to them when they enter the game.
//! they enter the game.
//! This type of selectors can be used when players have hard to type names. //! This type of selectors can be used when players have hard to type names.
//! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". //! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@".
//! "@", "@me", and "@self" are identical and can be used to specify player //! "@", "@me", and "@self" are identical and can be used to specify player that called
//! that called the command. //! the command.
//! "@admin" can be used to specify all admins in the game at once. //! "@admin" can be used to specify all admins in the game at once.
//! "@all" specifies all current players. //! "@all" specifies all current players.
//! In future it is planned to make macros extendable by allowing to bind //! In future it is planned to make macros extendable by allowing to bind more names to specific
//! more names to specific groups of players. //! groups of players.
//! 3. Name selectors: quoted strings and any other types of string that do not //! 3. Name selectors: quoted strings and any other types of string that do not start with
//! start with either "#" or "@". //! either "#" or "@".
//! These specify name prefixes: any player with specified prefix will be considered to match //! These specify name prefixes: any player with specified prefix will be considered to match
//! such selector. //! such selector.
//! //!
//! Negated selectors: "!<selector>". Specifying "!" in front of selector will //! Negated selectors: "!<selector>". Specifying "!" in front of selector will select all players
//! select all players that do not match it instead. //! that do not match it instead.
//! //!
//! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". //! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']".
//! Specified selectors are process in order: from left to right. //! Specified selectors are process in order: from left to right.
//! First selector works as usual and selects a set of players. //! First selector works as usual and selects a set of players.
//! All the following selectors either expand that list (additive ones, without //! All the following selectors either expand that list (additive ones, without "!" prefix) or
//! "!" prefix) or remove specific players from the list (the ones with "!" //! remove specific players from the list (the ones with "!" prefix).
//! prefix). Examples of that: //! Examples of that:
//! //!
//! * "[@admin, !@self]" - selects all admins, except the one who called //! * "[@admin, !@self]" - selects all admins, except the one who called the command
//! the command (whether he is admin or not). //! (whether he is admin or not).
//! * "[dkanus, 'mate']" - will select players "dkanus" and "mate". //! * "[dkanus, 'mate']" - will select players "dkanus" and "mate". Order also matters, since:
//! Order also matters, since: //! * "[@admin, !@admin]" - won't select anyone, since it will first add all the admins and
//! - "[@admin, !@admin]" - won't select anyone, since it will first add all //! then remove them.
//! the admins and then remove them. //! * "[!@admin, @admin]" - will select everyone, since it will first select everyone who is
//! - "[!@admin, @admin]" - will select everyone, since it will first select //! not an admin and then adds everyone else.
//! everyone who is not an admin and then adds everyone else.
//! //!
//! # Usage //! # Usage
//! //!
//! 1. Allocate `PlayerParser`; //! 1. Allocate `PlayerParser`;
//! 2. Set caller player through `SetSelf()` method to make "@" and "@me" //! 2. Set caller player through `SetSelf()` method to make "@" and "@me" selectors usable;
//! selectors usable; //! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that starts with proper
//! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that //! players selector;
//! starts with proper players selector;
//! 4. Call `GetPlayers()` to obtain selected players array. //! 4. Call `GetPlayers()` to obtain selected players array.
//! //!
//! # Implementation //! # Implementation
//! //!
//! When created, `PlayersParser` takes a snapshot (array) of current players on //! When created, `PlayersParser` takes a snapshot (array) of current players on the server.
//! the server. Then `currentSelection` is decided based on whether first //! Then `currentSelection` is decided based on whether first selector is positive
//! selector is positive (initial selection is taken as empty array) or negative //! (initial selection is taken as empty array) or negative
//! (initial selection is taken as full snapshot). //! (initial selection is taken as full snapshot).
//! //!
//! After that `PlayersParser` simply goes through specified selectors //! After that `PlayersParser` simply goes through specified selectors
//! (in case more than one is specified) and adds or removes appropriate players //! (in case more than one is specified) and adds or removes appropriate players in
//! in `currentSelection`, assuming that `playersSnapshot` is a current full //! `currentSelection`, assuming that `playersSnapshot` is a current full array of players.
//! array of players.
// Player for which "@", "@me", and "@self" macros will refer
/// Player for which "@", "@me", and "@self" macros will refer var private EPlayer selfPlayer;
var private EPlayer selfPlayer; // Copy of the list of current players at the moment of allocation of
/// Copy of the list of current players at the moment of allocation of // this `PlayersParser`.
/// this `PlayersParser`. var private array<EPlayer> playersSnapshot;
var private array<EPlayer> playersSnapshot; // Players, selected according to selectors we have parsed so far
/// Players, selected according to selectors we have parsed so far var private array<EPlayer> currentSelection;
var private array<EPlayer> currentSelection; // Have we parsed our first selector?
/// Have we parsed our first selector? // We need this to know whether to start with the list of
/// We need this to know whether to start with the list of // all players (if first selector removes them) or
/// all players (if first selector removes them) or // with empty list (if first selector adds them).
/// with empty list (if first selector adds them). var private bool parsedFirstSelector;
var private bool parsedFirstSelector; // Will be equal to a single-element array [","], used for parsing
/// Will be equal to a single-element array [","], used for parsing var private array<Text> selectorDelimiters;
var private array<Text> selectorDelimiters;
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA;
var const int TOPEN_BRACKET, TCLOSE_BRACKET; var const int TOPEN_BRACKET, TCLOSE_BRACKET;
@ -122,77 +118,8 @@ public final function SetSelf(EPlayer newSelfPlayer) {
} }
} }
/// Returns players parsed by the last `ParseWith()` or `Parse()` call. // Insert a new player into currently selected list of players (`currentSelection`) such that there
/// // will be no duplicates.
/// 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;
}
/// 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. // `none` values are auto-discarded.
private final function InsertPlayer(EPlayer toInsert) { private final function InsertPlayer(EPlayer toInsert) {
@ -334,8 +261,7 @@ private final function RemoveByMacro(BaseText macroText) {
} }
} }
// Parses one selector from `parser`, while accordingly modifying current player // Parses one selector from `parser`, while accordingly modifying current player selection list.
// selection list.
private final function ParseSelector(Parser parser) { private final function ParseSelector(Parser parser) {
local bool additiveSelector; local bool additiveSelector;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -373,8 +299,8 @@ private final function ParseSelector(Parser parser) {
ParseNameSelector(parser, additiveSelector); ParseNameSelector(parser, additiveSelector);
} }
// Parse key selector (assuming "#" is already consumed), while accordingly // Parse key selector (assuming "#" is already consumed), while accordingly modifying current player
// modifying current player selection list. // selection list.
private final function ParseKeySelector(Parser parser, bool additiveSelector) { private final function ParseKeySelector(Parser parser, bool additiveSelector) {
local int key; local int key;
@ -389,8 +315,8 @@ private final function ParseKeySelector(Parser parser, bool additiveSelector) {
} }
} }
// Parse macro selector (assuming "@" is already consumed), while accordingly // Parse macro selector (assuming "@" is already consumed), while accordingly modifying current
// modifying current player selection list. // player selection list.
private final function ParseMacroSelector(Parser parser, bool additiveSelector) { private final function ParseMacroSelector(Parser parser, bool additiveSelector) {
local MutableText macroName; local MutableText macroName;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -413,8 +339,7 @@ private final function ParseMacroSelector(Parser parser, bool additiveSelector)
_.memory.Free(macroName); _.memory.Free(macroName);
} }
// Parse name selector, while accordingly modifying current player selection // Parse name selector, while accordingly modifying current player selection list.
// list.
private final function ParseNameSelector(Parser parser, bool additiveSelector) { private final function ParseNameSelector(Parser parser, bool additiveSelector) {
local MutableText playerName; local MutableText playerName;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -437,11 +362,10 @@ private final function ParseNameSelector(Parser parser, bool additiveSelector) {
_.memory.Free(playerName); _.memory.Free(playerName);
} }
// Reads a string that can either be a body of name selector (some player's // Reads a string that can either be a body of name selector (some player's name prefix) or
// name prefix) or of a macro selector (what comes after "@"). // of a macro selector (what comes after "@").
// //
// This is different from `parser.MString()` because it also uses "," as // This is different from `parser.MString()` because it also uses "," as a separator.
// a separator.
private final function MutableText ParseLiteral(Parser parser) { private final function MutableText ParseLiteral(Parser parser) {
local MutableText literal; local MutableText literal;
local Parser.ParserState confirmedState; local Parser.ParserState confirmedState;
@ -457,6 +381,58 @@ private final function MutableText ParseLiteral(Parser parser) {
return literal; 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 // Resets this object to initial state before parsing and update
// `playersSnapshot` to contain current players. // `playersSnapshot` to contain current players.
private final function Reset() { private final function Reset() {
@ -470,6 +446,23 @@ private final function Reset() {
selectorDelimiters[1] = T(TCLOSE_BRACKET); 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 { defaultproperties {
TSELF = 0 TSELF = 0
stringConstants(0) = "self" stringConstants(0) = "self"

351
sources/BaseAPI/API/Commands/Tests/TEST_Voting.uc

@ -1,351 +0,0 @@
/**
* 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,
};
protected static function VotingModel MakeVotingModel(bool drawMeansWin) {
local VotingModel model;
model = VotingModel(__().memory.Allocate(class'VotingModel'));
model.Start(drawMeansWin);
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);
}
}
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);
}
}
protected static function TESTS() {
Test_VotingModel();
}
protected static function Test_VotingModel() {
SubTest_YesVoting();
SubTest_NoVoting();
SubTest_FaultyVoting();
SubTest_DisconnectVoting_DrawMeansWin();
SubTest_DisconnectVoting_DrawMeansLoss();
SubTest_ReconnectVoting_DrawMeansWin();
SubTest_ReconnectVoting_DrawMeansLoss();
}
protected static function SubTest_YesVoting() {
local VotingModel model;
Context("Testing \"yes\" voting.");
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "2", TEST_EO_Continue);
VoteYes(model, "4", TEST_EO_Continue);
VoteNo(model, "6", TEST_EO_Continue);
VoteNo(model, "1", TEST_EO_Continue);
VoteYes(model, "6", TEST_EO_End);
model = MakeVotingModel(false);
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_NoVoting() {
local VotingModel model;
Context("Testing \"no\" voting.");
model = MakeVotingModel(true);
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);
model = MakeVotingModel(false);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteNo(model, "1", TEST_EO_Continue);
VoteNo(model, "2", 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_FaultyVoting() {
local VotingModel model;
Context("Testing \"faulty\" voting.");
model = MakeVotingModel(false);
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); // voting already ended, so no more votes here
model = MakeVotingModel(true);
SetVoters(model, "1", "2", "3", "4", "5", "6");
VoteYes(model, "3", TEST_EO_Continue);
VoteYes(model, "5", 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); // voting already ended, so no more votes here
}
protected static function SubTest_DisconnectVoting_DrawMeansWin() {
local VotingModel model;
Context("Testing \"disconnect\" voting when draw means victory.");
model = MakeVotingModel(true);
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);
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);
SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 2 "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_DisconnectVoting_DrawMeansLoss() {
local VotingModel model;
Context("Testing \"disconnect\" voting when draw means loss.");
model = MakeVotingModel(false);
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_ReconnectVoting_DrawMeansWin() {
local VotingModel model;
Context("Testing \"reconnect\" voting when draw means victory.");
model = MakeVotingModel(true);
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 SubTest_ReconnectVoting_DrawMeansLoss() {
local VotingModel model;
Context("Testing \"reconnect\" voting when draw means loss.");
model = MakeVotingModel(false);
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);
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, "8", TEST_EO_Continue);
VoteNo(model, "3", TEST_EO_End);
}
defaultproperties {
caseGroup = "Commands"
caseName = "Voting model"
}

306
sources/BaseAPI/API/Commands/Tools/CmdItemsTool.uc

@ -1,306 +0,0 @@
/**
* 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 CmdItemsTool extends AcediaObject
dependson(CommandAPI)
abstract;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances and [`Voting`] classes: they both have in common
//! the need to remember who was authorized to use them (i.e. which user group)
//! and with what permissions (i.e. name of the config that contains appropriate
//! permissions).
//!
//! Aside from trivial accessors to its data, it also provides a way to resolve
//! the best permissions available to the user by finding the most priviledged
//! group he belongs to.
//!
//! NOTE: child classes must implement `MakeCard()` method and can override
//! `DiscardCard()` method to catch events of removing items from storage.
/// Allows to specify a base class requirement for this tool - only classes
/// that were derived from it can be stored inside.
var protected const class<AcediaObject> ruleBaseClass;
/// Names of user groups that can decide permissions for items,
/// in order of importance: from most significant to the least significant.
/// This is used for resolving the best permissions for each user.
var private array<Text> permissionGroupOrder;
/// Maps item names to their [`ItemCards`] with information about which groups
/// are authorized to use this particular item.
var private HashTable registeredCards;
var LoggerAPI.Definition errItemInvalidName;
var LoggerAPI.Definition errItemDuplicate;
protected function Constructor() {
registeredCards = _.collections.EmptyHashTable();
}
protected function Finalizer() {
_.memory.Free(registeredCards);
_.memory.FreeMany(permissionGroupOrder);
registeredCards = none;
permissionGroupOrder.length = 0;
}
/// Registers given item class under the specified (case-insensitive) name.
///
/// If name parameter is omitted (specified as `none`) or is an invalid name
/// (according to [`BaseText::IsValidName()`] method), then item class will not
/// be registered.
///
/// Returns `true` if item was successfully registered and `false` otherwise`.
///
/// # Errors
///
/// If provided name that is invalid or already taken by a different item -
/// a warning will be logged and item class won't be registered.
public function bool AddItemClass(class<AcediaObject> itemClass, BaseText itemName) {
local Text itemKey;
local ItemCard newCard, existingCard;
if (itemClass == none) return false;
if (itemName == none) return false;
if (registeredCards == none) return false;
if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
return false;
}
// The item name is transformed into lowercase, immutable value.
// This facilitates the use of item names as keys in a [`HashTable`],
// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
if (itemKey == none || !itemKey.IsValidName()) {
_.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
return false;
}
// Guaranteed to only store cards
existingCard = ItemCard(registeredCards.GetItem(itemName));
if (existingCard != none) {
_.logger.Auto(errItemDuplicate)
.ArgClass(existingCard.GetItemClass())
.Arg(itemKey)
.ArgClass(itemClass);
_.memory.Free(existingCard);
return false;
}
newCard = MakeCard(itemClass, itemName);
registeredCards.SetItem(itemKey, newCard);
_.memory.Free2(itemKey, newCard);
return true;
}
/// Removes item of given class from the list of registered items.
///
/// Removing once registered item is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of items).
///
/// Returns `true` if successfully removed registered item class and
/// `false` otherwise (either item wasn't registered or caller tool
/// initialized).
public function bool RemoveItemClass(class<AcediaObject> itemClass) {
local int i;
local CollectionIterator iter;
local ItemCard nextCard;
local array<Text> keysToRemove;
if (itemClass == none) return false;
if (registeredCards == none) return false;
// Removing items during iterator breaks an iterator, so first we find
// all the keys to remove
iter = registeredCards.Iterate();
iter.LeaveOnlyNotNone();
while (!iter.HasFinished()) {
// Guaranteed to only be `ItemCard`
nextCard = ItemCard(iter.Get());
if (nextCard.GetItemClass() == itemClass) {
keysToRemove[keysToRemove.length] = Text(iter.GetKey());
DiscardCard(nextCard);
}
_.memory.Free(nextCard);
iter.Next();
}
iter.FreeSelf();
// Actual clean up everything in `keysToRemove`
for (i = 0; i < keysToRemove.length; i += 1) {
registeredCards.RemoveItem(keysToRemove[i]);
}
_.memory.FreeMany(keysToRemove);
return (keysToRemove.length > 0);
}
/// Allows to specify the order of the user group in terms of privilege for
/// accessing stored items. Only specified groups will be used when resolving
/// appropriate permissions config name for a user.
public final function SetPermissionGroupOrder(array<Text> groupOrder) {
local int i;
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
for (i = 0; i < groupOrder.length; i += 1) {
if (groupOrder[i] != none) {
permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
}
}
}
/// Specifies what permissions (given by the config name) given user group has
/// when using an item with a specified name.
///
/// Method must be called after item with a given name is added.
///
/// If this config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, only an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// registered.
///
/// Method will return `true` if group was successfully authorized and `false`
/// otherwise (either group already authorized or no item with specified name
/// was added in the caller tool so far).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log a warning message about it.
public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
local bool result;
local ItemCard relevantCard;
if (configName != none && !configName.IsValidName()) {
return false;
}
relevantCard = GetCard(itemName);
if (relevantCard != none) {
result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
_.memory.Free(relevantCard);
}
return result;
}
/// Returns struct with item class (+ instance, if one was stored) for a given
/// case in-sensitive item name and name of the config with best permissions
/// available to the player with provided ID.
///
/// Function only returns `none` for item class if item with a given name
/// wasn't found.
/// Config name being `none` with non-`none` item class in the result means
/// that user with provided ID doesn't have permissions for using the item at
/// all.
public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
local int i;
local ItemCard relevantCard;
local CommandAPI.ItemConfigInfo result;
relevantCard = GetCard(itemName);
if (relevantCard == none) {
// At this point contains `none` for all values -> indicates a failure
// to find item in storage
return result;
}
result.instance = relevantCard.GetItem();
result.class = relevantCard.GetItemClass();
if (textID == none) {
return result;
}
// Look through all `permissionGroupOrder` in order to find most priviledged
// group that user with `textID` belongs to
for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
}
}
_.memory.Free(relevantCard);
return result;
}
/// Returns all item classes that are stored inside caller tool.
///
/// Doesn't check for duplicates (although with a normal usage, there shouldn't
/// be any).
public final function array< class<AcediaObject> > GetAllItemClasses() {
local array< class<AcediaObject> > result;
local ItemCard value;
local CollectionIterator iter;
for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
value = ItemCard(iter.Get());
if (value != none) {
result[result.length] = value.GetItemClass();
}
_.memory.Free(value);
}
iter.FreeSelf();
return result;
}
/// Returns array of names of all available items.
public final function array<Text> GetItemsNames() {
local array<Text> emptyResult;
if (registeredCards != none) {
return registeredCards.GetTextKeys();
}
return emptyResult;
}
/// Called each time a new card is to be created and stored.
///
/// Must be reimplemented by child classes.
protected function ItemCard MakeCard(class<AcediaObject> itemClass, BaseText itemName) {
return none;
}
/// Called each time a certain card is to be removed from storage.
///
/// Must be reimplemented by child classes
/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
protected function DiscardCard(ItemCard toDiscard) {
}
/// Find item card for the item that was stored with a specified
/// case-insensitive name
///
/// Function only returns `none` if item with a given name wasn't found
/// (or `none` was provided as an argument).
protected final function ItemCard GetCard(BaseText itemName) {
local Text itemKey;
local ItemCard relevantCard;
if (itemName == none) return none;
if (registeredCards == none) return none;
/// The item name is transformed into lowercase, immutable value.
/// This facilitates the use of item names as keys in a [`HashTable`],
/// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
relevantCard = ItemCard(registeredCards.GetItem(itemKey));
_.memory.Free(itemKey);
return relevantCard;
}
defaultproperties {
errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
}

142
sources/BaseAPI/API/Commands/Tools/CommandsTool.uc

@ -1,142 +0,0 @@
/**
* 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 CommandsTool extends CmdItemsTool;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances.
//!
//! This storage class allows for efficient manipulation and retrieval of
//! [`Command`]s, along with information about what use groups were authorized
//! to use them.
//!
//! Additionally, this tool allows for efficient fetching of commands that
//! belong to a particular *command group*.
/// [`HashTable`] that maps a command group name to a set of command names that
/// belong to it.
var private HashTable groupedCommands;
protected function Constructor() {
super.Constructor();
groupedCommands = _.collections.EmptyHashTable();
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(groupedCommands);
groupedCommands = none;
}
/// Returns all known command groups' names.
public final function array<Text> GetGroupsNames() {
local array<Text> emptyResult;
if (groupedCommands != none) {
return groupedCommands.GetTextKeys();
}
return emptyResult;
}
/// Returns array of names of all available commands belonging to the specified
/// group.
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
local int i;
local ArrayList commandNamesArray;
local array<Text> result;
if (groupedCommands == none) return result;
commandNamesArray = groupedCommands.GetArrayList(groupName);
if (commandNamesArray == none) return result;
for (i = 0; i < commandNamesArray.GetLength(); i += 1) {
result[result.length] = commandNamesArray.GetText(i);
}
_.memory.Free(commandNamesArray);
return result;
}
protected function ItemCard MakeCard(class<AcediaObject> commandClass, BaseText itemName) {
local Command newCommandInstance;
local ItemCard newCard;
local Text commandGroup;
if (class<Command>(commandClass) != none) {
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
newCommandInstance.Initialize(itemName);
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
newCard.InitializeWithInstance(newCommandInstance);
// Guaranteed to be lower case (keys of [`HashTable`])
if (itemName != none) {
itemName = itemName.LowerCopy();
} else {
itemName = newCommandInstance.GetPreferredName();
}
commandGroup = newCommandInstance.GetGroupName();
AssociateGroupAndName(commandGroup, itemName);
_.memory.Free3(newCommandInstance, itemName, commandGroup);
}
return newCard;
}
protected function DiscardCard(ItemCard toDiscard) {
local Text groupKey, commandName;
local Command storedCommand;
local ArrayList listOfCommands;
if (toDiscard == none) return;
// Guaranteed to store a [`Command`]
storedCommand = Command(toDiscard.GetItem());
if (storedCommand == none) return;
// Guaranteed to be stored in a lower case
commandName = storedCommand.GetName();
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands != none && commandName != none) {
listOfCommands.RemoveItem(commandName);
}
_.memory.Free2(commandName, storedCommand);
}
// Expect both arguments to be not `none`.
// Expect both arguments to be lower-case.
private final function AssociateGroupAndName(BaseText groupKey, BaseText commandName) {
local ArrayList listOfCommands;
if (groupedCommands != none) {
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands == none) {
listOfCommands = _.collections.EmptyArrayList();
}
if (listOfCommands.Find(commandName) < 0) {
// `< 0` means not found
listOfCommands.AddItem(commandName);
}
// Set `listOfCommands` in case we've just created that array.
// Won't do anything if it is already recorded there.
groupedCommands.SetItem(groupKey, listOfCommands);
}
}
defaultproperties {
ruleBaseClass = class'Command';
}

177
sources/BaseAPI/API/Commands/Tools/ItemCard.uc

@ -1,177 +0,0 @@
/**
* 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 ItemCard extends AcediaObject;
//! Utility class designed for storing either class of an object
//! (possibly also a specific instance) along with authorization information:
//! which user groups are allowed to use stored entity and with what level of
//! permissions (defined by the name of a config with permissions).
//!
//! [`ItemCard`] has to be initialized with either [`InitializeWithClass()`] or
//! [`InitializeWithInstance()`] before it can be used.
/// Class of object that this card describes.
var private class<AcediaObject> storedClass;
/// Instance of an object (can also *optionally* be stored in this card)
var private AcediaObject storedInstance;
/// This [`HashTable`] maps authorized groups to their respective config names.
///
/// Each key represents an authorized group, and its corresponding value
/// indicates the associated config name. If a key has a value of `none`,
/// the default config (named "default") should be used for that group.
var private HashTable groupToConfig;
var LoggerAPI.Definition errGroupAlreadyHasConfig;
protected function Finalizer() {
_.memory.Free2(storedInstance, groupToConfig);
storedInstance = none;
storedClass = none;
groupToConfig = none;
}
/// Initializes the caller [`ItemCard`] object with class to be stored.
///
/// Initialization can only be done once: once method returned `true`,
/// all future calls will fail.
///
/// Returns `false` if caller was already initialized or `none` is provided as
/// an argument. Otherwise succeeds and returns `true`.
public function bool InitializeWithClass(class<AcediaObject> toStore) {
if (storedClass != none) return false;
if (toStore == none) return false;
storedClass = toStore;
groupToConfig = _.collections.EmptyHashTable();
return true;
}
/// Initializes the caller [`ItemCard`] object with an object to be stored.
///
/// Initialization can only be done once: once method returned `true`,
/// all future calls will fail.
///
/// Returns `false` caller was already initialized or `none` is provided as
/// an argument. Otherwise succeeds and returns `true`.
public function bool InitializeWithInstance(AcediaObject toStore) {
if (storedClass != none) return false;
if (toStore == none) return false;
storedClass = toStore.class;
storedInstance = toStore;
storedInstance.NewRef();
groupToConfig = _.collections.EmptyHashTable();
return true;
}
/// Authorizes a new group to use the this card's item.
///
/// This function allows to specify the config name for a particular user group.
/// If this config name is skipped (specified as `none`), then "default" will be
/// used instead.
///
/// Function will return `true` if group was successfully authorized and
/// `false` otherwise (either group already authorized or caller [`ItemCard`]
/// isn't initialized).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log an error message about it.
public function bool AuthorizeGroupWithConfig(BaseText groupName, optional BaseText configName) {
local Text itemKey;
local Text storedConfigName;
if (storedClass == none) return false;
if (groupToConfig == none) return false;
if (groupName == none) return false;
if (groupName.IsEmpty()) return false;
/// Make group name immutable and have its characters have a uniform case to
/// be usable as case-insensitive keys for [`HashTable`].
itemKey = groupName.LowerCopy();
storedConfigName = groupToConfig.GetText(itemKey);
if (storedConfigName != none) {
_.logger.Auto(errGroupAlreadyHasConfig)
.ArgClass(storedClass)
.Arg(groupName.Copy())
.Arg(storedConfigName)
.Arg(configName.Copy());
_.memory.Free(itemKey);
return false;
}
// We don't actually record "default" value at this point, instead opting
// to return "default" in getter functions in case stored `configName`
// is `none`.
groupToConfig.SetItem(itemKey, configName);
_.memory.Free(itemKey);
return true;
}
/// Returns item instance for the caller [`ItemCard`].
///
/// Returns `none` iff this card wasn't initialized with an instance.
public function AcediaObject GetItem() {
if (storedInstance != none) {
storedInstance.NewRef();
}
return storedInstance;
}
/// Returns item class for the caller [`ItemCard`].
///
/// Returns `none` iff this card wasn't initialized.
public function class<AcediaObject> GetItemClass() {
return storedClass;
}
/// Returns the name of config that was authorized for the specified group.
///
/// Returns `none` if group wasn't authorized, otherwise guaranteed to
/// return non-`none` and non-empty `Text` value.
public function Text GetConfigNameForGroup(BaseText groupName) {
local Text groupNameAsKey, result;
if (storedClass == none) return none;
if (groupToConfig == none) return none;
if (groupName == none) return none;
/// Make group name immutable and have its characters a uniform case to
/// be usable as case-insensitive keys for [`HashTable`]
groupNameAsKey = groupName.LowerCopy();
if (groupToConfig.HasKey(groupNameAsKey)) {
result = groupToConfig.GetText(groupNameAsKey);
if (result == none) {
// If we do have specified group recorded as a key, then we must
// return non-`none` config name, defaulting to "default" value
// if none was provided
result = P("default").Copy();
}
}
_.memory.Free(groupNameAsKey);
return result;
}
defaultproperties {
errGroupAlreadyHasConfig = (l=LOG_Error,m="Item `%1` is already added to group '%2' with config '%3'. Attempt to add it with config '%4' is ignored.")
}

119
sources/BaseAPI/API/Commands/Tools/VotingsTool.uc

@ -1,119 +0,0 @@
/**
* 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 VotingsTool extends CmdItemsTool
dependson(CommandAPI);
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Voting`] classes.
//!
//! This storage class allows for efficient manipulation and retrieval of
//! [`Voting`] classes, along with information about what use groups were
//! authorized to use them.
//!
//! Additionally this tool is used to keep track of the currently ongoing
//! voting, preventing [`CommandsAPI`] from starting several votings at once.
/// Currently running voting process.
/// This tool doesn't actively track when voting ends, so reference can be
/// non-`none` even if voting has already ended. Instead `DropFinishedVoting()`
/// method is used as needed to figure out whether that voting has ended and
/// should be deallocated.
var private Voting currentVoting;
protected function Finalizer() {
super.Finalizer();
_.memory.Free(currentVoting);
currentVoting = none;
}
/// Starts a voting process with a given name, returning its result.
public final function CommandAPI.StartVotingResult StartVoting(
CommandAPI.VotingConfigInfo votingData,
HashTable arguments
) {
local CommandAPI.StartVotingResult result;
DropFinishedVoting();
if (currentVoting != none) {
return SVR_AlreadyInProgress;
}
if (votingData.votingClass == none) {
return SVR_UnknownVoting;
}
currentVoting = Voting(_.memory.Allocate(votingData.votingClass));
result = currentVoting.Start(votingData.config, arguments);
if (result != SVR_Success) {
_.memory.Free(currentVoting);
currentVoting = none;
}
return result;
}
/// Returns `true` iff some voting is currently active.
public final function bool IsVotingRunning() {
DropFinishedVoting();
return (currentVoting != none);
}
/// Returns instance of the active voting.
///
/// `none` iff no voting is currently active.
public final function Voting GetCurrentVoting() {
DropFinishedVoting();
if (currentVoting != none) {
currentVoting.NewRef();
}
return currentVoting;
}
protected function ItemCard MakeCard(class<AcediaObject> votingClass, BaseText itemName) {
local ItemCard newCard;
if (class<Voting>(votingClass) != none) {
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
newCard.InitializeWithClass(votingClass);
}
return newCard;
}
private final function class<Voting> GetVoting(BaseText itemName) {
local ItemCard relevantCard;
local class<Voting> result;
relevantCard = GetCard(itemName);
if (relevantCard != none) {
result = class<Voting>(relevantCard.GetItemClass());
}
_.memory.Free(relevantCard);
return result;
}
// Clears `currentVoting` if it has already finished
private final function DropFinishedVoting() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
}
defaultproperties {
ruleBaseClass = class'Voting'
}

563
sources/BaseAPI/API/Commands/Voting/TEST_Voting.uc

@ -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"
}

860
sources/BaseAPI/API/Commands/Voting/Voting.uc

File diff suppressed because it is too large Load Diff

243
sources/BaseAPI/API/Commands/Voting/VotingModel.uc

@ -19,30 +19,50 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class VotingModel extends AcediaObject class VotingModel extends AcediaObject;
dependsOn(MathApi);
//! This class counts votes according to the configured voting policies. //! This class counts votes according to the configured voting policies.
//! //!
//! Its main purpose is to separate the voting logic from the voting interface, //! Its main purpose is to separate the voting logic from the voting interface, making
//! making the implementation simpler and the logic easier to test. //! the implementation simpler and the logic easier to test.
//! //!
//! # Usage //! # Usage
//! //!
//! 1. Allocate an instance of the [`VotingModel`] class. //! 1. Allocate an instance of the [`VotingModel`] class.
//! 2. Call [`Start()`] to start voting with required policies. //! 2. Call [`Initialize()`] to set the required policies.
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote. //! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote.
//! You can change this set at any time before the voting has concluded. //! You can change this set at any time. The method used to recount the votes will depend on
//! The method used to recount the votes will depend on the policies set //! the policies set during the previous [`Initialize()`] call.
//! during the previous [`Initialize()`] call.
//! 4. Use [`CastVote()`] to add a vote from a user. //! 4. Use [`CastVote()`] to add a vote from a user.
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], //! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], check [`GetStatus()`] to
//! check [`GetStatus()`] to see if the voting has concluded. //! see if the voting has concluded.
//! Once voting has concluded, the result cannot be changed, so you can //! Once voting has concluded, the result cannot be changed, so you can release the reference
//! release the reference to the [`VotingModel`] object. //! to the [`VotingModel`] object.
//! 6. Alternatively, before voting has concluded naturally, you can use
//! [`ForceEnding()`] method to immediately end voting with result being /// Describes how [`VotingModel`] should react when a user performs potentially illegal actions.
//! determined by provided [`ForceEndingType`] argument. ///
/// 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. /// Current state of voting for this model.
enum VotingModelStatus { enum VotingModelStatus {
@ -53,7 +73,9 @@ enum VotingModelStatus {
/// Voting has ended with majority for its success /// Voting has ended with majority for its success
VPM_Success, VPM_Success,
/// Voting has ended with majority for its failure /// Voting has ended with majority for its failure
VPM_Failure VPM_Failure,
/// Voting has ended in a draw
VPM_Draw
}; };
/// A result of user trying to make a vote /// A result of user trying to make a vote
@ -80,20 +102,9 @@ enum PlayerVoteStatus {
PVS_VoteAgainst PVS_VoteAgainst
}; };
/// Types of possible outcomes when forcing a voting to end
enum ForceEndingType {
/// Result will be decided by the votes that already have been cast
FET_CurrentLeader,
/// Voting will end in success
FET_Success,
/// Voting will end in failure
FET_Failure
};
var private VotingModelStatus status; var private VotingModelStatus status;
/// Specifies whether draw would count as a victory for corresponding voting. var private bool policyCanLeave, policyCanChangeVote;
var private bool policyDrawWinsVoting;
var private array<UserID> votesFor, votesAgainst; var private array<UserID> votesFor, votesAgainst;
/// Votes of people that voted before, but then were forbidden to vote /// Votes of people that voted before, but then were forbidden to vote
@ -104,6 +115,8 @@ var private array<UserID> allowedVoters;
protected function Constructor() { protected function Constructor() {
status = VPM_Uninitialized; status = VPM_Uninitialized;
policyCanLeave = false;
policyCanChangeVote = false;
} }
protected function Finalizer() { protected function Finalizer() {
@ -120,42 +133,35 @@ protected function Finalizer() {
} }
/// Initializes voting by providing it with a set of policies to follow. /// Initializes voting by providing it with a set of policies to follow.
/// public final function Initialize(VotingPolicies policies) {
/// The only available policy is configuring whether draw means victory or loss if (status == VPM_Uninitialized) {
/// in voting. policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote);
/// policyCanChangeVote =
/// Can only be called once, after that will do nothing. (policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote);
public final function Start(bool drawWinsVoting) {
if (status != VPM_Uninitialized) {
return;
} }
policyDrawWinsVoting = drawWinsVoting;
status = VPM_InProgress; status = VPM_InProgress;
} }
/// Returns whether voting has already concluded. /// Returns whether voting has already concluded.
/// ///
/// This method should be checked after both [`CastVote()`] and /// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to /// whether either of them was enough to conclude the voting result.
/// conclude the voting result.
public final function bool HasEnded() { public final function bool HasEnded() {
return (status != VPM_Uninitialized && status != VPM_InProgress); return (status != VPM_Uninitialized && status != VPM_InProgress);
} }
/// Returns current status of voting. /// Returns current status of voting.
/// ///
/// This method should be checked after both [`CastVote()`] and /// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to /// whether either of them was enough to conclude the voting result.
/// conclude the voting result.
public final function VotingModelStatus GetStatus() { public final function VotingModelStatus GetStatus() {
return status; return status;
} }
/// Changes set of [`User`]s that are allowed to vote. /// Changes set of [`User`]s that are allowed to vote.
/// ///
/// Generally you want to provide this method with a list of current players, /// Generally you want to provide this method with a list of current players, optionally filtered
/// optionally filtered from spectators, users not in priviledged group or any /// from spectators, users not in priviledged group or any other relevant criteria.
/// other relevant criteria.
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
local int i; local int i;
@ -172,7 +178,8 @@ public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
/// Attempts to add a vote from specified user. /// Attempts to add a vote from specified user.
/// ///
/// Adding a vote can fail if [`voter`] isn't allowed to vote. /// 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) { public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
local bool votesSameWay; local bool votesSameWay;
local PlayerVoteStatus currentVote; local PlayerVoteStatus currentVote;
@ -183,12 +190,15 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
if (!IsVotingAllowedFor(voter)) { if (!IsVotingAllowedFor(voter)) {
return VFR_NotAllowed; return VFR_NotAllowed;
} }
currentVote = GetVote(voter); currentVote = HasVoted(voter);
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor)
|| (!voteForSuccess && currentVote == PVS_VoteAgainst); || (!voteForSuccess && currentVote == PVS_VoteAgainst);
if (votesSameWay) { if (votesSameWay) {
return VFR_AlreadyVoted; return VFR_AlreadyVoted;
} }
if (!policyCanChangeVote && currentVote != PVS_NoVote) {
return VFR_CannotChangeVote;
}
EraseVote(voter); EraseVote(voter);
voter.NewRef(); voter.NewRef();
if (voteForSuccess) { if (voteForSuccess) {
@ -200,11 +210,12 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
return VFR_Success; return VFR_Success;
} }
/// Checks if the provided user is allowed to vote based on the current list of /// Checks if the provided user is allowed to vote based on the current list of potential voters.
/// potential voters.
/// ///
/// The right to vote is decided solely by the list of potential voters set /// The right to vote is decided solely by the list of potential voters set using
/// using [`UpdatePotentialVoters()`]. /// [`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. /// Returns true if the user is allowed to vote, false otherwise.
public final function bool IsVotingAllowedFor(UserID voter) { public final function bool IsVotingAllowedFor(UserID voter) {
@ -223,8 +234,9 @@ public final function bool IsVotingAllowedFor(UserID voter) {
/// Returns the current vote status for the given voter. /// Returns the current vote status for the given voter.
/// ///
/// If the voter was previously allowed to vote, voted, and had their right to /// If the voter was previously allowed to vote, voted, and had their right to vote revoked, their
/// vote revoked, their vote won't count. /// 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) { public final function PlayerVoteStatus GetVote(UserID voter) {
local int i; local int i;
@ -241,100 +253,95 @@ public final function PlayerVoteStatus GetVote(UserID voter) {
return PVS_VoteAgainst; 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; return PVS_NoVote;
} }
/// Returns amount of current valid votes for the success of this voting. /// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() { public final function int GetVotesFor() {
return votesFor.length; if (policyCanLeave) {
return votesFor.length + storedVotesFor.length;
} else {
return votesFor.length;
}
} }
/// Returns amount of current valid votes against the success of this voting. /// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() { public final function int GetVotesAgainst() {
return votesAgainst.length; if (policyCanLeave) {
return votesAgainst.length + storedVotesAgainst.length;
} else {
return votesAgainst.length;
}
} }
/// Returns amount of users that are currently allowed to vote in this voting. /// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() { public final function int GetTotalPossibleVotes() {
return allowedVoters.length; if (policyCanLeave) {
return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length;
} else {
return allowedVoters.length;
}
} }
/// Checks whether, if stopped now, voting will win. // Checks if provided user has already voted.
public final function bool IsVotingWinning() { // Only checks among users that are currently allowed to vote, even if their past vote still counts.
if (status == VPM_Success) return true; private final function PlayerVoteStatus HasVoted(UserID voter) {
if (status == VPM_Failure) return false; local int i;
if (GetVotesFor() > GetVotesAgainst()) return true;
if (GetVotesFor() < GetVotesAgainst()) return false;
return policyDrawWinsVoting;
}
/// Forcibly ends the voting, deciding winner depending on the argument. if (voter == none) {
/// return PVS_NoVote;
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
///
/// By default decides result by the votes that already have been cast.
///
/// Returns `true` only if voting was actually ended with this call.
public final function bool ForceEnding(optional ForceEndingType type) {
if (status != VPM_InProgress) {
return false;
} }
switch (type) { for (i = 0; i < votesFor.length; i += 1) {
case FET_CurrentLeader: if (voter.IsEqual(votesFor[i])) {
if (IsVotingWinning()) { return PVS_VoteFor;
status = VPM_Success; }
} else { }
status = VPM_Failure; for (i = 0; i < votesAgainst.length; i += 1) {
} if (voter.IsEqual(votesAgainst[i])) {
break; return PVS_VoteAgainst;
case FET_Success: }
status = VPM_Success;
break;
case FET_Failure:
default:
status = VPM_Failure;
break;
} }
return true; return PVS_NoVote;
} }
private final function RecountVotes() { private final function RecountVotes() {
local MathApi.IntegerDivisionResult divisionResult; local bool canOverturn, everyoneVoted;
local int winningScore, losingScore;
local int totalPossibleVotes; local int totalPossibleVotes;
local int totalVotesFor, totalVotesAgainst;
local int lowerVoteCount, upperVoteCount, undecidedVoteCount;
if (status != VPM_InProgress) { if (status != VPM_InProgress) {
return; return;
} }
totalVotesFor = GetVotesFor();
totalVotesAgainst = GetVotesAgainst();
totalPossibleVotes = GetTotalPossibleVotes(); totalPossibleVotes = GetTotalPossibleVotes();
divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2); lowerVoteCount = Min(totalVotesFor, totalVotesAgainst);
if (divisionResult.remainder == 1) { upperVoteCount = Max(totalVotesFor, totalVotesAgainst);
// For odd amount of voters winning is simply majority undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount);
winningScore = divisionResult.quotient + 1; everyoneVoted = (undecidedVoteCount <= 0);
} else { canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount;
if (policyDrawWinsVoting) { if (everyoneVoted || !canOverturn) {
// For even amount of voters, exactly half is enough if draw means victory if (totalVotesFor > totalVotesAgainst) {
winningScore = divisionResult.quotient; status = VPM_Success;
} else if (totalVotesFor < totalVotesAgainst) {
status = VPM_Failure;
} else { } else {
// Otherwise - majority status = VPM_Draw;
winningScore = divisionResult.quotient + 1;
} }
} }
// The `winningScore` represents the number of votes required for a mean victory.
// If the number of votes against the mean is less than or equal to
// `totalPossibleVotes - winningScore`, then victory is still possible.
// However, if there is even one additional vote against, then victory is no longer achievable
// and a loss is inevitable.
losingScore = (totalPossibleVotes - winningScore) + 1;
// `totalPossibleVotes < losingScore + winningScore`, so only one of these inequalities
// can be satisfied at a time
if (GetVotesFor() >= winningScore) {
status = VPM_Success;
} else if (GetVotesAgainst() >= losingScore) {
status = VPM_Failure;
}
} }
private final function EraseVote(UserID voter) { private final function EraseVote(UserID voter) {

132
sources/BaseAPI/API/Commands/Voting/VotingPermissions.uc

@ -1,132 +0,0 @@
/**
* 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 VotingPermissions extends AcediaConfig
perobjectconfig
config(AcediaCommands);
/// Determines the duration of the voting period, specified in seconds.
/// Zero or negative values mean unlimited voting period.
var public config float votingTime;
/// Determines whether spectators are allowed to vote.
var public config bool allowSpectatorVoting;
/// Determines how draw will be interpreted.
/// `true` means draw counts as a vote's success, `false` means draw counts as a vote's failure.
var public config bool drawEqualsSuccess;
/// Specifies which group(s) of players are allowed to see who makes what vote.
var public config array<string> allowedToVoteGroup;
/// 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 forcibly end voting.
var public config array<string> allowedToForceGroup;
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);
data.SetBool(P("drawEqualsSuccess"), drawEqualsSuccess);
arrayOfTexts = _.collections.EmptyArrayList();
for (i = 0; i < allowedToVoteGroup.length; i += 1) {
arrayOfTexts.AddString(allowedToVoteGroup[i]);
}
data.SetItem(P("allowedToVoteGroup"), arrayOfTexts);
_.memory.Free(arrayOfTexts);
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 < allowedToForceGroup.length; i += 1) {
arrayOfTexts.AddString(allowedToForceGroup[i]);
}
data.SetItem(P("allowedToForceGroup"), 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);
drawEqualsSuccess = source.GetBool(P("drawEqualsSuccess"), false);
allowedToVoteGroup.length = 0;
arrayOfTexts = source.GetArrayList(P("allowedToVoteGroup"));
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) {
allowedToVoteGroup[allowedToVoteGroup.length] = arrayOfTexts.GetString(i);
}
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);
allowedToForceGroup.length = 0;
arrayOfTexts = source.GetArrayList(P("allowedToForceGroup"));
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) {
allowedToForceGroup[allowedToForceGroup.length] = arrayOfTexts.GetString(i);
}
_.memory.Free(arrayOfTexts);
}
protected function DefaultIt() {
votingTime = 30.0;
drawEqualsSuccess = false;
allowSpectatorVoting = false;
allowedToVoteGroup.length = 0;
allowedToSeeVotesGroup.length = 0;
allowedToForceGroup.length = 0;
allowedToVoteGroup[0] = "all";
allowedToSeeVotesGroup[0] = "all";
allowedToForceGroup[0] = "admin";
allowedToForceGroup[1] = "moderator";
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
votingTime = 30.0
drawEqualsSuccess = false
allowSpectatorVoting = false
allowedToVoteGroup(0) = "all"
allowedToSeeVotesGroup(0) = "all"
allowedToForceGroup(0) = "admin"
allowedToForceGroup(1) = "moderator"
}

75
sources/BaseAPI/API/Commands/Voting/VotingSettings.uc

@ -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"
}

4
sources/BaseAPI/API/Unflect/UnflectApi.uc

@ -58,7 +58,7 @@ protected function Finalizer() {
functionCaster = none; functionCaster = none;
} }
public final function _drop() { public final function bool _drop() {
local UFunction nextFunctionInstance; local UFunction nextFunctionInstance;
local Text nextFunctionName; local Text nextFunctionName;
local HashTableIterator iter; local HashTableIterator iter;
@ -67,7 +67,6 @@ public final function _drop() {
// Drop is called when Acedia is shutting down, so releasing references isn't necessary // Drop is called when Acedia is shutting down, so releasing references isn't necessary
iter = HashTableIterator(completedReplacements.Iterate()); iter = HashTableIterator(completedReplacements.Iterate());
while (!iter.HasFinished()) { while (!iter.HasFinished()) {
nextFunctionInstance = none;
nextFunctionName = Text(iter.GetKey()); nextFunctionName = Text(iter.GetKey());
nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName)); nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName));
if (nextSources != none ) { if (nextSources != none ) {
@ -76,7 +75,6 @@ public final function _drop() {
if (nextFunctionInstance != none) { if (nextFunctionInstance != none) {
nextFunctionInstance.script = nextSources.Get(); nextFunctionInstance.script = nextSources.Get();
} }
iter.Next();
} }
} }

5
sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc

@ -19,8 +19,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/ */
class AcediaEnvironment extends AcediaObject class AcediaEnvironment extends AcediaObject;
config(AcediaSystem);
//! API for management of running `Feature`s and loaded packages. //! API for management of running `Feature`s and loaded packages.
//! //!
@ -402,7 +401,7 @@ public final function DisableAllFeatures() {
defaultproperties defaultproperties
{ {
manifestSuffix = ".Manifest" manifestSuffix = ".Manifest"
debugMode = false debugMode = true
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.") errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")

2
sources/Chat/ChatAPI.uc

@ -210,7 +210,7 @@ public /*signal*/ function ChatAPI_OnMessageFor_Slot OnMessageFor(AcediaObject r
/// ///
/// # Slot description /// # Slot description
/// ///
/// void <slot>(EPlayer sender, ChatApi.BuiltInVoiceMessage message) /// bool <slot>(EPlayer sender, ChatApi.BuiltInVoiceMessage message)
/// ///
/// ## Parameters /// ## Parameters
/// ///

25
sources/Data/Database/DBAPI.uc

@ -250,49 +250,48 @@ public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
local DBRecord rootRecord; local DBRecord rootRecord;
local Text rootRecordName; local Text rootRecordName;
local LocalDatabase newConfig; local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance, result; local LocalDatabaseInstance newLocalDBInstance;
local Text dbKey; local Text dbKey;
if (databaseName == none) { if (databaseName == none) {
return none; return none;
} }
CreateLocalDBMapIfMissing(); CreateLocalDBMapIfMissing();
dbKey = databaseName.Copy(); if (loadedLocalDatabases.HasKey(databaseName))
if (loadedLocalDatabases.HasKey(dbKey))
{ {
result = LocalDatabaseInstance(loadedLocalDatabases.GetItem(dbKey)); return LocalDatabaseInstance(loadedLocalDatabases
_.memory.Free(dbKey); .GetItem(databaseName));
return result;
} }
// No need to check `dbKey` for being valid, // No need to check `databaseName` for being valid,
// since `Load()` will just return `none` if it is not. // since `Load()` will just return `none` if it is not.
newConfig = class'LocalDatabase'.static.Load(dbKey); newConfig = class'LocalDatabase'.static.Load(databaseName);
if (newConfig == none) { if (newConfig == none) {
_.memory.Free(dbKey);
return none; return none;
} }
if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) { if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) {
_.memory.Free(dbKey);
return none; return none;
} }
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass)); newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
dbKey = databaseName.Copy();
loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance); loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
dbKey.FreeSelf(); dbKey.FreeSelf();
if (newConfig.HasDefinedRoot()) if (newConfig.HasDefinedRoot())
{ {
rootRecordName = newConfig.GetRootName(); rootRecordName = newConfig.GetRootName();
rootRecord = class'DBRecord'.static.LoadRecord(rootRecordName, dbKey); rootRecord = class'DBRecord'.static
.LoadRecord(rootRecordName, databaseName);
} }
else else
{ {
rootRecord = class'DBRecord'.static.NewRecord(dbKey); rootRecord = class'DBRecord'.static.NewRecord(databaseName);
rootRecordName = _.text.FromString(string(rootRecord.name)); rootRecordName = _.text.FromString(string(rootRecord.name));
newConfig.SetRootName(rootRecordName); newConfig.SetRootName(rootRecordName);
newConfig.Save(); newConfig.Save();
} }
newLocalDBInstance.Initialize(newConfig, rootRecord); newLocalDBInstance.Initialize(newConfig, rootRecord);
_.logger.Auto(infoLocalDatabaseLoaded).Arg(dbKey); _.logger.Auto(infoLocalDatabaseLoaded).Arg(databaseName.Copy());
_.memory.Free(rootRecordName); _.memory.Free(rootRecordName);
_.memory.Free(newLocalDBInstance);
return newLocalDBInstance; return newLocalDBInstance;
} }

5
sources/Features/FeatureConfig.uc

@ -134,8 +134,7 @@ public static function bool SetAutoEnabledConfig(BaseText autoEnabledConfigName)
defaultproperties defaultproperties
{ {
usesObjectPool = false usesObjectPool = false
autoEnable = false autoEnable = false
supportsDataConversion = true
warningMultipleFeaturesAutoEnabled = (l=LOG_Warning,m="Multiple configs for `%1` were marked as \"auto enabled\". This is likely caused by an erroneous config. \"%2\" config will be used.") warningMultipleFeaturesAutoEnabled = (l=LOG_Warning,m="Multiple configs for `%1` were marked as \"auto enabled\". This is likely caused by an erroneous config. \"%2\" config will be used.")
} }

33
sources/InfoQueryHandler/InfoQueryHandler.uc

@ -34,7 +34,7 @@ var private const int TACEDIA_HELP_COMMANDS_CHAT, TACEDIA_HELP_COMMANDS_CONSOLE;
var private const int TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE; var private const int TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE;
var private const int TACEDIA_HELP_COMMANDS_NO, TACEDIA_HELP_COMMANDS_USELESS; var private const int TACEDIA_HELP_COMMANDS_NO, TACEDIA_HELP_COMMANDS_USELESS;
var private const int TACEDIA_RUNNING, TACEDIA_VERSION, TACEDIA_CREDITS; var private const int TACEDIA_RUNNING, TACEDIA_VERSION, TACEDIA_CREDITS;
var private const int TACEDIA_ACKNOWLEDGMENT, TPREFIX, THELP, TSEPARATOR; var private const int TACEDIA_ACKNOWLEDGMENT, TPREFIX, TSEPARATOR;
public static function StaticConstructor() public static function StaticConstructor()
{ {
@ -228,7 +228,7 @@ private final static function StopOutput()
private final static function OutAcediaHelp() private final static function OutAcediaHelp()
{ {
local MutableText prefix, helpName, builder; local MutableText prefix, builder;
default.currentOutput default.currentOutput
.Flush() .Flush()
@ -242,17 +242,12 @@ private final static function OutAcediaHelp()
.GetChatPrefix() .GetChatPrefix()
.IntoMutableText() .IntoMutableText()
.ChangeDefaultColor(__().color.TextEmphasis); .ChangeDefaultColor(__().color.TextEmphasis);
helpName = class'Commands_Feature'.static
.GetHelpCommandName()
.IntoMutableText()
.ChangeDefaultColor(__().color.TextEmphasis);
if ( class'Commands_Feature'.static.IsUsingChatInput() if ( class'Commands_Feature'.static.IsUsingChatInput()
&& class'Commands_Feature'.static.IsUsingMutateInput()) && class'Commands_Feature'.static.IsUsingMutateInput())
{ {
builder = builder =
T(default.TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE).MutableCopy(); T(default.TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE).MutableCopy();
builder.Replace(T(default.TPREFIX), prefix); builder.Replace(T(default.TPREFIX), prefix);
builder.Replace(T(default.THELP), helpName);
default.currentOutput.WriteLine(builder); default.currentOutput.WriteLine(builder);
__().memory.Free(builder); __().memory.Free(builder);
} }
@ -261,24 +256,20 @@ private final static function OutAcediaHelp()
builder = builder =
T(default.TACEDIA_HELP_COMMANDS_CHAT).MutableCopy(); T(default.TACEDIA_HELP_COMMANDS_CHAT).MutableCopy();
builder.Replace(T(default.TPREFIX), prefix); builder.Replace(T(default.TPREFIX), prefix);
builder.Replace(T(default.THELP), helpName);
default.currentOutput.WriteLine(builder); default.currentOutput.WriteLine(builder);
__().memory.Free(builder); __().memory.Free(builder);
} }
else if (class'Commands_Feature'.static.IsUsingMutateInput()) else if (class'Commands_Feature'.static.IsUsingMutateInput())
{ {
builder = default.currentOutput
T(default.TACEDIA_HELP_COMMANDS_CONSOLE).MutableCopy(); .WriteLine(T(default.TACEDIA_HELP_COMMANDS_CONSOLE));
builder.Replace(T(default.THELP), helpName);
default.currentOutput.WriteLine(builder);
__().memory.Free(builder);
} }
else else
{ {
default.currentOutput default.currentOutput
.WriteLine(T(default.TACEDIA_HELP_COMMANDS_USELESS)); .WriteLine(T(default.TACEDIA_HELP_COMMANDS_USELESS));
} }
__().memory.Free2(prefix, helpName); __().memory.Free(prefix);
} }
private final static function OutAcediaStatus() private final static function OutAcediaStatus()
@ -306,11 +297,11 @@ defaultproperties
TACEDIA_HELP = 2 TACEDIA_HELP = 2
stringConstants(2) = "Acedia always supports four commands: {$TextEmphasis help}, {$TextEmphasis status}, {$TextEmphasis version} and {$TextEmphasis credits}" stringConstants(2) = "Acedia always supports four commands: {$TextEmphasis help}, {$TextEmphasis status}, {$TextEmphasis version} and {$TextEmphasis credits}"
TACEDIA_HELP_COMMANDS_CHAT = 3 TACEDIA_HELP_COMMANDS_CHAT = 3
stringConstants(3) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%%HELP%} in chat" stringConstants(3) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help} in chat"
TACEDIA_HELP_COMMANDS_CONSOLE = 4 TACEDIA_HELP_COMMANDS_CONSOLE = 4
stringConstants(4) = "To get detailed information about available to you commands, please type {$TextEmphasis mutate %HELP% -l} in console" stringConstants(4) = "To get detailed information about available to you commands, please type {$TextEmphasis mutate help -l} in console"
TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE = 5 TACEDIA_HELP_COMMANDS_CHAT_AND_CONSOLE = 5
stringConstants(5) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%%HELP%} in chat or {$TextEmphasis mutate %HELP% -l} in console" stringConstants(5) = "To get detailed information about available to you commands, please type {$TextEmphasis %PREFIX%help} in chat or {$TextEmphasis mutate help -l} in console"
TACEDIA_HELP_COMMANDS_NO = 6 TACEDIA_HELP_COMMANDS_NO = 6
stringConstants(6) = "Unfortunately other commands aren't available right now. To enable them please type {$TextEmphasis mutate acediacommands} in console if you have enough rights to reenable them." stringConstants(6) = "Unfortunately other commands aren't available right now. To enable them please type {$TextEmphasis mutate acediacommands} in console if you have enough rights to reenable them."
TACEDIA_HELP_COMMANDS_USELESS = 7 TACEDIA_HELP_COMMANDS_USELESS = 7
@ -320,13 +311,11 @@ defaultproperties
TACEDIA_VERSION = 9 TACEDIA_VERSION = 9
stringConstants(9) = "AcediaCore version 0.1.dev8 - this is a development version, bugs and issues are expected" stringConstants(9) = "AcediaCore version 0.1.dev8 - this is a development version, bugs and issues are expected"
TACEDIA_CREDITS = 10 TACEDIA_CREDITS = 10
stringConstants(10) = "AcediaCore was developed by dkanus, 2019 - 2023" stringConstants(10) = "AcediaCore was developed by dkanus, 2019 - 2022"
TACEDIA_ACKNOWLEDGMENT = 11 TACEDIA_ACKNOWLEDGMENT = 11
stringConstants(11) = "Special thanks for NikC- and Chaos for suggestions, testing and discussion" stringConstants(11) = "Special thanks for NikC- and Chaos for suggestions, testing and discussion"
TPREFIX = 12 TPREFIX = 12
stringConstants(12) = "%PREFIX%" stringConstants(12) = "%PREFIX%"
THELP = 13 TSEPARATOR = 13
stringConstants(13) = "%HELP%" stringConstants(13) = "============================="
TSEPARATOR = 14
stringConstants(14) = "============================="
} }

13
sources/Players/EPlayer.uc

@ -422,19 +422,6 @@ public final function bool IsAdmin()
return (GetAdminStatus() != AS_None); return (GetAdminStatus() != AS_None);
} }
/// Checks if player is a spectator, i.e. observes game without actively
/// participating.
public final function bool IsSpectator()
{
local PlayerReplicationInfo myReplicationInfo;
myReplicationInfo = GetRI();
if (myReplicationInfo == none) {
return true;
}
return myReplicationInfo.bOnlySpectator;
}
/** /**
* Changes admin status of the caller `EPlayer`. * Changes admin status of the caller `EPlayer`.
* Can only fail if caller `EPlayer` has already disconnected. * Can only fail if caller `EPlayer` has already disconnected.

87
sources/Text/TextAPI.uc

@ -317,39 +317,10 @@ public final function bool IsEmpty(BaseText text)
public final function string IntoString(/*take*/ BaseText toConvert) public final function string IntoString(/*take*/ BaseText toConvert)
{ {
local string result; local string result;
if (toConvert != none) { if (toConvert != none) {
result = toConvert.ToString(); result = toConvert.ToString();
_.memory.Free(toConvert);
}
return result;
}
/**
* Converts given array of `BaseText`s into an array of plain `string`s,
* returning their values and deallocating passed `BaseText`.
*
* Method introduced to simplify a common use-case of converting returned
* copies of `BaseText`s into a `string`s, which required additional variable
* to store and later deallocate `BaseText` references.
*
* @param toConvert Array of `BaseText`s to convert.
* @return Array of `string`s, representing passed `BaseText`s as
* a plain string.
* Empty `string`, if `toConvert == none`.
*/
public final function array<string> IntoStrings(/*take*/ array<BaseText> toConvert) {
local int i;
local array<string> result;
for (i = 0; i < toConvert.length; i += 1) {
if (toConvert[i] != none) {
result[result.length] = toConvert[i].ToString();
_.memory.Free(toConvert[i]);
} else {
result[result.length] = "";
}
} }
_.memory.Free(toConvert);
return result; return result;
} }
@ -375,34 +346,6 @@ public final function string IntoColoredString(/*take*/ BaseText toConvert)
return result; return result;
} }
/**
* Converts given array of `BaseText`s into an array of colored `string`s,
* returning their values and deallocating passed `BaseText`.
*
* Method introduced to simplify a common use-case of converting returned
* copies of `BaseText`s into a `string`s, which required additional variable
* to store and later deallocate `BaseText` references.
*
* @param toConvert Array of `BaseText`s to convert.
* @return Array of `string`s, representing passed `BaseText`s as
* a colored string.
* Empty `string`, if `toConvert == none`.
*/
public final function array<string> IntoColoredStrings(/*take*/ array<BaseText> toConvert) {
local int i;
local array<string> result;
for (i = 0; i < toConvert.length; i += 1) {
if (toConvert[i] != none) {
result[result.length] = toConvert[i].ToColoredString();
_.memory.Free(toConvert[i]);
} else {
result[result.length] = "";
}
}
return result;
}
/** /**
* Converts given `BaseText` into a formatted `string`, returns it's value and * Converts given `BaseText` into a formatted `string`, returns it's value and
* deallocates passed `BaseText`. * deallocates passed `BaseText`.
@ -425,34 +368,6 @@ public final function string IntoFormattedString(/*take*/ BaseText toConvert)
return result; return result;
} }
/**
* Converts given array of `BaseText`s into an array of formatted `string`s,
* returning their values and deallocating passed `BaseText`.
*
* Method introduced to simplify a common use-case of converting returned
* copies of `BaseText`s into a `string`s, which required additional variable
* to store and later deallocate `BaseText` references.
*
* @param toConvert Array of `BaseText`s to convert.
* @return Array of `string`s, representing passed `BaseText`s as
* a formatted string.
* Empty `string`, if `toConvert == none`.
*/
public final function array<string> IntoFormattedStrings(/*take*/ array<BaseText> toConvert) {
local int i;
local array<string> result;
for (i = 0; i < toConvert.length; i += 1) {
if (toConvert[i] != none) {
result[result.length] = toConvert[i].ToFormattedString();
_.memory.Free(toConvert[i]);
} else {
result[result.length] = "";
}
}
return result;
}
/** /**
* Creates a `string` that consists only of a given character. * Creates a `string` that consists only of a given character.
* *

7
sources/Users/ACommandUserGroups.uc

@ -22,6 +22,7 @@ class ACommandUserGroups extends Command
protected function BuildData(CommandDataBuilder builder) protected function BuildData(CommandDataBuilder builder)
{ {
builder.Name(P("usergroups"));
builder.Group(P("admin")); builder.Group(P("admin"));
builder.Summary(P("User groups management.")); builder.Summary(P("User groups management."));
builder.Describe(P("Allows to add/remove user groups and users to these: groups. Changes made" builder.Describe(P("Allows to add/remove user groups and users to these: groups. Changes made"
@ -76,7 +77,7 @@ protected function BuildData(CommandDataBuilder builder)
builder.Describe(P("Allows to force usage of invalid user IDs.")); builder.Describe(P("Allows to force usage of invalid user IDs."));
} }
protected function Executed(CallData arguments, EPlayer instigator, CommandPermissions permissions) protected function Executed(CallData arguments, EPlayer instigator)
{ {
local bool forceOption; local bool forceOption;
local Text groupName, userID, userName, annotation; local Text groupName, userID, userName, annotation;
@ -652,6 +653,6 @@ private function DisplayUsersFor(Text groupName)
} }
} }
defaultproperties { defaultproperties
preferredName = "usergroups" {
} }

53
sources/Users/Users_Feature.uc

@ -70,7 +70,16 @@ var private LoggerAPI.Definition errDBContainsNonLowerRegister;
protected function OnEnabled() protected function OnEnabled()
{ {
local Commands_Feature feature;
_.users._reloadFeature(); _.users._reloadFeature();
feature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (feature != none)
{
feature.RegisterCommand(class'ACommandUserGroups');
feature.FreeSelf();
}
if (_server.IsAvailable()) { if (_server.IsAvailable()) {
LoadUserData(); LoadUserData();
SetupPersistentData(usePersistentData); SetupPersistentData(usePersistentData);
@ -87,7 +96,9 @@ protected function OnDisabled()
_.users._reloadFeature(); _.users._reloadFeature();
feature = feature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (feature != none) { if (feature != none)
{
feature.RemoveCommand(class'ACommandUserGroups');
feature.FreeSelf(); feature.FreeSelf();
} }
ResetUploadedUserGroups(); ResetUploadedUserGroups();
@ -579,9 +590,9 @@ public final function bool AddGroup(BaseText groupName)
local Text lowerCaseGroupName; local Text lowerCaseGroupName;
local HashTable emptyHashTable; local HashTable emptyHashTable;
if (groupName == none) return false; if (groupName == none) {
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return false; return false;
}
lowerCaseGroupName = groupName.LowerCopy(); lowerCaseGroupName = groupName.LowerCopy();
if (loadedGroupToUsersMap.HasKey(lowerCaseGroupName)) if (loadedGroupToUsersMap.HasKey(lowerCaseGroupName))
{ {
@ -665,9 +676,9 @@ public final function bool RemoveGroup(BaseText groupName)
local bool groupExists; local bool groupExists;
local Text lowerCaseGroupName; local Text lowerCaseGroupName;
if (groupName == none) return false; if (groupName == none) {
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return false; return false;
}
lowerCaseGroupName = groupName.LowerCopy(); lowerCaseGroupName = groupName.LowerCopy();
groupExists = loadedGroupToUsersMap.HasKey(lowerCaseGroupName); groupExists = loadedGroupToUsersMap.HasKey(lowerCaseGroupName);
if (!groupExists) if (!groupExists)
@ -743,9 +754,9 @@ public final function bool IsGroupExisting(BaseText groupName)
local bool result; local bool result;
local Text lowerCaseGroupName; local Text lowerCaseGroupName;
if (groupName == none) return false; if (groupName == none) {
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return true; return false;
}
lowerCaseGroupName = groupName.LowerCopy(); lowerCaseGroupName = groupName.LowerCopy();
result = loadedGroupToUsersMap.HasKey(lowerCaseGroupName); result = loadedGroupToUsersMap.HasKey(lowerCaseGroupName);
lowerCaseGroupName.FreeSelf(); lowerCaseGroupName.FreeSelf();
@ -797,10 +808,9 @@ public final function bool AddSteamIDToGroup(
local Text lowercaseGroupName; local Text lowercaseGroupName;
local HashTable groupUsers; local HashTable groupUsers;
if (steamID == none) return false; if (steamID == none) return false;
if (loadedGroupToUsersMap == none) return false; if (loadedGroupToUsersMap == none) return false;
if (groupName == none) return false; if (groupName == none) return false;
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return true;
lowercaseGroupName = groupName.LowerCopy(); lowercaseGroupName = groupName.LowerCopy();
groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName);
@ -988,10 +998,9 @@ public final function bool RemoveSteamIDFromGroup(
local Text lowercaseGroupName; local Text lowercaseGroupName;
local HashTable groupUsers; local HashTable groupUsers;
if (steamID == none) return false; if (steamID == none) return false;
if (groupName == none) return false; if (groupName == none) return false;
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return false; if (loadedGroupToUsersMap == none) return false;
if (loadedGroupToUsersMap == none) return false;
lowercaseGroupName = groupName.LowerCopy(); lowercaseGroupName = groupName.LowerCopy();
groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName);
@ -1186,7 +1195,6 @@ public final function array<Text> GetGroupsForSteamID(BaseText steamID)
if (loadedGroupToUsersMap == none) return result; if (loadedGroupToUsersMap == none) return result;
if (steamID == none) return result; if (steamID == none) return result;
result[0] = P("all").Copy();
immutableSteamID = steamID.LowerCopy(); immutableSteamID = steamID.LowerCopy();
iter = HashTableIterator(loadedGroupToUsersMap.Iterate()); iter = HashTableIterator(loadedGroupToUsersMap.Iterate());
while (!iter.HasFinished()) while (!iter.HasFinished())
@ -1952,10 +1960,9 @@ public final function bool IsSteamIDInGroup(
local Text lowerGroupName; local Text lowerGroupName;
local HashTable nextGroupUsers; local HashTable nextGroupUsers;
if (loadedGroupToUsersMap == none) return false; if (loadedGroupToUsersMap == none) return false;
if (groupName == none) return false; if (groupName == none) return false;
if (groupName.Compare(P("all"), SCASE_INSENSITIVE)) return true; if (steamID == none) return false;
if (steamID == none) return false;
lowerGroupName = groupName.LowerCopy(); lowerGroupName = groupName.LowerCopy();
nextGroupUsers = loadedGroupToUsersMap.GetHashTable(lowerGroupName); nextGroupUsers = loadedGroupToUsersMap.GetHashTable(lowerGroupName);

Loading…
Cancel
Save