Compare commits
No commits in common. 'master' and 'core_refactor' have entirely different histories.
master
...
core_refac
122 changed files with 5779 additions and 13911 deletions
@ -1,8 +0,0 @@ |
|||||||
; This config file allows you to configure command aliases. |
|
||||||
; Remember that aliases are case-insensitive. |
|
||||||
[AcediaCore.CommandAliasSource] |
|
||||||
record=(alias="yes",value="vote.yes") |
|
||||||
record=(alias="no",value="vote.no") |
|
||||||
|
|
||||||
[help CommandAliases] |
|
||||||
Alias="hlp" |
|
@ -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 |
|
@ -1,22 +0,0 @@ |
|||||||
[default VotingSettings] |
|
||||||
;= Determines the duration of the voting period, specified in seconds. |
|
||||||
votingTime=30 |
|
||||||
;= Determines whether spectators are allowed to vote. |
|
||||||
allowSpectatorVoting=false |
|
||||||
;= Specifies which group(s) of players are allowed to see who makes what vote. |
|
||||||
allowedToSeeVotesGroup="admin" |
|
||||||
allowedToSeeVotesGroup="moderator" |
|
||||||
;= Specifies which group(s) of players are allowed to vote. |
|
||||||
allowedToVoteGroup="everybody" |
|
||||||
|
|
||||||
[moderator VotingSettings] |
|
||||||
votingTime=30 |
|
||||||
allowSpectatorVoting=true |
|
||||||
allowedToSeeVotesGroup="admin" |
|
||||||
allowedToVoteGroup="moderator" |
|
||||||
allowedToVoteGroup="admin" |
|
||||||
|
|
||||||
[admin VotingSettings] |
|
||||||
votingTime=30 |
|
||||||
allowSpectatorVoting=true |
|
||||||
allowedToVoteGroup="admin" |
|
@ -1,163 +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 ACommandFakers extends Command |
|
||||||
dependsOn(VotingModel); |
|
||||||
|
|
||||||
var private array<UserID> fakers; |
|
||||||
|
|
||||||
protected static function StaticFinalizer() { |
|
||||||
__().memory.FreeMany(default.fakers); |
|
||||||
default.fakers.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
protected function BuildData(CommandDataBuilder builder) { |
|
||||||
builder.Group(P("debug")); |
|
||||||
builder.Summary(P("Adds fake voters for testing \"vote\" command.")); |
|
||||||
builder.Describe(P("Displays current fake voters.")); |
|
||||||
|
|
||||||
builder.SubCommand(P("amount")); |
|
||||||
builder.Describe(P("Specify amount of faker that are allowed to vote.")); |
|
||||||
builder.ParamInteger(P("fakers_amount")); |
|
||||||
|
|
||||||
builder.SubCommand(P("vote")); |
|
||||||
builder.Describe(P("Make a vote as a faker.")); |
|
||||||
builder.ParamInteger(P("faker_number")); |
|
||||||
builder.ParamBoolean(P("vote_for")); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Executed( |
|
||||||
CallData arguments, |
|
||||||
EPlayer instigator, |
|
||||||
CommandPermissions permissions |
|
||||||
) { |
|
||||||
if (arguments.subCommandName.IsEmpty()) { |
|
||||||
DisplayCurrentFakers(); |
|
||||||
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) { |
|
||||||
ChangeAmount(arguments.parameters.GetInt(P("fakers_amount"))); |
|
||||||
} else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) { |
|
||||||
CastVote( |
|
||||||
arguments.parameters.GetInt(P("faker_number")), |
|
||||||
arguments.parameters.GetBool(P("vote_for"))); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public final static function /*borrow*/ array<UserID> BorrowDebugVoters() { |
|
||||||
return default.fakers; |
|
||||||
} |
|
||||||
|
|
||||||
private final function CastVote(int fakerID, bool voteFor) { |
|
||||||
local Voting currentVoting; |
|
||||||
|
|
||||||
if (fakerID < 0 || fakerID >= fakers.length) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextFailure) |
|
||||||
.WriteLine(P("Faker number is out of bounds.")); |
|
||||||
return; |
|
||||||
} |
|
||||||
currentVoting = _.commands.GetCurrentVoting(); |
|
||||||
if (currentVoting == none) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextFailure) |
|
||||||
.WriteLine(P("There is no voting active right now.")); |
|
||||||
return; |
|
||||||
} |
|
||||||
currentVoting.CastVoteByID(fakers[fakerID], voteFor); |
|
||||||
_.memory.Free(currentVoting); |
|
||||||
} |
|
||||||
|
|
||||||
private final function ChangeAmount(int newAmount) { |
|
||||||
local int i; |
|
||||||
local Text nextIDName; |
|
||||||
local UserID nextID; |
|
||||||
local Voting currentVoting; |
|
||||||
|
|
||||||
if (newAmount < 0) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextFailure) |
|
||||||
.WriteLine(P("Cannot specify negative amount.")); |
|
||||||
} |
|
||||||
if (newAmount == fakers.length) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextNeutral) |
|
||||||
.WriteLine(P("Specified same amount of fakers.")); |
|
||||||
} else if (newAmount > fakers.length) { |
|
||||||
for (i = fakers.length; i < newAmount; i += 1) { |
|
||||||
nextIDName = _.text.FromString("DEBUG:FAKER:" $ i); |
|
||||||
nextID = UserID(__().memory.Allocate(class'UserID')); |
|
||||||
nextID.Initialize(nextIDName); |
|
||||||
_.memory.Free(nextIDName); |
|
||||||
fakers[fakers.length] = nextID; |
|
||||||
} |
|
||||||
} else { |
|
||||||
for (i = fakers.length - 1; i >= newAmount; i -= 1) { |
|
||||||
_.memory.Free(fakers[i]); |
|
||||||
} |
|
||||||
fakers.length = newAmount; |
|
||||||
} |
|
||||||
default.fakers = fakers; |
|
||||||
currentVoting = _.commands.GetCurrentVoting(); |
|
||||||
if (currentVoting != none) { |
|
||||||
currentVoting.SetDebugVoters(default.fakers); |
|
||||||
_.memory.Free(currentVoting); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private function DisplayCurrentFakers() { |
|
||||||
local int i; |
|
||||||
local VotingModel.PlayerVoteStatus nextVoteStatus; |
|
||||||
local MutableText nextNumber; |
|
||||||
local Voting currentVoting; |
|
||||||
|
|
||||||
if (fakers.length <= 0) { |
|
||||||
callerConsole.WriteLine(P("No fakers!")); |
|
||||||
return; |
|
||||||
} |
|
||||||
currentVoting =_.commands.GetCurrentVoting(); |
|
||||||
for (i = 0; i < fakers.length; i += 1) { |
|
||||||
nextNumber = _.text.FromIntM(i); |
|
||||||
callerConsole |
|
||||||
.Write(P("Faker #")) |
|
||||||
.Write(nextNumber) |
|
||||||
.Write(P(": ")); |
|
||||||
if (currentVoting != none) { |
|
||||||
nextVoteStatus = currentVoting.GetVote(fakers[i]); |
|
||||||
} |
|
||||||
switch (nextVoteStatus) { |
|
||||||
case PVS_NoVote: |
|
||||||
callerConsole.WriteLine(P("no vote")); |
|
||||||
break; |
|
||||||
case PVS_VoteFor: |
|
||||||
callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for")); |
|
||||||
break; |
|
||||||
case PVS_VoteAgainst: |
|
||||||
callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against")); |
|
||||||
break; |
|
||||||
default: |
|
||||||
callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!")); |
|
||||||
} |
|
||||||
_.memory.Free(nextNumber); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
preferredName = "fakers" |
|
||||||
} |
|
@ -1,69 +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 ACommandNotify extends Command |
|
||||||
dependsOn(ChatApi); |
|
||||||
|
|
||||||
protected function BuildData(CommandDataBuilder builder) { |
|
||||||
builder.Group(P("core")); |
|
||||||
builder.Summary(P("Notifies players with provided message.")); |
|
||||||
builder.ParamText(P("message")); |
|
||||||
builder.OptionalParams(); |
|
||||||
builder.ParamNumber(P("duration")); |
|
||||||
builder.Describe(P("Notify to players message with distinct header and body.")); |
|
||||||
builder.RequireTarget(); |
|
||||||
|
|
||||||
builder.Option(P("title")); |
|
||||||
builder.Describe(P("Specify the optional title of the notification.")); |
|
||||||
builder.ParamText(P("title")); |
|
||||||
|
|
||||||
builder.Option(P("channel")); |
|
||||||
builder.Describe(P("Specify the optional channel. A channel is a grouping mechanism used to" |
|
||||||
@ "control the display of related notifications. Only last message from the same channel is" |
|
||||||
@ "stored in queue.")); |
|
||||||
builder.ParamText(P("channel_name")); |
|
||||||
} |
|
||||||
|
|
||||||
protected function ExecutedFor( |
|
||||||
EPlayer target, |
|
||||||
CallData arguments, |
|
||||||
EPlayer instigator, |
|
||||||
CommandPermissions permissions |
|
||||||
) { |
|
||||||
local Text title, message, plainTitle, plainMessage; |
|
||||||
|
|
||||||
plainMessage = arguments.parameters.GetText(P("message")); |
|
||||||
if (arguments.options.HasKey(P("title"))) { |
|
||||||
plainTitle = arguments.options.GetTextBy(P("/title/title")); |
|
||||||
} |
|
||||||
title = _.text.FromFormatted(plainTitle); |
|
||||||
message = _.text.FromFormatted(plainMessage); |
|
||||||
target.Notify( |
|
||||||
title, |
|
||||||
message, |
|
||||||
arguments.parameters.GetFloat(P("duration")), |
|
||||||
arguments.options.GetTextBy(P("/channel/channel_name"))); |
|
||||||
_.memory.Free4(title, message, plainTitle, plainMessage); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
preferredName = "notify" |
|
||||||
} |
|
@ -1,197 +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 ACommandSideEffects extends Command; |
|
||||||
|
|
||||||
// Maps `UserID` to `ArrayList` with side effects listed for that player last time |
|
||||||
var private HashTable displayedLists; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
super.Constructor(); |
|
||||||
displayedLists = _.collections.EmptyHashTable(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
super.Finalizer(); |
|
||||||
_.memory.Free(displayedLists); |
|
||||||
displayedLists = none; |
|
||||||
} |
|
||||||
|
|
||||||
protected function BuildData(CommandDataBuilder builder) { |
|
||||||
builder.Group(P("debug")); |
|
||||||
builder.Summary(P("Displays information about current side effects.")); |
|
||||||
builder.Describe(P("This command allows to display current side effects, optionally filtering" |
|
||||||
@ "them by specified package names.")); |
|
||||||
builder.OptionalParams(); |
|
||||||
builder.ParamTextList(P("package_names")); |
|
||||||
|
|
||||||
builder.SubCommand(P("show")); |
|
||||||
builder.Describe(P("This sub-command is only usable after side effects have been shown" |
|
||||||
@ "at least once. It takes an index from the last displayed list and displays a verbose" |
|
||||||
@ "information about it.")); |
|
||||||
builder.ParamInteger(P("side_effect_number")); |
|
||||||
|
|
||||||
builder.Option(P("verbose")); |
|
||||||
builder.Describe(P("Display verbose information about each side effect.")); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Executed( |
|
||||||
CallData arguments, |
|
||||||
EPlayer instigator, |
|
||||||
CommandPermissions permissions |
|
||||||
) { |
|
||||||
local UserID playerID; |
|
||||||
local array<SideEffect> relevantSideEffects; |
|
||||||
local ArrayList packagesList, storedSideEffectsList; |
|
||||||
|
|
||||||
playerID = instigator.GetUserID(); |
|
||||||
if (arguments.subCommandName.IsEmpty()) { |
|
||||||
relevantSideEffects = _.sideEffects.GetAll(); |
|
||||||
packagesList = arguments.parameters.GetArrayList(P("package_names")); |
|
||||||
FilterSideEffects(/*out*/ relevantSideEffects, packagesList); |
|
||||||
_.memory.Free(packagesList); |
|
||||||
DisplaySideEffects(relevantSideEffects, arguments.options.HasKey(P("verbose"))); |
|
||||||
// Store new side effect list |
|
||||||
storedSideEffectsList = _.collections.NewArrayList(relevantSideEffects); |
|
||||||
displayedLists.SetItem(playerID, storedSideEffectsList); |
|
||||||
_.memory.FreeMany(relevantSideEffects); |
|
||||||
_.memory.Free(storedSideEffectsList); |
|
||||||
} else { |
|
||||||
ShowInfoFor(playerID, arguments.parameters.GetInt(P("side_effect_number"))); |
|
||||||
} |
|
||||||
_.memory.Free(playerID); |
|
||||||
} |
|
||||||
|
|
||||||
private function FilterSideEffects(out array<SideEffect> sideEffects, ArrayList allowedPackages) { |
|
||||||
local int i, j; |
|
||||||
local int packagesLength; |
|
||||||
local bool matchedPackage; |
|
||||||
local Text nextSideEffectPackage, nextAllowedPackage; |
|
||||||
|
|
||||||
if (allowedPackages == none) return; |
|
||||||
if (allowedPackages.GetLength() <= 0) return; |
|
||||||
|
|
||||||
packagesLength = allowedPackages.GetLength(); |
|
||||||
while (i < sideEffects.length) { |
|
||||||
nextSideEffectPackage = sideEffects[i].GetPackage(); |
|
||||||
matchedPackage = false; |
|
||||||
for (j = 0; j < packagesLength; j += 1) { |
|
||||||
nextAllowedPackage = allowedPackages.GetText(j); |
|
||||||
if (nextAllowedPackage.Compare(nextSideEffectPackage, SCASE_INSENSITIVE)) { |
|
||||||
matchedPackage = true; |
|
||||||
_.memory.Free(nextAllowedPackage); |
|
||||||
break; |
|
||||||
} |
|
||||||
_.memory.Free(nextAllowedPackage); |
|
||||||
} |
|
||||||
if (!matchedPackage) { |
|
||||||
sideEffects.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
_.memory.Free(nextSideEffectPackage); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private function DisplaySideEffects(array<SideEffect> toDisplay, bool verbose) { |
|
||||||
local int i; |
|
||||||
local MutableText nextPrefix; |
|
||||||
|
|
||||||
if (toDisplay.length <= 0) { |
|
||||||
callerConsole.Write(F("List of side effects is {$TextNeutral empty}.")); |
|
||||||
} |
|
||||||
for (i = 0; i < toDisplay.length; i += 1) { |
|
||||||
nextPrefix = _.text.FromIntM(i + 1); |
|
||||||
nextPrefix.Append(P(".")); |
|
||||||
DisplaySideEffect(toDisplay[i], nextPrefix, verbose); |
|
||||||
_.memory.Free(nextPrefix); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private function DisplaySideEffect(SideEffect toDisplay, BaseText prefix, bool verbose) { |
|
||||||
local Text effectName, effectDescription, effectPackage, effectSource, effectStatus; |
|
||||||
|
|
||||||
if (toDisplay == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (prefix != none) { |
|
||||||
callerConsole.Write(prefix); |
|
||||||
callerConsole.Write(P(" ")); |
|
||||||
} |
|
||||||
effectName = toDisplay.GetName(); |
|
||||||
effectPackage = toDisplay.GetPackage(); |
|
||||||
effectSource = toDisplay.GetSource(); |
|
||||||
effectStatus = toDisplay.GetStatus(); |
|
||||||
callerConsole.UseColor(_.color.TextEmphasis); |
|
||||||
callerConsole.Write(P("[")); |
|
||||||
callerConsole.Write(effectPackage); |
|
||||||
callerConsole.Write(P(" \\ ")); |
|
||||||
callerConsole.Write(effectSource); |
|
||||||
callerConsole.Write(P("] ")); |
|
||||||
callerConsole.ResetColor(); |
|
||||||
callerConsole.Write(effectName); |
|
||||||
callerConsole.Write(P(" {")); |
|
||||||
callerConsole.Write(effectStatus); |
|
||||||
callerConsole.WriteLine(P("}")); |
|
||||||
if (verbose) { |
|
||||||
effectDescription = toDisplay.GetDescription(); |
|
||||||
callerConsole.WriteBlock(effectDescription); |
|
||||||
} |
|
||||||
_.memory.Free5(effectName, effectDescription, effectPackage, effectSource, effectStatus); |
|
||||||
} |
|
||||||
|
|
||||||
private function ShowInfoFor(UserID playerID, int sideEffectIndex) { |
|
||||||
local SideEffect toDisplay; |
|
||||||
local ArrayList sideEffectList; |
|
||||||
|
|
||||||
if (playerID == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (sideEffectIndex <= 0) { |
|
||||||
callerConsole.WriteLine(F("Specified side effect index {$TextNegative isn't positive}!")); |
|
||||||
return; |
|
||||||
} |
|
||||||
sideEffectList = displayedLists.GetArrayList(playerID); |
|
||||||
if (sideEffectList == none) { |
|
||||||
callerConsole.WriteLine(F("{$TextNegative Cannot display} side effect by index without" |
|
||||||
@ "first listing them. Call {$TextEmphasis sideeffects} command without" |
|
||||||
@ "{$TextEmphasis show} subcommand first.")); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (sideEffectIndex > sideEffectList.GetLength()) { |
|
||||||
callerConsole.WriteLine(F("Specified side effect index is {$TextNegative out of bounds}.")); |
|
||||||
_.memory.Free(sideEffectList); |
|
||||||
return; |
|
||||||
} |
|
||||||
// Above we checked that `sideEffectIndex` lies within `[0; sideEffectList.GetLength()]` segment |
|
||||||
// This means that `sideEffectIndex - 1` points at non-`none` value |
|
||||||
toDisplay = SideEffect(sideEffectList.GetItem(sideEffectIndex - 1)); |
|
||||||
if (!_.sideEffects.IsRegistered(toDisplay)) { |
|
||||||
callerConsole.UseColorOnce(_.color.TextWarning); |
|
||||||
callerConsole.WriteLine(P("Selected side effect is no longer active!")); |
|
||||||
} |
|
||||||
DisplaySideEffect(toDisplay, none, true); |
|
||||||
_.memory.Free2(toDisplay, sideEffectList); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
preferredName = "sideeffects" |
|
||||||
} |
|
@ -1,220 +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 ACommandVote extends Command |
|
||||||
dependson(CommandAPI) |
|
||||||
dependson(VotingModel); |
|
||||||
|
|
||||||
var private CommandDataBuilder dataBuilder; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
ResetVotingInfo(); |
|
||||||
_.commands.OnVotingAdded(self).connect = AddVotingInfo; |
|
||||||
_.commands.OnVotingRemoved(self).connect = HandleRemovedVoting; |
|
||||||
_.chat.OnVoiceMessage(self).connect = VoteWithVoice; |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
super.Finalizer(); |
|
||||||
_.memory.Free(dataBuilder); |
|
||||||
dataBuilder = none; |
|
||||||
_.commands.OnVotingAdded(self).Disconnect(); |
|
||||||
_.commands.OnVotingRemoved(self).Disconnect(); |
|
||||||
_.chat.OnVoiceMessage(self).Disconnect(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function BuildData(CommandDataBuilder builder) { |
|
||||||
builder.Group(P("core")); |
|
||||||
builder.Summary(P("Allows players to initiate any available voting." |
|
||||||
@ "Voting options themselves are specified as sub-commands.")); |
|
||||||
builder.Describe(P("Default command simply displaces information about current vote.")); |
|
||||||
|
|
||||||
dataBuilder.SubCommand(P("yes")); |
|
||||||
builder.Describe(P("Vote `yes` on the current vote.")); |
|
||||||
dataBuilder.SubCommand(P("no")); |
|
||||||
builder.Describe(P("Vote `no` on the current vote.")); |
|
||||||
|
|
||||||
builder.Option(P("force")); |
|
||||||
builder.Describe(P("Tries to force voting to end immediately with the desired result.")); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Executed( |
|
||||||
CallData arguments, |
|
||||||
EPlayer instigator, |
|
||||||
CommandPermissions permissions |
|
||||||
) { |
|
||||||
local bool forcingVoting; |
|
||||||
local VotingModel.ForceEndingType forceType; |
|
||||||
local Voting currentVoting; |
|
||||||
|
|
||||||
forcingVoting = arguments.options.HasKey(P("force")); |
|
||||||
currentVoting = _.commands.GetCurrentVoting(); |
|
||||||
if (arguments.subCommandName.IsEmpty()) { |
|
||||||
DisplayInfoAboutVoting(instigator, currentVoting); |
|
||||||
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) { |
|
||||||
CastVote(currentVoting, instigator, true); |
|
||||||
forceType = FET_Success; |
|
||||||
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) { |
|
||||||
CastVote(currentVoting, instigator, false); |
|
||||||
forceType = FET_Failure; |
|
||||||
} else if (StartVoting(arguments, currentVoting, instigator)) { |
|
||||||
_.memory.Free(currentVoting); |
|
||||||
currentVoting = _.commands.GetCurrentVoting(); |
|
||||||
forceType = FET_Success; |
|
||||||
} else { |
|
||||||
forcingVoting = false; |
|
||||||
} |
|
||||||
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(currentVoting); |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds sub-command information about given voting with a given name. |
|
||||||
public final function AddVotingInfo(class<Voting> processClass, Text processName) { |
|
||||||
if (processName == none) return; |
|
||||||
if (processClass == none) return; |
|
||||||
if (dataBuilder == none) return; |
|
||||||
|
|
||||||
dataBuilder.SubCommand(processName); |
|
||||||
processClass.static.AddInfo(dataBuilder); |
|
||||||
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. |
|
||||||
public final function ResetVotingInfo() { |
|
||||||
_.memory.Free(dataBuilder); |
|
||||||
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
|
||||||
BuildData(dataBuilder); |
|
||||||
commandData = dataBuilder.BorrowData(); |
|
||||||
} |
|
||||||
|
|
||||||
private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) { |
|
||||||
if (currentVoting == none) { |
|
||||||
callerConsole.WriteLine(P("No voting is active right now.")); |
|
||||||
} else { |
|
||||||
currentVoting.PrintVotingInfoFor(instigator); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) { |
|
||||||
if (currentVoting != none) { |
|
||||||
currentVoting.CastVote(voter, voteForSuccess); |
|
||||||
} else { |
|
||||||
callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now.")); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes all arguments aren't `none`. |
|
||||||
private final function bool StartVoting( |
|
||||||
CallData arguments, |
|
||||||
Voting currentVoting, |
|
||||||
EPlayer instigator |
|
||||||
) { |
|
||||||
local Voting newVoting; |
|
||||||
local User callerUser; |
|
||||||
local CommandAPI.VotingConfigInfo pair; |
|
||||||
local CommandAPI.StartVotingResult result; |
|
||||||
|
|
||||||
callerUser = instigator.GetIdentity(); |
|
||||||
pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser); |
|
||||||
_.memory.Free(callerUser); |
|
||||||
if (pair.votingClass == none) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextFailure) |
|
||||||
.Write(P("Unknown voting option \"")) |
|
||||||
.Write(arguments.subCommandName) |
|
||||||
.WriteLine(P("\"")); |
|
||||||
return false; |
|
||||||
} |
|
||||||
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 |
|
||||||
.UseColor(_.color.TextWarning) |
|
||||||
.WriteLine(P("Another voting is already in progress!")); |
|
||||||
return false; |
|
||||||
} |
|
||||||
if (result == SVR_NoVoters) { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextWarning) |
|
||||||
.WriteLine(P("There are no players eligible for that voting.")); |
|
||||||
return false; |
|
||||||
} |
|
||||||
// Cast a vote from instigator |
|
||||||
newVoting = _.commands.GetCurrentVoting(); |
|
||||||
if (newVoting != none) { |
|
||||||
newVoting.CastVote(instigator, true); |
|
||||||
} else { |
|
||||||
callerConsole |
|
||||||
.UseColor(_.color.TextFailure) |
|
||||||
.WriteLine(P("Voting should be available, but it isn't." |
|
||||||
@ "This is unexpected, something broke terribly.")); |
|
||||||
_.memory.Free(newVoting); |
|
||||||
return false; |
|
||||||
} |
|
||||||
_.memory.Free(newVoting); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
preferredName = "vote" |
|
||||||
} |
|
@ -1,805 +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 Command extends AcediaObject |
|
||||||
dependson(BaseText); |
|
||||||
|
|
||||||
//! This class is meant to represent a command type. |
|
||||||
//! |
|
||||||
//! Command class provides an automated way to add a command to a server through |
|
||||||
//! AcediaCore's features. It takes care of: |
|
||||||
//! |
|
||||||
//! 1. Verifying that player has passed correct (expected parameters); |
|
||||||
//! 2. Parsing these parameters into usable values (both standard, built-in |
|
||||||
//! types like `bool`, `int`, `float`, etc. and more advanced types such |
|
||||||
//! as players lists and JSON values); |
|
||||||
//! 3. Allowing you to easily specify a set of players you are targeting by |
|
||||||
//! supporting several ways to refer to them, such as *by name*, *by id* |
|
||||||
//! and *by selector* (@ and @self refer to caller player, @all refers |
|
||||||
//! to all players). |
|
||||||
//! 4. It can be registered inside AcediaCore's commands feature and be |
|
||||||
//! automatically called through the unified system that supports *chat* |
|
||||||
//! and *mutate* inputs (as well as allowing you to hook in any other |
|
||||||
//! input source); |
|
||||||
//! 5. Will also automatically provide a help page through built-in "help" |
|
||||||
//! command; |
|
||||||
//! 6. Subcommand support - when one command can have several distinct |
|
||||||
//! functions, depending on how its called (e.g. "inventory add" vs |
|
||||||
//! "inventory remove"). These subcommands have a special treatment in |
|
||||||
//! help pages, which makes them more preferable, compared to simply |
|
||||||
//! matching first `Text` argument; |
|
||||||
//! 7. Add support for "options" - additional flags that can modify commands |
|
||||||
//! behavior and behave like usual command options "--force"/"-f". |
|
||||||
//! Their short versions can even be combined: |
|
||||||
//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af". |
|
||||||
//! And they can have their own parameters: "give@all --list sharp". |
|
||||||
//! |
|
||||||
//! # Implementation |
|
||||||
//! |
|
||||||
//! The idea of `Command`'s implementation is simple: command is basically the |
|
||||||
//! `Command.Data` struct that is filled via `CommandDataBuilder`. |
|
||||||
//! Whenever command is called it uses `CommandParser` to parse user's input |
|
||||||
//! based on its `Command.Data` and either report error (in case of failure) or |
|
||||||
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success). |
|
||||||
//! |
|
||||||
//! When command is called is decided by `Commands_Feature` that tracks possible |
|
||||||
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for |
|
||||||
//! adding custom command inputs). That feature basically parses first part of |
|
||||||
//! the command: its name (not the subcommand's names) and target players |
|
||||||
//! (using `PlayersParser`, but only if command is targeted). |
|
||||||
//! |
|
||||||
//! Majority of the command-related code either serves to build `Command.Data` |
|
||||||
//! or to parse command input by using it (`CommandParser`). |
|
||||||
|
|
||||||
/// Possible errors that can arise when parsing command parameters from user |
|
||||||
/// input |
|
||||||
enum ErrorType { |
|
||||||
/// No error |
|
||||||
CET_None, |
|
||||||
/// Bad parser was provided to parse user input (this should not be possible) |
|
||||||
CET_BadParser, |
|
||||||
/// Sub-command name was not specified or was incorrect |
|
||||||
/// (this should not be possible) |
|
||||||
CET_NoSubCommands, |
|
||||||
/// Specified sub-command does not exist |
|
||||||
/// (only relevant when it is enforced for parser, e.g. by an alias) |
|
||||||
CET_BadSubCommand, |
|
||||||
/// Required param for command / option was not specified |
|
||||||
CET_NoRequiredParam, |
|
||||||
CET_NoRequiredParamForOption, |
|
||||||
/// Unknown option key was specified |
|
||||||
CET_UnknownOption, |
|
||||||
/// Unknown short option key was specified |
|
||||||
CET_UnknownShortOption, |
|
||||||
/// Same option appeared twice in one command call |
|
||||||
CET_RepeatedOption, |
|
||||||
/// Part of user's input could not be interpreted as a part of |
|
||||||
/// command's call |
|
||||||
CET_UnusedCommandParameters, |
|
||||||
/// In one short option specification (e.g. '-lah') several options require |
|
||||||
/// parameters: this introduces ambiguity and is not allowed |
|
||||||
CET_MultipleOptionsWithParams, |
|
||||||
/// Targets are specified incorrectly (for targeted commands only) |
|
||||||
CET_IncorrectTargetList, |
|
||||||
// No targets are specified (for targeted commands only) |
|
||||||
CET_EmptyTargetList |
|
||||||
}; |
|
||||||
|
|
||||||
/// Structure that contains all the information about how `Command` was called. |
|
||||||
struct CallData { |
|
||||||
/// Targeted players (if applicable) |
|
||||||
var public array<EPlayer> targetPlayers; |
|
||||||
/// Specified sub-command and parameters/options |
|
||||||
var public Text subCommandName; |
|
||||||
/// Provided parameters and specified options |
|
||||||
var public HashTable parameters; |
|
||||||
var public HashTable options; |
|
||||||
/// Errors that occurred during command call processing are described by |
|
||||||
/// error type. |
|
||||||
var public ErrorType parsingError; |
|
||||||
/// Optional error textual name of the object (parameter, option, etc.) |
|
||||||
/// that caused it. |
|
||||||
var public Text errorCause; |
|
||||||
}; |
|
||||||
|
|
||||||
/// Possible types of parameters. |
|
||||||
enum ParameterType { |
|
||||||
/// Parses into `BoolBox` |
|
||||||
CPT_Boolean, |
|
||||||
/// Parses into `IntBox` |
|
||||||
CPT_Integer, |
|
||||||
/// Parses into `FloatBox` |
|
||||||
CPT_Number, |
|
||||||
/// Parses into `Text` |
|
||||||
CPT_Text, |
|
||||||
/// Special parameter that consumes the rest of the input into `Text` |
|
||||||
CPT_Remainder, |
|
||||||
/// Parses into `HashTable` |
|
||||||
CPT_Object, |
|
||||||
/// Parses into `ArrayList` |
|
||||||
CPT_Array, |
|
||||||
/// Parses into any JSON value |
|
||||||
CPT_JSON, |
|
||||||
/// Parses into an array of specified players |
|
||||||
CPT_Players |
|
||||||
}; |
|
||||||
|
|
||||||
/// Possible forms a boolean variable can be used as. |
|
||||||
/// Boolean parameter can define it's preferred format, which will be used for |
|
||||||
/// help page generation. |
|
||||||
enum PreferredBooleanFormat { |
|
||||||
PBF_TrueFalse, |
|
||||||
PBF_EnableDisable, |
|
||||||
PBF_OnOff, |
|
||||||
PBF_YesNo |
|
||||||
}; |
|
||||||
|
|
||||||
// Defines a singular command parameter |
|
||||||
struct Parameter { |
|
||||||
/// Display name (for the needs of help page displaying) |
|
||||||
var Text displayName; |
|
||||||
/// Type of value this parameter would store |
|
||||||
var ParameterType type; |
|
||||||
/// Does it take only a singular value or can it contain several of them, |
|
||||||
/// written in a list |
|
||||||
var bool allowsList; |
|
||||||
/// Variable name that will be used as a key to store parameter's value |
|
||||||
var Text variableName; |
|
||||||
/// (For `CPT_Boolean` type variables only) - preferred boolean format, |
|
||||||
/// used in help pages |
|
||||||
var PreferredBooleanFormat booleanFormat; |
|
||||||
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some |
|
||||||
/// source during parsing. |
|
||||||
/// For command to attempt that, this field must be not-`none` and contain |
|
||||||
/// the name of the alias source (either "weapon", "color", "feature", |
|
||||||
/// "entity" or some kind of custom alias source name). |
|
||||||
/// |
|
||||||
/// Only relevant when given value is prefixed with "$" character. |
|
||||||
var Text aliasSourceName; |
|
||||||
}; |
|
||||||
|
|
||||||
/// Defines a sub-command of a this command |
|
||||||
/// (specified as "<command> <sub_command>"). |
|
||||||
/// |
|
||||||
/// Using sub-command is not optional, but if none defined |
|
||||||
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`) |
|
||||||
/// one is automatically created / used. |
|
||||||
struct SubCommand { |
|
||||||
/// Name of the sub command. Cannot be `none`. |
|
||||||
var Text name; |
|
||||||
/// Human-readable description of the subcommand. Can be `none`. |
|
||||||
var Text description; |
|
||||||
/// List of required parameters of this [`Command`]. |
|
||||||
var array<Parameter> required; |
|
||||||
/// List of optional parameters of this [`Command`]. |
|
||||||
var array<Parameter> optional; |
|
||||||
}; |
|
||||||
|
|
||||||
/// Defines command's option (options are specified by "--long" or "-l"). |
|
||||||
/// Options are independent from sub-commands. |
|
||||||
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; |
|
||||||
/// [`Option`]'s full name, e.g. "--force". |
|
||||||
var Text longName; |
|
||||||
/// Human-readable description of the option. Can be `none`. |
|
||||||
var Text description; |
|
||||||
/// List of required parameters of this [`Command::Option`]. |
|
||||||
var array<Parameter> required; |
|
||||||
/// List of required parameters of this [`Command::Option`]. |
|
||||||
var array<Parameter> optional; |
|
||||||
}; |
|
||||||
|
|
||||||
/// Structure that defines what sub-commands and options command has |
|
||||||
/// (and what parameters they take) |
|
||||||
struct Data { |
|
||||||
/// Command group this command belongs to |
|
||||||
var protected Text group; |
|
||||||
/// Short summary of what command does (recommended to |
|
||||||
/// keep it to 80 characters) |
|
||||||
var protected Text summary; |
|
||||||
/// Available subcommands. |
|
||||||
var protected array<SubCommand> subCommands; |
|
||||||
/// Available options, common to all subcommands. |
|
||||||
var protected array<Option> options; |
|
||||||
/// `true` iff related [`Command`] targets players. |
|
||||||
var protected bool requiresTarget; |
|
||||||
}; |
|
||||||
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 |
|
||||||
// of `Command`, so we will simply store and reuse one created instance. |
|
||||||
var private Command mainInstance; |
|
||||||
|
|
||||||
/// When command is being executed we create several instances of |
|
||||||
/// `ConsoleWriter` that can be used for command output. |
|
||||||
/// They will also be automatically deallocated once command is executed. |
|
||||||
/// |
|
||||||
/// DO NOT modify them or deallocate any of them manually. |
|
||||||
/// |
|
||||||
/// This should make output more convenient and standardized. |
|
||||||
/// |
|
||||||
/// 1. `publicConsole` - sends messages to all present players; |
|
||||||
/// 2. `callerConsole` - sends messages to the player that called the command; |
|
||||||
/// 3. `targetConsole` - sends messages to the player that is currently being |
|
||||||
/// targeted (different each call of `ExecutedFor()` and `none` during |
|
||||||
/// `Executed()` call); |
|
||||||
/// 4. `othersConsole` - sends messaged to every player that is neither |
|
||||||
/// "caller" or "target". |
|
||||||
var protected ConsoleWriter publicConsole, othersConsole; |
|
||||||
var protected ConsoleWriter callerConsole, targetConsole; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
local CommandDataBuilder dataBuilder; |
|
||||||
|
|
||||||
if (permissionsConfigClass != none) { |
|
||||||
permissionsConfigClass.static.Initialize(); |
|
||||||
} |
|
||||||
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
|
||||||
// Let user fill-in the rest |
|
||||||
BuildData(dataBuilder); |
|
||||||
commandData = dataBuilder.BorrowData(); |
|
||||||
dataBuilder.FreeSelf(); |
|
||||||
dataBuilder = none; |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
local int i; |
|
||||||
local array<SubCommand> subCommands; |
|
||||||
local array<Option> options; |
|
||||||
|
|
||||||
DeallocateConsoles(); |
|
||||||
_.memory.Free(usedName); |
|
||||||
_.memory.Free(commandData.summary); |
|
||||||
usedName = none; |
|
||||||
commandData.summary = none; |
|
||||||
subCommands = commandData.subCommands; |
|
||||||
for (i = 0; i < options.length; i += 1) { |
|
||||||
_.memory.Free(subCommands[i].name); |
|
||||||
_.memory.Free(subCommands[i].description); |
|
||||||
CleanParameters(subCommands[i].required); |
|
||||||
CleanParameters(subCommands[i].optional); |
|
||||||
subCommands[i].required.length = 0; |
|
||||||
subCommands[i].optional.length = 0; |
|
||||||
} |
|
||||||
commandData.subCommands.length = 0; |
|
||||||
options = commandData.options; |
|
||||||
for (i = 0; i < options.length; i += 1) { |
|
||||||
_.memory.Free(options[i].longName); |
|
||||||
_.memory.Free(options[i].description); |
|
||||||
CleanParameters(options[i].required); |
|
||||||
CleanParameters(options[i].optional); |
|
||||||
options[i].required.length = 0; |
|
||||||
options[i].optional.length = 0; |
|
||||||
} |
|
||||||
commandData.options.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
/// Initializes command, providing it with a specific name. |
|
||||||
/// |
|
||||||
/// 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(); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/// Overload this method to use `builder` to define parameters and options for |
|
||||||
/// your command. |
|
||||||
protected function BuildData(CommandDataBuilder builder){} |
|
||||||
|
|
||||||
/// Overload this method to perform required actions when your command is |
|
||||||
/// called. |
|
||||||
/// |
|
||||||
/// [`arguments`] is a `struct` filled with parameters that your command has |
|
||||||
/// been called with. Guaranteed to not be in error state. |
|
||||||
/// [`instigator`] is a player that instigated this execution. |
|
||||||
/// [`permissions`] is a config with permissions for this command call. |
|
||||||
protected function Executed( |
|
||||||
CallData arguments, |
|
||||||
EPlayer instigator, |
|
||||||
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 |
|
||||||
/// for each. |
|
||||||
/// |
|
||||||
/// If your command does not require a target - this method will not be called. |
|
||||||
/// |
|
||||||
/// [`target`] is a player that this command must perform an action on. |
|
||||||
/// [`arguments`] is a `struct` filled with parameters that your command has |
|
||||||
/// been called with. Guaranteed to not be in error state. |
|
||||||
/// [`instigator`] is a player that instigated this execution. |
|
||||||
/// [`permissions`] is a config with permissions for this command call. |
|
||||||
protected function ExecutedFor( |
|
||||||
EPlayer target, |
|
||||||
CallData arguments, |
|
||||||
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() { |
|
||||||
if (default.mainInstance == none) { |
|
||||||
default.mainInstance = Command(__().memory.Allocate(default.class)); |
|
||||||
} |
|
||||||
return default.mainInstance; |
|
||||||
} |
|
||||||
|
|
||||||
/// Forces command to process (parse) player's input, producing a structure with |
|
||||||
/// parsed data in Acedia's format instead. |
|
||||||
/// |
|
||||||
/// Use `Execute()` for actually performing command's actions. |
|
||||||
/// |
|
||||||
/// [`subCommandName`] can be optionally specified to use as sub-command. |
|
||||||
/// If this argument's value is `none` - sub-command name will be parsed from |
|
||||||
/// the `parser`'s data. |
|
||||||
/// |
|
||||||
/// Returns `CallData` structure that contains all the information about |
|
||||||
/// parameters specified in `parser`'s contents. |
|
||||||
/// Returned structure contains objects that must be deallocated, which can |
|
||||||
/// easily be done by the auxiliary `DeallocateCallData()` method. |
|
||||||
public final function CallData ParseInputWith( |
|
||||||
Parser parser, |
|
||||||
EPlayer callerPlayer, |
|
||||||
optional BaseText subCommandName |
|
||||||
) { |
|
||||||
local array<EPlayer> targetPlayers; |
|
||||||
local CommandParser commandParser; |
|
||||||
local CallData callData; |
|
||||||
|
|
||||||
if (parser == none || !parser.Ok()) { |
|
||||||
callData.parsingError = CET_BadParser; |
|
||||||
return callData; |
|
||||||
} |
|
||||||
// Parse targets and handle errors that can arise here |
|
||||||
if (commandData.requiresTarget) { |
|
||||||
targetPlayers = ParseTargets(parser, callerPlayer); |
|
||||||
if (!parser.Ok()) { |
|
||||||
callData.parsingError = CET_IncorrectTargetList; |
|
||||||
return callData; |
|
||||||
} |
|
||||||
if (targetPlayers.length <= 0) { |
|
||||||
callData.parsingError = CET_EmptyTargetList; |
|
||||||
return callData; |
|
||||||
} |
|
||||||
} |
|
||||||
// Parse parameters themselves |
|
||||||
commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); |
|
||||||
callData = commandParser.ParseWith( |
|
||||||
parser, |
|
||||||
commandData, |
|
||||||
callerPlayer, |
|
||||||
subCommandName); |
|
||||||
callData.targetPlayers = targetPlayers; |
|
||||||
commandParser.FreeSelf(); |
|
||||||
return callData; |
|
||||||
} |
|
||||||
|
|
||||||
/// Executes caller `Command` with data provided by `callData` if it is in |
|
||||||
/// a correct state and reports error to `callerPlayer` if `callData` is |
|
||||||
/// invalid. |
|
||||||
/// |
|
||||||
/// Returns `true` if command was successfully executed and `false` otherwise. |
|
||||||
/// Execution is considered successful if `Execute()` call was made, regardless |
|
||||||
/// of whether `Command` can actually perform required action. |
|
||||||
/// For example, giving a weapon to a player can fail because he does not have |
|
||||||
/// enough space in his inventory, but it will still be considered a successful |
|
||||||
/// execution as far as return value is concerned. |
|
||||||
/// |
|
||||||
/// [`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 array<EPlayer> targetPlayers; |
|
||||||
|
|
||||||
if (callerPlayer == none) return false; |
|
||||||
if (!callerPlayer.IsExistent()) return false; |
|
||||||
|
|
||||||
// Report or execute |
|
||||||
if (callData.parsingError != CET_None) { |
|
||||||
ReportError(callData, callerPlayer); |
|
||||||
return false; |
|
||||||
} |
|
||||||
targetPlayers = callData.targetPlayers; |
|
||||||
publicConsole = _.console.ForAll(); |
|
||||||
callerConsole = _.console.For(callerPlayer); |
|
||||||
callerConsole |
|
||||||
.Write(P("Executing command `")) |
|
||||||
.Write(usedName) |
|
||||||
.Say(P("`")); |
|
||||||
// `othersConsole` should also exist in time for `Executed()` call |
|
||||||
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); |
|
||||||
Executed(callData, callerPlayer, permissions); |
|
||||||
_.memory.Free(othersConsole); |
|
||||||
if (commandData.requiresTarget) { |
|
||||||
for (i = 0; i < targetPlayers.length; i += 1) { |
|
||||||
targetConsole = _.console.For(targetPlayers[i]); |
|
||||||
othersConsole = _.console |
|
||||||
.ForAll() |
|
||||||
.ButPlayer(callerPlayer) |
|
||||||
.ButPlayer(targetPlayers[i]); |
|
||||||
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions); |
|
||||||
_.memory.Free(othersConsole); |
|
||||||
_.memory.Free(targetConsole); |
|
||||||
} |
|
||||||
} |
|
||||||
othersConsole = none; |
|
||||||
targetConsole = none; |
|
||||||
DeallocateConsoles(); |
|
||||||
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() { |
|
||||||
if (publicConsole != none && publicConsole.IsAllocated()) { |
|
||||||
_.memory.Free(publicConsole); |
|
||||||
} |
|
||||||
if (callerConsole != none && callerConsole.IsAllocated()) { |
|
||||||
_.memory.Free(callerConsole); |
|
||||||
} |
|
||||||
if (targetConsole != none && targetConsole.IsAllocated()) { |
|
||||||
_.memory.Free(targetConsole); |
|
||||||
} |
|
||||||
if (othersConsole != none && othersConsole.IsAllocated()) { |
|
||||||
_.memory.Free(othersConsole); |
|
||||||
} |
|
||||||
publicConsole = none; |
|
||||||
callerConsole = none; |
|
||||||
targetConsole = none; |
|
||||||
othersConsole = none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Reports given error to the `callerPlayer`, appropriately picking |
|
||||||
/// message color |
|
||||||
private final function ReportError(CallData callData, EPlayer callerPlayer) { |
|
||||||
local Text errorMessage; |
|
||||||
local ConsoleWriter console; |
|
||||||
|
|
||||||
if (callerPlayer == none) return; |
|
||||||
if (!callerPlayer.IsExistent()) return; |
|
||||||
|
|
||||||
// Setup console color |
|
||||||
console = callerPlayer.BorrowConsole(); |
|
||||||
if (callData.parsingError == CET_EmptyTargetList) { |
|
||||||
console.UseColor(_.color.textWarning); |
|
||||||
} else { |
|
||||||
console.UseColor(_.color.textFailure); |
|
||||||
} |
|
||||||
// Send message |
|
||||||
errorMessage = PrintErrorMessage(callData); |
|
||||||
console.Say(errorMessage); |
|
||||||
errorMessage.FreeSelf(); |
|
||||||
// Restore console color |
|
||||||
console.ResetColor().Flush(); |
|
||||||
} |
|
||||||
|
|
||||||
private final function Text PrintErrorMessage(CallData callData) { |
|
||||||
local Text result; |
|
||||||
local MutableText builder; |
|
||||||
|
|
||||||
builder = _.text.Empty(); |
|
||||||
switch (callData.parsingError) { |
|
||||||
case CET_BadParser: |
|
||||||
builder.Append(P("Internal error occurred: invalid parser")); |
|
||||||
break; |
|
||||||
case CET_NoSubCommands: |
|
||||||
builder.Append(P("Ill defined command: no subcommands")); |
|
||||||
break; |
|
||||||
case CET_BadSubCommand: |
|
||||||
builder |
|
||||||
.Append(P("Ill defined sub-command: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_NoRequiredParam: |
|
||||||
builder |
|
||||||
.Append(P("Missing required parameter: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_NoRequiredParamForOption: |
|
||||||
builder |
|
||||||
.Append(P("Missing required parameter for option: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_UnknownOption: |
|
||||||
builder |
|
||||||
.Append(P("Invalid option specified: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_UnknownShortOption: |
|
||||||
builder.Append(P("Invalid short option specified")); |
|
||||||
break; |
|
||||||
case CET_RepeatedOption: |
|
||||||
builder |
|
||||||
.Append(P("Option specified several times: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_UnusedCommandParameters: |
|
||||||
builder.Append(P("Part of command could not be parsed: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_MultipleOptionsWithParams: |
|
||||||
builder |
|
||||||
.Append(P("Multiple short options in one declarations require parameters: ")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_IncorrectTargetList: |
|
||||||
builder |
|
||||||
.Append(P("Target players are incorrectly specified.")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
case CET_EmptyTargetList: |
|
||||||
builder |
|
||||||
.Append(P("List of target players is empty")) |
|
||||||
.Append(callData.errorCause); |
|
||||||
break; |
|
||||||
default: |
|
||||||
} |
|
||||||
result = builder.Copy(); |
|
||||||
builder.FreeSelf(); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
// Auxiliary method for parsing list of targeted players. |
|
||||||
// Assumes given parser is not `none` and not in a failed state. |
|
||||||
// If parsing failed, guaranteed to return an empty array. |
|
||||||
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) { |
|
||||||
local array<EPlayer> targetPlayers; |
|
||||||
local PlayersParser targetsParser; |
|
||||||
|
|
||||||
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); |
|
||||||
targetsParser.SetSelf(callerPlayer); |
|
||||||
targetsParser.ParseWith(parser); |
|
||||||
if (parser.Ok()) { |
|
||||||
targetPlayers = targetsParser.GetPlayers(); |
|
||||||
} |
|
||||||
targetsParser.FreeSelf(); |
|
||||||
return targetPlayers; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
preferredName = "" |
|
||||||
permissionsConfigClass = none |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -1,939 +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 CommandDataBuilder extends AcediaObject |
|
||||||
dependson(Command); |
|
||||||
|
|
||||||
//! This is an auxiliary class for convenient creation of [`Command::Data`] |
|
||||||
//! using a builder pattern. |
|
||||||
//! |
|
||||||
//! ## Implementation |
|
||||||
//! |
|
||||||
//! We will store all defined data in two ways: |
|
||||||
//! |
|
||||||
//! 1. Selected data: data about parameters for subcommand/option that is |
|
||||||
//! currently being filled; |
|
||||||
//! 2. Prepared data: data that was already filled as "selected data" then |
|
||||||
//! stored in these records. Whenever we want to switch to filling another |
|
||||||
//! subcommand/option or return already prepared data we must dump |
|
||||||
//! "selected data" into "prepared data" first and then return the latter. |
|
||||||
//! |
|
||||||
//! Builder object is automatically created when new `Command` instance is |
|
||||||
//! allocated and doesn't normally need to be allocated by hand. |
|
||||||
|
|
||||||
// "Prepared data" |
|
||||||
var private Text commandName, commandGroup; |
|
||||||
var private Text commandSummary; |
|
||||||
var private array<Command.SubCommand> subcommands; |
|
||||||
var private array<Command.Option> options; |
|
||||||
var private bool requiresTarget; |
|
||||||
|
|
||||||
// Auxiliary arrays signifying that we've started adding optional parameters |
|
||||||
// into appropriate `subcommands` and `options`. |
|
||||||
// |
|
||||||
// All optional parameters must follow strictly after required parameters and |
|
||||||
// so, after user have started adding optional parameters to subcommand/option, |
|
||||||
// we prevent them from adding required ones (to that particular |
|
||||||
// command/option). |
|
||||||
var private array<byte> subcommandsIsOptional; |
|
||||||
var private array<byte> optionsIsOptional; |
|
||||||
|
|
||||||
// "Selected data" |
|
||||||
// `false` means we have selected sub-command, `true` - option |
|
||||||
var private bool selectedItemIsOption; |
|
||||||
// `name` for sub-commands, `longName` for options |
|
||||||
var private Text selectedItemName; |
|
||||||
// Description of selected sub-command/option |
|
||||||
var private Text selectedDescription; |
|
||||||
// Are we filling optional parameters (`true`)? Or required ones (`false`)? |
|
||||||
var private bool selectionIsOptional; |
|
||||||
// Array of parameters we are currently filling (either required or optional) |
|
||||||
var private array<Command.Parameter> selectedParameterArray; |
|
||||||
|
|
||||||
var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong; |
|
||||||
var private LoggerAPI.Definition warnSameLongName, warnSameShortName; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
// Fill empty subcommand (no special key word) by default |
|
||||||
SubCommand(P("")); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
subcommands.length = 0; |
|
||||||
subcommandsIsOptional.length = 0; |
|
||||||
options.length = 0; |
|
||||||
optionsIsOptional.length = 0; |
|
||||||
selectedParameterArray.length = 0; |
|
||||||
commandName = none; |
|
||||||
commandGroup = none; |
|
||||||
commandSummary = none; |
|
||||||
selectedItemName = none; |
|
||||||
selectedDescription = none; |
|
||||||
requiresTarget = false; |
|
||||||
selectedItemIsOption = false; |
|
||||||
selectionIsOptional = false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Method that starts defining a new sub-command. |
|
||||||
/// |
|
||||||
/// Creates new sub-command with a given name (if it's missing) and then selects |
|
||||||
/// sub-command with a given name to add parameters to. |
|
||||||
/// |
|
||||||
/// [`name`] defines name of the sub-command user wants, case-sensitive. |
|
||||||
/// If `none` is passed, this method will do nothing. |
|
||||||
public final function SubCommand(BaseText name) { |
|
||||||
local int subcommandIndex; |
|
||||||
|
|
||||||
if (name == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
RecordSelection(); |
|
||||||
subcommandIndex = FindSubCommandIndex(name); |
|
||||||
if (subcommandIndex < 0) { |
|
||||||
MakeEmptySelection(name, false); |
|
||||||
return; |
|
||||||
} |
|
||||||
// Load appropriate prepared data, if it exists for |
|
||||||
// sub-command with name `name` |
|
||||||
selectedItemIsOption = false; |
|
||||||
selectedItemName = subcommands[subcommandIndex].name; |
|
||||||
selectedDescription = subcommands[subcommandIndex].description; |
|
||||||
selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0; |
|
||||||
if (selectionIsOptional) { |
|
||||||
selectedParameterArray = subcommands[subcommandIndex].optional; |
|
||||||
} else { |
|
||||||
selectedParameterArray = subcommands[subcommandIndex].required; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Method that starts defining a new option. |
|
||||||
/// |
|
||||||
/// This method checks if some of the recorded options are in conflict with |
|
||||||
/// given `longName` and `shortName` (already using one and only one of them). |
|
||||||
/// In case there is no conflict, it creates new option with specified long and |
|
||||||
/// short names (if such option is missing) and selects option with a long name |
|
||||||
/// `longName` to add parameters to. |
|
||||||
/// |
|
||||||
/// [`longName`] defines long name of the option, case-sensitive (for using |
|
||||||
/// an option in form "--..."). Must be at least two characters long. |
|
||||||
/// [`shortName`] defines short name of the option, case-sensitive (for using |
|
||||||
/// an option in form "-..."). Must be exactly one character. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// Errors will be logged in case either of arguments are `none`, have |
|
||||||
/// inappropriate length or are in conflict with each other. |
|
||||||
public final function Option(BaseText longName, optional BaseText shortName) { |
|
||||||
local int optionIndex; |
|
||||||
local BaseText.Character shortNameAsCharacter; |
|
||||||
|
|
||||||
// Unlike for `SubCommand()`, we need to ensure that option naming is |
|
||||||
// correct and does not conflict with existing options |
|
||||||
// (user might attempt to add two options with same long names and |
|
||||||
// different short ones). |
|
||||||
shortNameAsCharacter = GetValidShortName(longName, shortName); |
|
||||||
if ( !_.text.IsValidCharacter(shortNameAsCharacter) |
|
||||||
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) { |
|
||||||
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()` |
|
||||||
// are responsible for logging warnings/errors |
|
||||||
return; |
|
||||||
} |
|
||||||
SelectOption(longName); |
|
||||||
// Set short name for new options |
|
||||||
optionIndex = FindOptionIndex(longName); |
|
||||||
if (optionIndex < 0) { |
|
||||||
// We can only be here if option was created for the first time |
|
||||||
RecordSelection(); |
|
||||||
// So now it cannot fail |
|
||||||
optionIndex = FindOptionIndex(longName); |
|
||||||
options[optionIndex].shortName = shortNameAsCharacter; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds description to the selected sub-command / option. |
|
||||||
/// |
|
||||||
/// Highlights parts of the description in-between "`" characters. |
|
||||||
/// |
|
||||||
/// Does nothing if nothing is yet selected. |
|
||||||
public final function Describe(BaseText description) { |
|
||||||
local int fromIndex, toIndex; |
|
||||||
local BaseText.Formatting keyWordFormatting; |
|
||||||
local bool lookingForEnd; |
|
||||||
local MutableText coloredDescription; |
|
||||||
|
|
||||||
if (description == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis); |
|
||||||
coloredDescription = description.MutableCopy(); |
|
||||||
while (true) { |
|
||||||
if (lookingForEnd) { |
|
||||||
toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1); |
|
||||||
} else { |
|
||||||
fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1); |
|
||||||
} |
|
||||||
if (toIndex < 0 || fromIndex < 0) { |
|
||||||
break; |
|
||||||
} |
|
||||||
if (lookingForEnd) { |
|
||||||
coloredDescription.ChangeFormatting( |
|
||||||
keyWordFormatting, |
|
||||||
fromIndex, |
|
||||||
toIndex - fromIndex + 1); |
|
||||||
lookingForEnd = false; |
|
||||||
} else { |
|
||||||
lookingForEnd = true; |
|
||||||
} |
|
||||||
} |
|
||||||
coloredDescription.Replace(P("`"), P("")); |
|
||||||
if (lookingForEnd) { |
|
||||||
coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex); |
|
||||||
} |
|
||||||
_.memory.Free(selectedDescription); |
|
||||||
selectedDescription = coloredDescription.IntoText(); |
|
||||||
} |
|
||||||
|
|
||||||
/// Sets new group of `Command.Data` under construction. |
|
||||||
/// |
|
||||||
/// Group name is meant to be shared among several commands, allowing user to |
|
||||||
/// filter or fetch commands of a certain group. |
|
||||||
/// Group name is case-insensitive. |
|
||||||
public final function Group(BaseText newName) { |
|
||||||
if (newName != none && newName == commandGroup) { |
|
||||||
return; |
|
||||||
} |
|
||||||
_.memory.Free(commandGroup); |
|
||||||
if (newName != none) { |
|
||||||
commandGroup = newName.Copy(); |
|
||||||
} else { |
|
||||||
commandGroup = none; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Sets new summary of `Command.Data` under construction. |
|
||||||
/// |
|
||||||
/// Summary gives a short description of the command on the whole that will |
|
||||||
/// be displayed when "help" command is listing available command |
|
||||||
public final function Summary(BaseText newSummary) { |
|
||||||
if (newSummary != none && newSummary == commandSummary) { |
|
||||||
return; |
|
||||||
} |
|
||||||
_.memory.Free(commandSummary); |
|
||||||
if (newSummary != none) { |
|
||||||
commandSummary = newSummary.Copy(); |
|
||||||
} else { |
|
||||||
commandSummary = none; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Makes caller builder to mark `Command.Data` under construction to require |
|
||||||
/// a player target. |
|
||||||
public final function RequireTarget() { |
|
||||||
requiresTarget = true; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
/// Any parameters added to currently selected sub-command / option after |
|
||||||
/// calling this method will be marked as optional. |
|
||||||
/// |
|
||||||
/// Further calls when the same sub-command / option is selected will do |
|
||||||
/// nothing. |
|
||||||
public final function OptionalParams() |
|
||||||
{ |
|
||||||
if (selectionIsOptional) { |
|
||||||
return; |
|
||||||
} |
|
||||||
// Record all required parameters first, otherwise there would be no way |
|
||||||
// to distinguish between them and optional parameters |
|
||||||
RecordSelection(); |
|
||||||
selectionIsOptional = true; |
|
||||||
selectedParameterArray.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns data that has been constructed so far by the caller |
|
||||||
/// [`CommandDataBuilder`]. |
|
||||||
/// |
|
||||||
/// Does not reset progress. |
|
||||||
public final function Command.Data BorrowData() { |
|
||||||
local Command.Data newData; |
|
||||||
|
|
||||||
// TODO: is this copying needed? |
|
||||||
RecordSelection(); |
|
||||||
newData.group = commandGroup; |
|
||||||
newData.summary = commandSummary; |
|
||||||
newData.subcommands = subcommands; |
|
||||||
newData.options = options; |
|
||||||
newData.requiresTarget = requiresTarget; |
|
||||||
return newData; |
|
||||||
} |
|
||||||
|
|
||||||
// Adds new parameter to selected sub-command / option |
|
||||||
private final function PushParameter(Command.Parameter newParameter) { |
|
||||||
selectedParameterArray[selectedParameterArray.length] = newParameter; |
|
||||||
} |
|
||||||
|
|
||||||
// Fills `Command.ParameterType` struct with given values |
|
||||||
// (except boolean format). |
|
||||||
// Assumes `displayName != none`. |
|
||||||
private final function Command.Parameter NewParameter( |
|
||||||
BaseText displayName, |
|
||||||
Command.ParameterType parameterType, |
|
||||||
bool isListParameter, |
|
||||||
optional BaseText variableName |
|
||||||
) { |
|
||||||
local Command.Parameter newParameter; |
|
||||||
|
|
||||||
newParameter.displayName = displayName.Copy(); |
|
||||||
newParameter.type = parameterType; |
|
||||||
newParameter.allowsList = isListParameter; |
|
||||||
if (variableName != none) { |
|
||||||
newParameter.variableName = variableName.Copy(); |
|
||||||
} else { |
|
||||||
newParameter.variableName = displayName.Copy(); |
|
||||||
} |
|
||||||
return newParameter; |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new boolean parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`format`] defines preferred format of boolean values. |
|
||||||
/// Command parser will still accept boolean values in any form, this setting |
|
||||||
/// only affects how parameter will be displayed in generated help. |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command |
|
||||||
/// input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamBoolean( |
|
||||||
BaseText name, |
|
||||||
optional Command.PreferredBooleanFormat format, |
|
||||||
optional BaseText variableName |
|
||||||
) { |
|
||||||
local Command.Parameter newParam; |
|
||||||
|
|
||||||
if (name != none) { |
|
||||||
newParam = NewParameter(name, CPT_Boolean, false, variableName); |
|
||||||
newParam.booleanFormat = format; |
|
||||||
PushParameter(newParam); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new integer list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`format`] defines preferred format of boolean values. |
|
||||||
/// Command parser will still accept boolean values in any form, this setting |
|
||||||
/// only affects how |
|
||||||
/// parameter will be displayed in generated help. |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamBooleanList( |
|
||||||
BaseText name, |
|
||||||
optional Command.PreferredBooleanFormat format, |
|
||||||
optional BaseText variableName |
|
||||||
) { |
|
||||||
local Command.Parameter newParam; |
|
||||||
|
|
||||||
if (name != none) { |
|
||||||
newParam = NewParameter(name, CPT_Boolean, true, variableName); |
|
||||||
newParam.booleanFormat = format; |
|
||||||
PushParameter(newParam); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new integer parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamInteger(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Integer, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new integer list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter (it would appear in |
|
||||||
/// the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamIntegerList(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Integer, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new numeric parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter (it would appear in the |
|
||||||
/// generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamNumber(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Number, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new numeric list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter (it would appear in the |
|
||||||
/// generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamNumberList(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Number, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new text parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter (it would appear in the |
|
||||||
/// generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
/// |
|
||||||
/// [`aliasSourceName`] defines name of the alias source that must be used to |
|
||||||
/// auto-resolve this parameter's value. `none` means that parameter will be |
|
||||||
/// recorded as-is, any other value (either "weapon", "color", "feature", |
|
||||||
/// "entity" or some kind of custom alias source name) will make values prefixed |
|
||||||
/// with "$" to be resolved as custom aliases. |
|
||||||
/// In case auto-resolving is used, value will be recorded as a `HasTable` with |
|
||||||
/// two fields: "alias" - value provided by user and (in case "$" prefix was |
|
||||||
/// used) "value" - actual resolved value of an alias. |
|
||||||
/// If alias has failed to be resolved, `none` will be stored as a value. |
|
||||||
public final function ParamText( |
|
||||||
BaseText name, |
|
||||||
optional BaseText variableName, |
|
||||||
optional BaseText aliasSourceName |
|
||||||
) { |
|
||||||
local Command.Parameter newParameterValue; |
|
||||||
|
|
||||||
if (name == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
newParameterValue = NewParameter(name, CPT_Text, false, variableName); |
|
||||||
if (aliasSourceName != none) { |
|
||||||
newParameterValue.aliasSourceName = aliasSourceName.Copy(); |
|
||||||
} |
|
||||||
PushParameter(newParameterValue); |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new text list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter (it would appear in the |
|
||||||
/// generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. If left `none`, - will coincide with |
|
||||||
/// `name` parameter. |
|
||||||
/// |
|
||||||
/// [`aliasSourceName`] defines name of the alias source that must be used to |
|
||||||
/// auto-resolve this parameter's value. `none` means that parameter will be |
|
||||||
/// recorded as-is, any other value (either "weapon", "color", "feature", |
|
||||||
/// "entity" or some kind of custom alias source name) will make values prefixed |
|
||||||
/// with "$" to be resolved as custom aliases. |
|
||||||
/// In case auto-resolving is used, value will be recorded as a `HasTable` with |
|
||||||
/// two fields: "alias" - value provided by user and (in case "$" prefix was |
|
||||||
/// used) "value" - actual resolved value of an alias. |
|
||||||
/// If alias has failed to be resolved, `none` will be stored as a value. |
|
||||||
public final function ParamTextList( |
|
||||||
BaseText name, |
|
||||||
optional BaseText variableName, |
|
||||||
optional BaseText aliasSourceName |
|
||||||
) { |
|
||||||
local Command.Parameter newParameterValue; |
|
||||||
|
|
||||||
if (name == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
newParameterValue = NewParameter(name, CPT_Text, true, variableName); |
|
||||||
if (aliasSourceName != none) { |
|
||||||
newParameterValue.aliasSourceName = aliasSourceName.Copy(); |
|
||||||
} |
|
||||||
PushParameter(newParameterValue); |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new remainder parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// Remainder parameter is a special parameter that will simply consume all |
|
||||||
/// remaining command's input as-is. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamRemainder(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Remainder, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON object parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamObject(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Object, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON object list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamObjectList(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Object, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON array parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamArray(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Array, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON array list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamArrayList(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_Array, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON value parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamJSON(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_JSON, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new JSON value list parameter (required or optional depends on whether |
|
||||||
/// `OptionalParams()` call happened) to the currently selected |
|
||||||
/// sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamJSONList(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_JSON, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
/// Adds new parameter that defines a set of players (required or optional |
|
||||||
/// depends on whether `OptionalParams()` call happened) to the currently |
|
||||||
/// selected sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamPlayers(BaseText name, optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds new parameter that defines a list of sets of players (required or |
|
||||||
/// optional depends on whether `OptionalParams()` call happened) to |
|
||||||
/// the currently selected sub-command / option. |
|
||||||
/// |
|
||||||
/// Only fails if provided `name` is `none`. |
|
||||||
/// |
|
||||||
/// List parameters expect user to enter one or more value of the same type as |
|
||||||
/// command's arguments. |
|
||||||
/// |
|
||||||
/// [`name`] will become the name of the parameter |
|
||||||
/// (it would appear in the generated "help" command info). |
|
||||||
/// |
|
||||||
/// [`variableName`] will become key for this parameter's value in `HashTable` |
|
||||||
/// after user's command input is parsed. |
|
||||||
/// If left `none`, - will coincide with `name` parameter. |
|
||||||
public final function ParamPlayersList(BaseText name,optional BaseText variableName) { |
|
||||||
if (name != none) { |
|
||||||
PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Find index of sub-command with a given name `name` in `subcommands`. |
|
||||||
// `-1` if there's not sub-command with such name. |
|
||||||
// Case-sensitive. |
|
||||||
private final function int FindSubCommandIndex(BaseText name) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (name == none) { |
|
||||||
return -1; |
|
||||||
} |
|
||||||
for (i = 0; i < subcommands.length; i += 1) { |
|
||||||
if (name.Compare(subcommands[i].name)) { |
|
||||||
return i; |
|
||||||
} |
|
||||||
} |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
// Find index of option with a given name `name` in `options`. |
|
||||||
// `-1` if there's not sub-command with such name. |
|
||||||
// Case-sensitive. |
|
||||||
private final function int FindOptionIndex(BaseText longName) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (longName == none) { |
|
||||||
return -1; |
|
||||||
} |
|
||||||
for (i = 0; i < options.length; i += 1) { |
|
||||||
if (longName.Compare(options[i].longName)) { |
|
||||||
return i; |
|
||||||
} |
|
||||||
} |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
// Creates an empty selection record for subcommand or option with name (long name) `name`. |
|
||||||
// Doe not check whether subcommand/option with that name already exists. |
|
||||||
// Copies passed `name`, assumes that it is not `none`. |
|
||||||
private final function MakeEmptySelection(BaseText name, bool selectedOption) { |
|
||||||
selectedItemIsOption = selectedOption; |
|
||||||
selectedItemName = name.Copy(); |
|
||||||
selectedDescription = none; |
|
||||||
selectedParameterArray.length = 0; |
|
||||||
selectionIsOptional = false; |
|
||||||
} |
|
||||||
|
|
||||||
// Select option with a given long name `longName` from `options`. |
|
||||||
// If there is no option with specified `longName` in prepared data - creates new record in |
|
||||||
// selection, otherwise copies previously saved data. |
|
||||||
// Automatically saves previously selected data into prepared data. |
|
||||||
// Copies `name` if it has to create new record. |
|
||||||
private final function SelectOption(BaseText longName) { |
|
||||||
local int optionIndex; |
|
||||||
|
|
||||||
if (longName == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
RecordSelection(); |
|
||||||
optionIndex = FindOptionIndex(longName); |
|
||||||
if (optionIndex < 0) { |
|
||||||
MakeEmptySelection(longName, true); |
|
||||||
return; |
|
||||||
} |
|
||||||
// Load appropriate prepared data, if it exists for |
|
||||||
// option with long name `longName` |
|
||||||
selectedItemIsOption = true; |
|
||||||
selectedItemName = options[optionIndex].longName; |
|
||||||
selectedDescription = options[optionIndex].description; |
|
||||||
selectionIsOptional = optionsIsOptional[optionIndex] > 0; |
|
||||||
if (selectionIsOptional) { |
|
||||||
selectedParameterArray = options[optionIndex].optional; |
|
||||||
} else { |
|
||||||
selectedParameterArray = options[optionIndex].required; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Saves currently selected data into prepared data. |
|
||||||
private final function RecordSelection() { |
|
||||||
if (selectedItemName == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (selectedItemIsOption) { |
|
||||||
RecordSelectedOption(); |
|
||||||
} else { |
|
||||||
RecordSelectedSubCommand(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Saves selected sub-command into prepared records. |
|
||||||
// Assumes that command and not an option is selected. |
|
||||||
private final function RecordSelectedSubCommand() { |
|
||||||
local int selectedSubCommandIndex; |
|
||||||
local Command.SubCommand newSubcommand; |
|
||||||
|
|
||||||
if (selectedItemName == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName); |
|
||||||
if (selectedSubCommandIndex < 0) { |
|
||||||
selectedSubCommandIndex = subcommands.length; |
|
||||||
subcommands[selectedSubCommandIndex] = newSubcommand; |
|
||||||
} |
|
||||||
subcommands[selectedSubCommandIndex].name = selectedItemName; |
|
||||||
subcommands[selectedSubCommandIndex].description = selectedDescription; |
|
||||||
if (selectionIsOptional) { |
|
||||||
subcommands[selectedSubCommandIndex].optional = selectedParameterArray; |
|
||||||
subcommandsIsOptional[selectedSubCommandIndex] = 1; |
|
||||||
} else { |
|
||||||
subcommands[selectedSubCommandIndex].required = selectedParameterArray; |
|
||||||
subcommandsIsOptional[selectedSubCommandIndex] = 0; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Saves currently selected option into prepared records. |
|
||||||
// Assumes that option and not an command is selected. |
|
||||||
private final function RecordSelectedOption() { |
|
||||||
local int selectedOptionIndex; |
|
||||||
local Command.Option newOption; |
|
||||||
|
|
||||||
if (selectedItemName == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
selectedOptionIndex = FindOptionIndex(selectedItemName); |
|
||||||
if (selectedOptionIndex < 0) { |
|
||||||
selectedOptionIndex = options.length; |
|
||||||
options[selectedOptionIndex] = newOption; |
|
||||||
} |
|
||||||
options[selectedOptionIndex].longName = selectedItemName; |
|
||||||
options[selectedOptionIndex].description = selectedDescription; |
|
||||||
if (selectionIsOptional) { |
|
||||||
options[selectedOptionIndex].optional = selectedParameterArray; |
|
||||||
optionsIsOptional[selectedOptionIndex] = 1; |
|
||||||
} else { |
|
||||||
options[selectedOptionIndex].required = selectedParameterArray; |
|
||||||
optionsIsOptional[selectedOptionIndex] = 0; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Validates names (printing errors in case of failure) for the option. |
|
||||||
// Long name must be at least 2 characters long. |
|
||||||
// Short name must be either: |
|
||||||
// 1. exactly one character long; |
|
||||||
// 2. `none`, which leads to deriving `shortName` from `longName` |
|
||||||
// as a first character. |
|
||||||
// Anything else will result in logging a failure and rejection of |
|
||||||
// the option altogether. |
|
||||||
// Returns `none` if validation failed and chosen short name otherwise |
|
||||||
// (if `shortName` was used for it - it's value will be copied). |
|
||||||
private final function BaseText.Character GetValidShortName( |
|
||||||
BaseText longName, |
|
||||||
BaseText shortName |
|
||||||
) { |
|
||||||
// Validate `longName` |
|
||||||
if (longName == none) { |
|
||||||
return _.text.GetInvalidCharacter(); |
|
||||||
} |
|
||||||
if (longName.GetLength() < 2) { |
|
||||||
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy()); |
|
||||||
return _.text.GetInvalidCharacter(); |
|
||||||
} |
|
||||||
// Validate `shortName`, |
|
||||||
// deriving if from `longName` if necessary & possible |
|
||||||
if (shortName == none) { |
|
||||||
return longName.GetCharacter(0); |
|
||||||
} |
|
||||||
if (shortName.IsEmpty() || shortName.GetLength() > 1) { |
|
||||||
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy()); |
|
||||||
return _.text.GetInvalidCharacter(); |
|
||||||
} |
|
||||||
return shortName.GetCharacter(0); |
|
||||||
} |
|
||||||
|
|
||||||
// Checks that if any option record has a long/short name from a given pair of |
|
||||||
// names (`longName`, `shortName`), then it also has another one. |
|
||||||
// |
|
||||||
// i.e. we cannot have several options with identical names: |
|
||||||
// (--silent, -s) and (--sick, -s). |
|
||||||
private final function bool VerifyNoOptionNamingConflict( |
|
||||||
BaseText longName, |
|
||||||
BaseText.Character shortName |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
local bool sameShortNames, sameLongNames; |
|
||||||
|
|
||||||
// To make sure we will search through the up-to-date `options`, |
|
||||||
// record selection into prepared records. |
|
||||||
RecordSelection(); |
|
||||||
for (i = 0; i < options.length; i += 1) { |
|
||||||
sameShortNames = _.text.AreEqual(shortName, options[i].shortName); |
|
||||||
sameLongNames = longName.Compare(options[i].longName); |
|
||||||
if (sameLongNames && !sameShortNames) { |
|
||||||
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
if (!sameLongNames && sameShortNames) { |
|
||||||
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName)); |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties |
|
||||||
{ |
|
||||||
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2") |
|
||||||
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2") |
|
||||||
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.") |
|
||||||
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.") |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -1,983 +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 CommandParser extends AcediaObject |
|
||||||
dependson(Command); |
|
||||||
|
|
||||||
|
|
||||||
//! Class specialized for parsing user input of the command's call into |
|
||||||
//![ `Command.CallData`] structure with the information about all parsed |
|
||||||
//! arguments. |
|
||||||
//! |
|
||||||
//! [`CommandParser`] is not made to parse the whole input: |
|
||||||
//! |
|
||||||
//! * Command's name needs to be parsed and resolved as an alias before using |
|
||||||
//! this parser - it won't do this hob for you; |
|
||||||
//! * List of targeted players must also be parsed using [`PlayersParser`] - |
|
||||||
//! [`CommandParser`] won't do this for you; |
|
||||||
//! * Optionally one can also decide on the referred subcommand and pass it into |
|
||||||
//! [`ParseWith()`] method. If subcommand's name is not passed - |
|
||||||
//! [`CommandParser`] will try to parse it itself. |
|
||||||
//! This feature is used to add support for subcommand aliases. |
|
||||||
//! |
|
||||||
//! However, above steps are handled by [`Commands_Feature`] and one only needs to |
|
||||||
//! call that feature's [`HandleInput()`] methods to pass user input with command |
|
||||||
//! call line there. |
|
||||||
//! |
|
||||||
//! # Usage |
|
||||||
//! |
|
||||||
//! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with: |
|
||||||
//! |
|
||||||
//! 1. [`Parser`], filled with command call input; |
|
||||||
//! 2. Command's data that describes subcommands, options and their parameters |
|
||||||
//! for the command, which call we are parsing; |
|
||||||
//! 3. (Optionally) [`EPlayer`] reference to the player that initiated |
|
||||||
//! the command call; |
|
||||||
//! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`] |
|
||||||
//! from parsing subcommand name itself. Used for implementing aliases that |
|
||||||
//! refer to a particular subcommand. |
|
||||||
//! |
|
||||||
//! # Implementation |
|
||||||
//! |
|
||||||
//! [`CommandParser`] stores both its state and command data, relevant to parsing, |
|
||||||
//! as its member variables during the whole parsing process, instead of passing |
|
||||||
//! that data around in every single method. |
|
||||||
//! |
|
||||||
//! We will give a brief overview of how around 20 parsing methods below are |
|
||||||
//! interconnected. |
|
||||||
//! |
|
||||||
//! The only public method [`ParseWith()`] is used to start parsing and it uses |
|
||||||
//! [`PickSubCommand()`] to first try and figure out what sub command is |
|
||||||
//! intended by user's input. |
|
||||||
//! |
|
||||||
//! Main bulk of the work is done by [`ParseParameterArrays()`] method, for |
|
||||||
//! simplicity broken into two [`ParseRequiredParameterArray()`] and |
|
||||||
//! [`ParseOptionalParameterArray()`] methods that can parse |
|
||||||
//! parameters for both command itself and it's options. |
|
||||||
//! |
|
||||||
//! They go through arrays of required and optional parameters, calling |
|
||||||
//! [`ParseParameter()`] for each parameters, which in turn can make several |
|
||||||
//! calls of [`ParseSingleValue()`] to parse parameters' values: it is called |
|
||||||
//! once for single-valued parameters, but possibly several times for list |
|
||||||
//! parameters that can contain several values. |
|
||||||
//! |
|
||||||
//! So main parsing method looks something like: |
|
||||||
//! |
|
||||||
//! ``` |
|
||||||
//! ParseParameterArrays() { |
|
||||||
//! loop ParseParameter() { |
|
||||||
//! loop ParseSingleValue() |
|
||||||
//! } |
|
||||||
//! } |
|
||||||
//! ``` |
|
||||||
//! |
|
||||||
//! [`ParseSingleValue()`] is essentially that redirects it's method call to |
|
||||||
//! another, more specific, parsing method based on the parameter type. |
|
||||||
//! |
|
||||||
//! Finally, to allow users to specify options at any point in command, we call |
|
||||||
//! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`] |
|
||||||
//! (the only parameter that has higher priority than options is |
|
||||||
//! [`CPT_Remainder`]), since option definition can appear at any place between |
|
||||||
//! parameters. We also call `TryParsingOptions()` *after* we've parsed all |
|
||||||
//! command's parameters, since that case won't be detected by parsing them |
|
||||||
//! *before* every parameter. |
|
||||||
//! |
|
||||||
//! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes |
|
||||||
//! (filtering out negative numeric values) and then redirect the call to either |
|
||||||
//! of more specialized methods: [`ParseLongOption()`] or |
|
||||||
//! [`ParseShortOption()`], that can in turn make another |
|
||||||
//! [`ParseParameterArrays()`] call, if specified option has parameters. |
|
||||||
//! |
|
||||||
//! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option |
|
||||||
//! declaration always interrupts previous option's parameter list. |
|
||||||
//! Rest of the methods perform simple auxiliary functions. |
|
||||||
|
|
||||||
// Describes which parameters we are currently parsing, classifying them |
|
||||||
// as either "necessary" or "extra". |
|
||||||
// |
|
||||||
// E.g. if last require parameter is a list of integers, |
|
||||||
// then after parsing first integer we are: |
|
||||||
// |
|
||||||
// * Still parsing required *parameter* "integer list"; |
|
||||||
// * But no more integers are *necessary* for successful parsing. |
|
||||||
// |
|
||||||
// Therefore we consider parameter "necessary" if the lack of it will |
|
||||||
// result in failed parsing and "extra" otherwise. |
|
||||||
enum ParsingTarget { |
|
||||||
// We are in the process of parsing required parameters, that must all |
|
||||||
// be present. |
|
||||||
// This case does not include parsing last required parameter: it needs |
|
||||||
// to be treated differently to track when we change from "necessary" to |
|
||||||
// "extra" parameters. |
|
||||||
CPT_NecessaryParameter, |
|
||||||
// We are parsing last necessary parameter. |
|
||||||
CPT_LastNecessaryParameter, |
|
||||||
// We are not parsing extra parameters that can be safely omitted. |
|
||||||
CPT_ExtraParameter, |
|
||||||
}; |
|
||||||
|
|
||||||
// Parser filled with user input. |
|
||||||
var private Parser commandParser; |
|
||||||
// Data for sub-command specified by both command we are parsing |
|
||||||
// and user's input; determined early during parsing. |
|
||||||
var private Command.SubCommand pickedSubCommand; |
|
||||||
// Options available for the command we are parsing. |
|
||||||
var private array<Command.Option> availableOptions; |
|
||||||
// Result variable we are filling during the parsing process, |
|
||||||
// should be `none` outside of [`self.ParseWith()`] method call. |
|
||||||
var private Command.CallData nextResult; |
|
||||||
|
|
||||||
// Parser for player parameters, setup with a caller for current parsing |
|
||||||
var private PlayersParser currentPlayersParser; |
|
||||||
// Current [`ParsingTarget`], see it's enum description for more details |
|
||||||
var private ParsingTarget currentTarget; |
|
||||||
// `true` means we are parsing parameters for a command's option and |
|
||||||
// `false` means we are parsing command's own parameters |
|
||||||
var private bool currentTargetIsOption; |
|
||||||
// If we are parsing parameters for an option (`currentTargetIsOption == true`) |
|
||||||
// this variable will store that option's data. |
|
||||||
var private Command.Option targetOption; |
|
||||||
// Last successful state of [`commandParser`]. |
|
||||||
var Parser.ParserState confirmedState; |
|
||||||
// Options we have so far encountered during parsing, necessary since we want |
|
||||||
// to forbid specifying th same option more than once. |
|
||||||
var private array<Command.Option> usedOptions; |
|
||||||
|
|
||||||
// Literals that can be used as boolean values |
|
||||||
var private array<string> booleanTrueEquivalents; |
|
||||||
var private array<string> booleanFalseEquivalents; |
|
||||||
|
|
||||||
var LoggerAPI.Definition errNoSubCommands; |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
Reset(); |
|
||||||
} |
|
||||||
|
|
||||||
/// 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 |
|
||||||
private final function Reset() { |
|
||||||
local Command.CallData blankCallData; |
|
||||||
|
|
||||||
_.memory.Free(currentPlayersParser); |
|
||||||
currentPlayersParser = none; |
|
||||||
// We didn't create this one and are not meant to free it either |
|
||||||
commandParser = none; |
|
||||||
nextResult = blankCallData; |
|
||||||
currentTarget = CPT_NecessaryParameter; |
|
||||||
currentTargetIsOption = false; |
|
||||||
usedOptions.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
// Auxiliary method for recording errors |
|
||||||
private final function DeclareError(Command.ErrorType type, optional BaseText cause) { |
|
||||||
nextResult.parsingError = type; |
|
||||||
if (cause != none) { |
|
||||||
nextResult.errorCause = cause.Copy(); |
|
||||||
} |
|
||||||
if (commandParser != none) { |
|
||||||
commandParser.Fail(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser != none`, is in successful state. |
|
||||||
// |
|
||||||
// Picks a sub command based on it's contents (parser's pointer must be before |
|
||||||
// where subcommand's name is specified). |
|
||||||
// |
|
||||||
// If [`specifiedSubCommand`] is not `none` - will always use that value instead |
|
||||||
// of parsing it from [`commandParser`]. |
|
||||||
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) { |
|
||||||
local int i; |
|
||||||
local MutableText candidateSubCommandName; |
|
||||||
local Command.SubCommand emptySubCommand; |
|
||||||
local array<Command.SubCommand> allSubCommands; |
|
||||||
|
|
||||||
allSubCommands = commandData.subCommands; |
|
||||||
if (allSubcommands.length == 0) { |
|
||||||
_.logger.Auto(errNoSubCommands).ArgClass(class); |
|
||||||
pickedSubCommand = emptySubCommand; |
|
||||||
return; |
|
||||||
} |
|
||||||
// Get candidate name |
|
||||||
confirmedState = commandParser.GetCurrentState(); |
|
||||||
if (specifiedSubCommand != none) { |
|
||||||
candidateSubCommandName = specifiedSubCommand.MutableCopy(); |
|
||||||
} else { |
|
||||||
commandParser.Skip().MUntil(candidateSubCommandName,, true); |
|
||||||
} |
|
||||||
// Try matching it to sub commands |
|
||||||
pickedSubCommand = allSubcommands[0]; |
|
||||||
if (candidateSubCommandName.IsEmpty()) { |
|
||||||
candidateSubCommandName.FreeSelf(); |
|
||||||
return; |
|
||||||
} |
|
||||||
for (i = 0; i < allSubcommands.length; i += 1) { |
|
||||||
if (candidateSubCommandName.Compare(allSubcommands[i].name)) { |
|
||||||
candidateSubCommandName.FreeSelf(); |
|
||||||
pickedSubCommand = allSubcommands[i]; |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
// We will only reach here if we did not match any sub commands, |
|
||||||
// meaning that whatever consumed by[ `candidateSubCommandName`] probably |
|
||||||
// has a different meaning. |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` is not `none` |
|
||||||
// Declares an error if `commandParser` still has any input left |
|
||||||
private final function AssertNoTrailingInput() { |
|
||||||
local Text remainder; |
|
||||||
|
|
||||||
if (!commandParser.Ok()) return; |
|
||||||
if (commandParser.Skip().GetRemainingLength() <= 0) return; |
|
||||||
|
|
||||||
remainder = commandParser.GetRemainder(); |
|
||||||
DeclareError(CET_UnusedCommandParameters, remainder); |
|
||||||
remainder.FreeSelf(); |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` is not `none`. |
|
||||||
// Parses given required and optional parameters along with any possible option |
|
||||||
// declarations. |
|
||||||
// Returns `HashTable` filled with (variable, parsed value) pairs. |
|
||||||
// Failure is equal to `commandParser` entering into a failed state. |
|
||||||
private final function HashTable ParseParameterArrays( |
|
||||||
array<Command.Parameter> requiredParameters, |
|
||||||
array<Command.Parameter> optionalParameters |
|
||||||
) { |
|
||||||
local HashTable parsedParameters; |
|
||||||
|
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return none; |
|
||||||
} |
|
||||||
parsedParameters = _.collections.EmptyHashTable(); |
|
||||||
// Parse parameters |
|
||||||
ParseRequiredParameterArray(parsedParameters, requiredParameters); |
|
||||||
ParseOptionalParameterArray(parsedParameters, optionalParameters); |
|
||||||
// Parse trailing options |
|
||||||
while (TryParsingOptions()); |
|
||||||
return parsedParameters; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses given required parameters along with any possible option declarations into given |
|
||||||
// `parsedParameters` `HashTable`. |
|
||||||
private final function ParseRequiredParameterArray( |
|
||||||
HashTable parsedParameters, |
|
||||||
array<Command.Parameter> requiredParameters |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return; |
|
||||||
} |
|
||||||
currentTarget = CPT_NecessaryParameter; |
|
||||||
while (i < requiredParameters.length) { |
|
||||||
if (i == requiredParameters.length - 1) { |
|
||||||
currentTarget = CPT_LastNecessaryParameter; |
|
||||||
} |
|
||||||
// Parse parameters one-by-one, reporting appropriate errors |
|
||||||
if (!ParseParameter(parsedParameters, requiredParameters[i])) { |
|
||||||
// Any failure to parse required parameter leads to error |
|
||||||
if (currentTargetIsOption) { |
|
||||||
DeclareError( CET_NoRequiredParamForOption, |
|
||||||
targetOption.longName); |
|
||||||
} else { |
|
||||||
DeclareError( CET_NoRequiredParam, |
|
||||||
requiredParameters[i].displayName); |
|
||||||
} |
|
||||||
return; |
|
||||||
} |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
currentTarget = CPT_ExtraParameter; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses given optional parameters along with any possible option declarations |
|
||||||
// into given `parsedParameters` hash table. |
|
||||||
private final function ParseOptionalParameterArray( |
|
||||||
HashTable parsedParameters, |
|
||||||
array<Command.Parameter> optionalParameters |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return; |
|
||||||
} |
|
||||||
while (i < optionalParameters.length) { |
|
||||||
confirmedState = commandParser.GetCurrentState(); |
|
||||||
// Parse parameters one-by-one, reporting appropriate errors |
|
||||||
if (!ParseParameter(parsedParameters, optionalParameters[i])) { |
|
||||||
// Propagate errors |
|
||||||
if (nextResult.parsingError != CET_None) { |
|
||||||
return; |
|
||||||
} |
|
||||||
// Failure to parse optional parameter is fine if |
|
||||||
// it is caused by that parameters simply missing |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
break; |
|
||||||
} |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses one given parameter along with any possible option declarations into |
|
||||||
// given `parsedParameters` `HashTable`. |
|
||||||
// |
|
||||||
// Returns `true` if we've successfully parsed given parameter without any |
|
||||||
// errors. |
|
||||||
private final function bool ParseParameter( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local bool parsedEnough; |
|
||||||
|
|
||||||
confirmedState = commandParser.GetCurrentState(); |
|
||||||
while (ParseSingleValue(parsedParameters, expectedParameter)) { |
|
||||||
if (currentTarget == CPT_LastNecessaryParameter) { |
|
||||||
currentTarget = CPT_ExtraParameter; |
|
||||||
} |
|
||||||
parsedEnough = true; |
|
||||||
// We are done if there is either no more input or we only needed |
|
||||||
// to parse a single value |
|
||||||
if (!expectedParameter.allowsList) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
if (commandParser.Skip().HasFinished()) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
confirmedState = commandParser.GetCurrentState(); |
|
||||||
} |
|
||||||
// We only succeeded in parsing if we've parsed enough for |
|
||||||
// a given parameter and did not encounter any errors |
|
||||||
if (parsedEnough && nextResult.parsingError == CET_None) { |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
return true; |
|
||||||
} |
|
||||||
// Clean up any values `ParseSingleValue` might have recorded |
|
||||||
parsedParameters.RemoveItem(expectedParameter.variableName); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses a single value for a given parameter (e.g. one integer for integer or |
|
||||||
// integer list parameter types) along with any possible option declarations |
|
||||||
// into given `parsedParameters`. |
|
||||||
// |
|
||||||
// Returns `true` if we've successfully parsed a single value without |
|
||||||
// any errors. |
|
||||||
private final function bool ParseSingleValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
// Before parsing any other value we need to check if user has specified any options instead. |
|
||||||
// |
|
||||||
// However this might lead to errors if we are already parsing necessary parameters of another |
|
||||||
// option: we must handle such situation and report an error. |
|
||||||
if (currentTargetIsOption) { |
|
||||||
// There is no problem is option's parameter is remainder |
|
||||||
if (expectedParameter.type == CPT_Remainder) { |
|
||||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
|
||||||
} |
|
||||||
if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) { |
|
||||||
DeclareError(CET_NoRequiredParamForOption, targetOption.longName); |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
while (TryParsingOptions()); |
|
||||||
// First we try `CPT_Remainder` parameter, since it is a special case that |
|
||||||
// consumes all further input |
|
||||||
if (expectedParameter.type == CPT_Remainder) { |
|
||||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
|
||||||
} |
|
||||||
// Propagate errors after parsing options |
|
||||||
if (nextResult.parsingError != CET_None) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
// Try parsing one of the variable types |
|
||||||
if (expectedParameter.type == CPT_Boolean) { |
|
||||||
return ParseBooleanValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Integer) { |
|
||||||
return ParseIntegerValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Number) { |
|
||||||
return ParseNumberValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Text) { |
|
||||||
return ParseTextValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Remainder) { |
|
||||||
return ParseRemainderValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Object) { |
|
||||||
return ParseObjectValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Array) { |
|
||||||
return ParseArrayValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_JSON) { |
|
||||||
return ParseJSONValue(parsedParameters, expectedParameter); |
|
||||||
} else if (expectedParameter.type == CPT_Players) { |
|
||||||
return ParsePlayersValue(parsedParameters, expectedParameter); |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses a single boolean value into given `parsedParameters` hash table. |
|
||||||
private final function bool ParseBooleanValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
local bool isValidBooleanLiteral; |
|
||||||
local bool booleanValue; |
|
||||||
local MutableText parsedLiteral; |
|
||||||
|
|
||||||
commandParser.Skip().MUntil(parsedLiteral,, true); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
_.memory.Free(parsedLiteral); |
|
||||||
return false; |
|
||||||
} |
|
||||||
// Try to match parsed literal to any recognizable boolean literals |
|
||||||
for (i = 0; i < booleanTrueEquivalents.length; i += 1) { |
|
||||||
if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) { |
|
||||||
isValidBooleanLiteral = true; |
|
||||||
booleanValue = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
for (i = 0; i < booleanFalseEquivalents.length; i += 1) { |
|
||||||
if (isValidBooleanLiteral) { |
|
||||||
break; |
|
||||||
} |
|
||||||
if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) { |
|
||||||
isValidBooleanLiteral = true; |
|
||||||
booleanValue = false; |
|
||||||
} |
|
||||||
} |
|
||||||
parsedLiteral.FreeSelf(); |
|
||||||
if (!isValidBooleanLiteral) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue)); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single integer value into given `parsedParameters` hash table. |
|
||||||
private final function bool ParseIntegerValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local int integerValue; |
|
||||||
|
|
||||||
commandParser.Skip().MInteger(integerValue); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue)); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single number (float) value into given `parsedParameters` |
|
||||||
// hash table. |
|
||||||
private final function bool ParseNumberValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local float numberValue; |
|
||||||
|
|
||||||
commandParser.Skip().MNumber(numberValue); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue)); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single `Text` value into given `parsedParameters` |
|
||||||
// hash table. |
|
||||||
private final function bool ParseTextValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local bool failedParsing; |
|
||||||
local MutableText textValue; |
|
||||||
local Parser.ParserState initialState; |
|
||||||
local HashTable resolvedPair; |
|
||||||
|
|
||||||
// (needs some work for reading formatting `string`s from `Text` objects) |
|
||||||
initialState = commandParser.Skip().GetCurrentState(); |
|
||||||
// Try manually parsing as a string literal first, since then we will |
|
||||||
// allow empty `textValue` as a result |
|
||||||
commandParser.MStringLiteral(textValue); |
|
||||||
failedParsing = !commandParser.Ok(); |
|
||||||
// Otherwise - empty values are not allowed |
|
||||||
if (failedParsing) { |
|
||||||
_.memory.Free(textValue); |
|
||||||
commandParser.RestoreState(initialState).MString(textValue); |
|
||||||
failedParsing = (!commandParser.Ok() || textValue.IsEmpty()); |
|
||||||
} |
|
||||||
if (failedParsing) { |
|
||||||
_.memory.Free(textValue); |
|
||||||
commandParser.Fail(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName); |
|
||||||
if (resolvedPair != none) { |
|
||||||
RecordParameter(parsedParameters, expectedParameter, resolvedPair); |
|
||||||
_.memory.Free(textValue); |
|
||||||
} else { |
|
||||||
RecordParameter(parsedParameters, expectedParameter, textValue.IntoText()); |
|
||||||
} |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Resolves alias and returns it, along with the resolved value, if parameter |
|
||||||
// was specified to be auto-resolved. |
|
||||||
// Returns `none` otherwise. |
|
||||||
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) { |
|
||||||
local HashTable result; |
|
||||||
local Text resolvedValue, immutableValue; |
|
||||||
|
|
||||||
if (textValue == none) return none; |
|
||||||
if (aliasSourceName == none) return none; |
|
||||||
|
|
||||||
// Always create `HashTable` with at least "alias" key |
|
||||||
result = _.collections.EmptyHashTable(); |
|
||||||
immutableValue = textValue.Copy(); |
|
||||||
result.SetItem(P("alias"), immutableValue); |
|
||||||
_.memory.Free(immutableValue); |
|
||||||
// Add "value" key only after we've checked for "$" prefix |
|
||||||
if (!textValue.StartsWithS("$")) { |
|
||||||
result.SetItem(P("value"), immutableValue); |
|
||||||
return result; |
|
||||||
} |
|
||||||
if (aliasSourceName.Compare(P("weapon"))) { |
|
||||||
resolvedValue = _.alias.ResolveWeapon(textValue, true); |
|
||||||
} else if (aliasSourceName.Compare(P("color"))) { |
|
||||||
resolvedValue = _.alias.ResolveColor(textValue, true); |
|
||||||
} else if (aliasSourceName.Compare(P("feature"))) { |
|
||||||
resolvedValue = _.alias.ResolveFeature(textValue, true); |
|
||||||
} else if (aliasSourceName.Compare(P("entity"))) { |
|
||||||
resolvedValue = _.alias.ResolveEntity(textValue, true); |
|
||||||
} else { |
|
||||||
resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true); |
|
||||||
} |
|
||||||
result.SetItem(P("value"), resolvedValue); |
|
||||||
_.memory.Free(resolvedValue); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses a single `Text` value into given `parsedParameters` hash table, |
|
||||||
// consuming all remaining contents. |
|
||||||
private final function bool ParseRemainderValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local MutableText value; |
|
||||||
|
|
||||||
commandParser.Skip().MUntil(value); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, value.IntoText()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// |
|
||||||
// Parses a single JSON object into given `parsedParameters` hash table. |
|
||||||
private final function bool ParseObjectValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local HashTable objectValue; |
|
||||||
|
|
||||||
objectValue = _.json.ParseHashTableWith(commandParser); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, objectValue); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single JSON array into given `parsedParameters` hash table. |
|
||||||
private final function bool ParseArrayValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local ArrayList arrayValue; |
|
||||||
|
|
||||||
arrayValue = _.json.ParseArrayListWith(commandParser); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, arrayValue); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single JSON value into given `parsedParameters` |
|
||||||
// hash table. |
|
||||||
private final function bool ParseJSONValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter |
|
||||||
) { |
|
||||||
local AcediaObject jsonValue; |
|
||||||
|
|
||||||
jsonValue = _.json.ParseWith(commandParser); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
RecordParameter(parsedParameters, expectedParameter, jsonValue); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `parsedParameters` are not `none`. |
|
||||||
// Parses a single JSON value into given `parsedParameters` hash table. |
|
||||||
private final function bool ParsePlayersValue( |
|
||||||
HashTable parsedParameters, |
|
||||||
Command.Parameter expectedParameter) |
|
||||||
{ |
|
||||||
local ArrayList resultPlayerList; |
|
||||||
local array<EPlayer> targetPlayers; |
|
||||||
|
|
||||||
currentPlayersParser.ParseWith(commandParser); |
|
||||||
if (commandParser.Ok()) { |
|
||||||
targetPlayers = currentPlayersParser.GetPlayers(); |
|
||||||
} else { |
|
||||||
return false; |
|
||||||
} |
|
||||||
resultPlayerList = _.collections.NewArrayList(targetPlayers); |
|
||||||
_.memory.FreeMany(targetPlayers); |
|
||||||
RecordParameter(parsedParameters, expectedParameter, resultPlayerList); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `parsedParameters` is not `none`. |
|
||||||
// |
|
||||||
// Records `value` for a given `parameter` into a given `parametersArray`. |
|
||||||
// If parameter is not a list type - simply records `value` as value under |
|
||||||
// `parameter.variableName` key. |
|
||||||
// If parameter is a list type - pushed value at the end of an array, recorded at |
|
||||||
// `parameter.variableName` key (creating it if missing). |
|
||||||
// |
|
||||||
// All recorded values are managed by `parametersArray`. |
|
||||||
private final function RecordParameter( |
|
||||||
HashTable parametersArray, |
|
||||||
Command.Parameter parameter, |
|
||||||
/*take*/ AcediaObject value |
|
||||||
) { |
|
||||||
local ArrayList parameterVariable; |
|
||||||
|
|
||||||
if (!parameter.allowsList) { |
|
||||||
parametersArray.SetItem(parameter.variableName, value); |
|
||||||
_.memory.Free(value); |
|
||||||
return; |
|
||||||
} |
|
||||||
parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName)); |
|
||||||
if (parameterVariable == none) { |
|
||||||
parameterVariable = _.collections.EmptyArrayList(); |
|
||||||
} |
|
||||||
parameterVariable.AddItem(value); |
|
||||||
_.memory.Free(value); |
|
||||||
parametersArray.SetItem(parameter.variableName, parameterVariable); |
|
||||||
_.memory.Free(parameterVariable); |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` is not `none`. |
|
||||||
// |
|
||||||
// Tries to parse an option declaration (along with all of it's parameters) with |
|
||||||
// `commandParser`. |
|
||||||
// |
|
||||||
// Returns `true` on success and `false` otherwise. |
|
||||||
// |
|
||||||
// In case of failure to detect option declaration also reverts state of |
|
||||||
// `commandParser` to that before `TryParsingOptions()` call. |
|
||||||
// However, if option declaration was present, but invalid (or had invalid |
|
||||||
// parameters) parser will be left in a failed state. |
|
||||||
private final function bool TryParsingOptions() { |
|
||||||
local int temporaryInt; |
|
||||||
|
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
confirmedState = commandParser.GetCurrentState(); |
|
||||||
// Long options |
|
||||||
commandParser.Skip().Match(P("--")); |
|
||||||
if (commandParser.Ok()) { |
|
||||||
return ParseLongOption(); |
|
||||||
} |
|
||||||
// Filter out negative numbers that start similarly to short options: |
|
||||||
// -3, -5.7, -.9 |
|
||||||
commandParser |
|
||||||
.RestoreState(confirmedState) |
|
||||||
.Skip() |
|
||||||
.Match(P("-")) |
|
||||||
.MUnsignedInteger(temporaryInt, 10, 1); |
|
||||||
if (commandParser.Ok()) { |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
return false; |
|
||||||
} |
|
||||||
commandParser.RestoreState(confirmedState).Skip().Match(P("-.")); |
|
||||||
if (commandParser.Ok()) { |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
return false; |
|
||||||
} |
|
||||||
// Short options |
|
||||||
commandParser.RestoreState(confirmedState).Skip().Match(P("-")); |
|
||||||
if (commandParser.Ok()) { |
|
||||||
return ParseShortOption(); |
|
||||||
} |
|
||||||
commandParser.RestoreState(confirmedState); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` is not `none`. |
|
||||||
// |
|
||||||
// Tries to parse a long option name along with all of it's possible parameters |
|
||||||
// with `commandParser`. |
|
||||||
// |
|
||||||
// Returns `true` on success and `false` otherwise. At the point this method is |
|
||||||
// called, option declaration is already assumed to be detected and any failure |
|
||||||
// implies parsing error (ending in failed `Command.CallData`). |
|
||||||
private final function bool ParseLongOption() { |
|
||||||
local int i, optionIndex; |
|
||||||
local MutableText optionName; |
|
||||||
|
|
||||||
commandParser.MUntil(optionName,, true); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
while (optionIndex < availableOptions.length) { |
|
||||||
if (optionName.Compare(availableOptions[optionIndex].longName)) break; |
|
||||||
optionIndex += 1; |
|
||||||
} |
|
||||||
if (optionIndex >= availableOptions.length) { |
|
||||||
DeclareError(CET_UnknownOption, optionName); |
|
||||||
optionName.FreeSelf(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (i = 0; i < usedOptions.length; i += 1) { |
|
||||||
if (optionName.Compare(usedOptions[i].longName)) { |
|
||||||
DeclareError(CET_RepeatedOption, optionName); |
|
||||||
optionName.FreeSelf(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
//usedOptions[usedOptions.length] = availableOptions[optionIndex]; |
|
||||||
optionName.FreeSelf(); |
|
||||||
return ParseOptionParameters(availableOptions[optionIndex]); |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `nextResult` are not `none`. |
|
||||||
// |
|
||||||
// Tries to parse a short option name along with all of it's possible parameters |
|
||||||
// with `commandParser`. |
|
||||||
// |
|
||||||
// Returns `true` on success and `false` otherwise. At the point this |
|
||||||
// method is called, option declaration is already assumed to be detected |
|
||||||
// and any failure implies parsing error (ending in failed `Command.CallData`). |
|
||||||
private final function bool ParseShortOption() { |
|
||||||
local int i; |
|
||||||
local bool pickedOptionWithParameters; |
|
||||||
local MutableText optionsList; |
|
||||||
|
|
||||||
commandParser.MUntil(optionsList,, true); |
|
||||||
if (!commandParser.Ok()) { |
|
||||||
optionsList.FreeSelf(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (i = 0; i < optionsList.GetLength(); i += 1) { |
|
||||||
if (nextResult.parsingError != CET_None) break; |
|
||||||
pickedOptionWithParameters = |
|
||||||
AddOptionByCharacter( |
|
||||||
optionsList.GetCharacter(i), |
|
||||||
optionsList, |
|
||||||
pickedOptionWithParameters) |
|
||||||
|| pickedOptionWithParameters; |
|
||||||
} |
|
||||||
optionsList.FreeSelf(); |
|
||||||
return (nextResult.parsingError == CET_None); |
|
||||||
} |
|
||||||
|
|
||||||
// Assumes `commandParser` and `nextResult` are not `none`. |
|
||||||
// |
|
||||||
// Auxiliary method that adds option by it's short version's character |
|
||||||
// `optionCharacter`. |
|
||||||
// |
|
||||||
// It also accepts `optionSourceList` that describes short option expression |
|
||||||
// (e.g. "-rtV") from |
|
||||||
// which it originated for error reporting and `forbidOptionWithParameters` |
|
||||||
// that, when set to `true`, forces this method to cause the |
|
||||||
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters. |
|
||||||
// |
|
||||||
// Method returns `true` if added option had non-empty parameters and `false` |
|
||||||
// otherwise. |
|
||||||
// |
|
||||||
// Any parsing failure inside this method always causes |
|
||||||
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()` |
|
||||||
// to check if method has failed. |
|
||||||
private final function bool AddOptionByCharacter( |
|
||||||
BaseText.Character optionCharacter, |
|
||||||
BaseText optionSourceList, |
|
||||||
bool forbidOptionWithParameters |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
local bool optionHasParameters; |
|
||||||
|
|
||||||
// Prevent same option appearing twice |
|
||||||
for (i = 0; i < usedOptions.length; i += 1) { |
|
||||||
if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) { |
|
||||||
DeclareError(CET_RepeatedOption, usedOptions[i].longName); |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
// If it's a new option - look it up in all available options |
|
||||||
for (i = 0; i < availableOptions.length; i += 1) { |
|
||||||
if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
usedOptions[usedOptions.length] = availableOptions[i]; |
|
||||||
optionHasParameters = (availableOptions[i].required.length > 0 |
|
||||||
|| availableOptions[i].optional.length > 0); |
|
||||||
// Enforce `forbidOptionWithParameters` flag restriction |
|
||||||
if (optionHasParameters && forbidOptionWithParameters) { |
|
||||||
DeclareError(CET_MultipleOptionsWithParams, optionSourceList); |
|
||||||
return optionHasParameters; |
|
||||||
} |
|
||||||
// Parse parameters (even if they are empty) and bail |
|
||||||
commandParser.Skip(); |
|
||||||
ParseOptionParameters(availableOptions[i]); |
|
||||||
break; |
|
||||||
} |
|
||||||
if (i >= availableOptions.length) { |
|
||||||
DeclareError(CET_UnknownShortOption); |
|
||||||
} |
|
||||||
return optionHasParameters; |
|
||||||
} |
|
||||||
|
|
||||||
// Auxiliary method for parsing option's parameters (including empty ones). |
|
||||||
// Automatically fills `nextResult` with parsed parameters (or `none` if option |
|
||||||
// has no parameters). |
|
||||||
// Assumes `commandParser` and `nextResult` are not `none`. |
|
||||||
private final function bool ParseOptionParameters(Command.Option pickedOption) { |
|
||||||
local HashTable optionParameters; |
|
||||||
|
|
||||||
// If we are already parsing other option's parameters and did not finish |
|
||||||
// parsing all required ones - we cannot start another option |
|
||||||
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) { |
|
||||||
DeclareError(CET_NoRequiredParamForOption, targetOption.longName); |
|
||||||
return false; |
|
||||||
} |
|
||||||
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) { |
|
||||||
nextResult.options.SetItem(pickedOption.longName, none); |
|
||||||
return true; |
|
||||||
} |
|
||||||
currentTargetIsOption = true; |
|
||||||
targetOption = pickedOption; |
|
||||||
optionParameters = ParseParameterArrays( |
|
||||||
pickedOption.required, |
|
||||||
pickedOption.optional); |
|
||||||
currentTargetIsOption = false; |
|
||||||
if (commandParser.Ok()) { |
|
||||||
nextResult.options.SetItem(pickedOption.longName, optionParameters); |
|
||||||
_.memory.Free(optionParameters); |
|
||||||
return true; |
|
||||||
} |
|
||||||
_.memory.Free(optionParameters); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
booleanTrueEquivalents(0) = "true" |
|
||||||
booleanTrueEquivalents(1) = "enable" |
|
||||||
booleanTrueEquivalents(2) = "on" |
|
||||||
booleanTrueEquivalents(3) = "yes" |
|
||||||
booleanFalseEquivalents(0) = "false" |
|
||||||
booleanFalseEquivalents(1) = "disable" |
|
||||||
booleanFalseEquivalents(2) = "off" |
|
||||||
booleanFalseEquivalents(3) = "no" |
|
||||||
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.") |
|
||||||
} |
|
@ -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 |
|
||||||
} |
|
@ -1,81 +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 CommandRegistrationJob extends SchedulerJob |
|
||||||
dependson(CommandAPI); |
|
||||||
|
|
||||||
var private CommandAPI.AsyncTask nextItem; |
|
||||||
|
|
||||||
// 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() { |
|
||||||
nextItem = _.commands._popPending(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName); |
|
||||||
nextItem.entityClass = none; |
|
||||||
nextItem.entityName = none; |
|
||||||
nextItem.userGroup = none; |
|
||||||
nextItem.configName = none; |
|
||||||
} |
|
||||||
|
|
||||||
public function bool IsCompleted() { |
|
||||||
return (nextItem.entityName == none); |
|
||||||
} |
|
||||||
|
|
||||||
public function DoWork(int allottedWorkUnits) { |
|
||||||
while (allottedWorkUnits > 0 && nextItem.entityName != none) { |
|
||||||
if (nextItem.type == CAJT_AddCommand) { |
|
||||||
allottedWorkUnits -= ADDING_COMMAND_COST; |
|
||||||
_.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName); |
|
||||||
_.memory.Free(nextItem.entityName); |
|
||||||
} else if (nextItem.type == CAJT_AddVoting) { |
|
||||||
allottedWorkUnits -= ADDING_VOTING_COST; |
|
||||||
_.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName); |
|
||||||
_.memory.Free(nextItem.entityName); |
|
||||||
} 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 { |
|
||||||
} |
|
@ -1,230 +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 Commands extends FeatureConfig |
|
||||||
perobjectconfig |
|
||||||
config(AcediaCommands); |
|
||||||
|
|
||||||
/// 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; |
|
||||||
/// Chat messages, prepended by this prefix will be treated as commands. |
|
||||||
/// Default is "!". Empty values are also treated as "!". |
|
||||||
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() { |
|
||||||
local int i; |
|
||||||
local HashTable data; |
|
||||||
local ArrayList innerList; |
|
||||||
local HashTable innerPair; |
|
||||||
|
|
||||||
data = __().collections.EmptyHashTable(); |
|
||||||
data.SetBool(P("useChatInput"), useChatInput, true); |
|
||||||
data.SetBool(P("useMutateInput"), useMutateInput, true); |
|
||||||
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; |
|
||||||
} |
|
||||||
|
|
||||||
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) { |
|
||||||
return; |
|
||||||
} |
|
||||||
useChatInput = source.GetBool(P("useChatInput")); |
|
||||||
useMutateInput = source.GetBool(P("useMutateInput")); |
|
||||||
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() { |
|
||||||
local CommandSetGroupPair defaultPair; |
|
||||||
|
|
||||||
useChatInput = true; |
|
||||||
useMutateInput = true; |
|
||||||
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 { |
|
||||||
configName = "AcediaCommands" |
|
||||||
} |
|
@ -1,708 +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 Commands_Feature extends Feature |
|
||||||
dependson(CommandAPI) |
|
||||||
dependson(Commands); |
|
||||||
|
|
||||||
//! This feature manages commands that automatically parse their arguments into standard Acedia |
|
||||||
//! collections. |
|
||||||
//! |
|
||||||
//! # Implementation |
|
||||||
//! |
|
||||||
//! Implementation is simple: calling a method `RegisterCommand()` adds |
|
||||||
//! command into two caches `registeredCommands` for obtaining registered |
|
||||||
//! commands by name and `groupedCommands` for obtaining arrays of commands by |
|
||||||
//! their group name. These arrays are used for providing methods for fetching |
|
||||||
//! arrays of commands and obtaining pre-allocated `Command` instances by their |
|
||||||
//! name. |
|
||||||
//! Depending on settings, this feature also connects to corresponding |
|
||||||
//! signals for catching "mutate"/chat input, then it checks user-specified name |
|
||||||
//! for being an alias and picks correct command from `registeredCommands`. |
|
||||||
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that |
|
||||||
//! enforces connecting to the "mutate" input. |
|
||||||
|
|
||||||
/// Auxiliary struct for passing name of the command to call with pre-specified |
|
||||||
/// sub-command name. |
|
||||||
/// |
|
||||||
/// Normally sub-command name is parsed by the command itself, however command |
|
||||||
/// aliases can try to enforce one. |
|
||||||
struct CommandCallPair { |
|
||||||
var MutableText commandName; |
|
||||||
/// Not `none` in case it is enforced by an alias |
|
||||||
var MutableText subCommandName; |
|
||||||
}; |
|
||||||
|
|
||||||
/// Auxiliary struct that stores all the information needed to load |
|
||||||
/// a certain command |
|
||||||
struct EntityLoadInfo { |
|
||||||
/// Command class to load. |
|
||||||
var public class<AcediaObject> entityClass; |
|
||||||
/// Name to load that command class under. |
|
||||||
var public Text name; |
|
||||||
/// Groups that are authorized to use that command. |
|
||||||
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 |
|
||||||
var private array<Text> commandDelimiters; |
|
||||||
|
|
||||||
/// 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 /*config*/ bool useChatInput; |
|
||||||
var private /*config*/ bool useMutateInput; |
|
||||||
var private /*config*/ Text chatCommandPrefix; |
|
||||||
var public /*config*/ array<string> commandGroup; |
|
||||||
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet; |
|
||||||
var public /*config*/ array<Commands.RenamingRulePair> renamingRule; |
|
||||||
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() { |
|
||||||
helpCommandName = P("help"); |
|
||||||
// Macro selector |
|
||||||
commandDelimiters[0] = _.text.FromString("@"); |
|
||||||
// Key selector |
|
||||||
commandDelimiters[1] = _.text.FromString("#"); |
|
||||||
// Player array (possibly JSON array) |
|
||||||
commandDelimiters[2] = _.text.FromString("["); |
|
||||||
// Negation of the selector |
|
||||||
// NOT the same thing as default command prefix in chat |
|
||||||
commandDelimiters[3] = _.text.FromString("!"); |
|
||||||
if (useChatInput) { |
|
||||||
_.chat.OnMessage(self).connect = HandleCommands; |
|
||||||
} |
|
||||||
else { |
|
||||||
_.chat.OnMessage(self).Disconnect(); |
|
||||||
} |
|
||||||
if (useMutateInput || emergencyEnabledMutate) { |
|
||||||
if (__server() != none) { |
|
||||||
__server().unreal.mutator.OnMutate(self).connect = HandleMutate; |
|
||||||
} else { |
|
||||||
_.logger.Auto(errServerAPIUnavailable); |
|
||||||
} |
|
||||||
} |
|
||||||
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() { |
|
||||||
if (useChatInput) { |
|
||||||
_.chat.OnMessage(self).Disconnect(); |
|
||||||
} |
|
||||||
if (useMutateInput && __server() != none) { |
|
||||||
__server().unreal.mutator.OnMutate(self).Disconnect(); |
|
||||||
} |
|
||||||
|
|
||||||
useChatInput = false; |
|
||||||
useMutateInput = false; |
|
||||||
_.memory.Free3(tools.commands, tools.votings, chatCommandPrefix); |
|
||||||
tools.commands = none; |
|
||||||
tools.votings = none; |
|
||||||
chatCommandPrefix = none; |
|
||||||
|
|
||||||
_.memory.FreeMany(commandDelimiters); |
|
||||||
commandDelimiters.length = 0; |
|
||||||
|
|
||||||
_.memory.FreeMany(permissionGroupOrder); |
|
||||||
permissionGroupOrder.length = 0; |
|
||||||
|
|
||||||
FreeUsedCommandSets(); |
|
||||||
FreeRenamingRules(); |
|
||||||
|
|
||||||
_.commands._reloadFeature(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function SwapConfig(FeatureConfig config) { |
|
||||||
local Commands newConfig; |
|
||||||
|
|
||||||
newConfig = Commands(config); |
|
||||||
if (newConfig == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
_.memory.Free(chatCommandPrefix); |
|
||||||
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); |
|
||||||
useChatInput = newConfig.useChatInput; |
|
||||||
useMutateInput = newConfig.useMutateInput; |
|
||||||
commandGroup = newConfig.commandGroup; |
|
||||||
addCommandSet = newConfig.addCommandList; |
|
||||||
renamingRule = newConfig.renamingRule; |
|
||||||
votingRenamingRule = newConfig.votingRenamingRule; |
|
||||||
} |
|
||||||
|
|
||||||
/// This method allows to forcefully enable `Command_Feature` along with |
|
||||||
/// "mutate" input in case something goes wrong. |
|
||||||
/// |
|
||||||
/// `Command_Feature` is a critical command to have running on your server and, |
|
||||||
/// if disabled by accident, there will be no way of starting it again without |
|
||||||
/// restarting the level or even editing configs. |
|
||||||
public final static function EmergencyEnable() { |
|
||||||
local bool noWayToInputCommands; |
|
||||||
local Text autoConfig; |
|
||||||
local Commands_Feature feature; |
|
||||||
|
|
||||||
if (!IsEnabled()) { |
|
||||||
autoConfig = GetAutoEnabledConfig(); |
|
||||||
EnableMe(autoConfig); |
|
||||||
__().memory.Free(autoConfig); |
|
||||||
} |
|
||||||
feature = Commands_Feature(GetEnabledInstance()); |
|
||||||
noWayToInputCommands = !feature.emergencyEnabledMutate |
|
||||||
&& !feature.IsUsingMutateInput() |
|
||||||
&& !feature.IsUsingChatInput(); |
|
||||||
if (noWayToInputCommands) { |
|
||||||
default.emergencyEnabledMutate = true; |
|
||||||
feature.emergencyEnabledMutate = true; |
|
||||||
if (__server() != none) { |
|
||||||
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate; |
|
||||||
} else { |
|
||||||
__().logger.Auto(default.errServerAPIUnavailable); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if `Commands_Feature` currently uses chat as input. |
|
||||||
/// |
|
||||||
/// If `Commands_Feature` is not enabled, then it does not use anything |
|
||||||
/// as input. |
|
||||||
public final static function bool IsUsingChatInput() { |
|
||||||
local Commands_Feature instance; |
|
||||||
|
|
||||||
instance = Commands_Feature(GetEnabledInstance()); |
|
||||||
if (instance != none) { |
|
||||||
return instance.useChatInput; |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if `Commands_Feature` currently uses mutate command as input. |
|
||||||
/// |
|
||||||
/// If `Commands_Feature` is not enabled, then it does not use anything |
|
||||||
/// as input. |
|
||||||
public final static function bool IsUsingMutateInput() { |
|
||||||
local Commands_Feature instance; |
|
||||||
|
|
||||||
instance = Commands_Feature(GetEnabledInstance()); |
|
||||||
if (instance != none) { |
|
||||||
return instance.useMutateInput; |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns prefix that will indicate that chat message is intended to be |
|
||||||
/// a command. By default "!". |
|
||||||
/// |
|
||||||
/// If `Commands_Feature` is disabled, always returns `none`. |
|
||||||
public final static function Text GetChatPrefix() { |
|
||||||
local Commands_Feature instance; |
|
||||||
|
|
||||||
instance = Commands_Feature(GetEnabledInstance()); |
|
||||||
if (instance != none && instance.chatCommandPrefix != none) { |
|
||||||
return instance.chatCommandPrefix.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns name, under which [`ACommandHelp`] is registered. |
|
||||||
/// |
|
||||||
/// If `Commands_Feature` is disabled, always returns `none`. |
|
||||||
public final static function Text GetHelpCommandName() { |
|
||||||
local Commands_Feature instance; |
|
||||||
|
|
||||||
instance = Commands_Feature(GetEnabledInstance()); |
|
||||||
if (instance != none && instance.helpCommandName != none) { |
|
||||||
return instance.helpCommandName.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Executes command based on the input. |
|
||||||
/// |
|
||||||
/// Takes [`commandLine`] as input with command's call, finds appropriate |
|
||||||
/// registered command instance and executes it with parameters specified in |
|
||||||
/// the [`commandLine`]. |
|
||||||
/// |
|
||||||
/// [`callerPlayer`] has to be specified and represents instigator of this |
|
||||||
/// command that will receive appropriate result/error messages. |
|
||||||
/// |
|
||||||
/// Returns `true` iff command was successfully executed. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// Doesn't log any errors, but can complain about errors in name or parameters |
|
||||||
/// to the [`callerPlayer`] |
|
||||||
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) { |
|
||||||
local bool result; |
|
||||||
local Parser wrapper; |
|
||||||
|
|
||||||
if (input == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
wrapper = input.Parse(); |
|
||||||
result = HandleInputWith(wrapper, callerPlayer); |
|
||||||
wrapper.FreeSelf(); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Executes command based on the input. |
|
||||||
/// |
|
||||||
/// Takes [`commandLine`] as input with command's call, finds appropriate |
|
||||||
/// registered command instance and executes it with parameters specified in |
|
||||||
/// the [`commandLine`]. |
|
||||||
/// |
|
||||||
/// [`callerPlayer`] has to be specified and represents instigator of this |
|
||||||
/// command that will receive appropriate result/error messages. |
|
||||||
/// |
|
||||||
/// Returns `true` iff command was successfully executed. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// Doesn't log any errors, but can complain about errors in name or parameters |
|
||||||
/// to the [`callerPlayer`] |
|
||||||
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) { |
|
||||||
local bool errorOccured; |
|
||||||
local User identity; |
|
||||||
local CommandAPI.CommandConfigInfo commandPair; |
|
||||||
local Command.CallData callData; |
|
||||||
local CommandCallPair callPair; |
|
||||||
|
|
||||||
if (parser == none) return false; |
|
||||||
if (callerPlayer == none) return false; |
|
||||||
if (!parser.Ok()) return false; |
|
||||||
|
|
||||||
identity = callerPlayer.GetIdentity(); |
|
||||||
callPair = ParseCommandCallPairWith(parser); |
|
||||||
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity); |
|
||||||
if (commandPair.instance == none || commandPair.usageForbidden) { |
|
||||||
if (callerPlayer != none && callerPlayer.IsExistent()) { |
|
||||||
callerPlayer |
|
||||||
.BorrowConsole() |
|
||||||
.Flush() |
|
||||||
.Say(F("{$TextFailure Command not found!}")); |
|
||||||
} |
|
||||||
} |
|
||||||
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) { |
|
||||||
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 |
|
||||||
// specified name is an alias with specified sub-command name. |
|
||||||
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) { |
|
||||||
local Text resolvedValue; |
|
||||||
local MutableText userSpecifiedName; |
|
||||||
local CommandCallPair result; |
|
||||||
local Text.Character dotCharacter; |
|
||||||
|
|
||||||
if (parser == none) return result; |
|
||||||
if (!parser.Ok()) return result; |
|
||||||
|
|
||||||
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); |
|
||||||
resolvedValue = _.alias.ResolveCommand(userSpecifiedName); |
|
||||||
// This isn't an alias |
|
||||||
if (resolvedValue == none) { |
|
||||||
result.commandName = userSpecifiedName; |
|
||||||
return result; |
|
||||||
} |
|
||||||
// It is an alias - parse it |
|
||||||
dotCharacter = _.text.GetCharacter("."); |
|
||||||
resolvedValue.Parse() |
|
||||||
.MUntil(result.commandName, dotCharacter) |
|
||||||
.MatchS(".") |
|
||||||
.MUntil(result.subCommandName, dotCharacter) |
|
||||||
.FreeSelf(); |
|
||||||
if (result.subCommandName.IsEmpty()) { |
|
||||||
result.subCommandName.FreeSelf(); |
|
||||||
result.subCommandName = none; |
|
||||||
} |
|
||||||
resolvedValue.FreeSelf(); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) { |
|
||||||
local Parser parser; |
|
||||||
|
|
||||||
// We are only interested in messages that start with `chatCommandPrefix` |
|
||||||
parser = _.text.Parse(message); |
|
||||||
if (!parser.Match(chatCommandPrefix).Ok()) { |
|
||||||
parser.FreeSelf(); |
|
||||||
return true; |
|
||||||
} |
|
||||||
// Pass input to command feature |
|
||||||
HandleInputWith(parser, sender); |
|
||||||
parser.FreeSelf(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
private function HandleMutate(string command, PlayerController sendingPlayer) { |
|
||||||
local Parser parser; |
|
||||||
local EPlayer sender; |
|
||||||
|
|
||||||
// A lot of other mutators use these commands |
|
||||||
if (command ~= "help") return; |
|
||||||
if (command ~= "version") return; |
|
||||||
if (command ~= "status") return; |
|
||||||
if (command ~= "credits") return; |
|
||||||
|
|
||||||
parser = _.text.ParseString(command); |
|
||||||
sender = _.players.FromController(sendingPlayer); |
|
||||||
HandleInputWith(parser, sender); |
|
||||||
sender.FreeSelf(); |
|
||||||
parser.FreeSelf(); |
|
||||||
} |
|
||||||
|
|
||||||
private final function LoadConfigArrays() { |
|
||||||
local int i; |
|
||||||
local CommandListGroupPair nextCommandSetGroupPair; |
|
||||||
|
|
||||||
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() { |
|
||||||
local int i, j; |
|
||||||
local Text nextName; |
|
||||||
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; |
|
||||||
} |
|
||||||
|
|
||||||
// Guaranteed to not return `none` items in the array |
|
||||||
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 { |
|
||||||
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy()); |
|
||||||
} |
|
||||||
} |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
// 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; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function FreeUsedCommandSets() { |
|
||||||
local int i; |
|
||||||
|
|
||||||
for (i = 0; i < usedCommandLists.length; i += 1) { |
|
||||||
_.memory.Free(usedCommandLists[i].commandListName); |
|
||||||
_.memory.Free(usedCommandLists[i].permissionGroup); |
|
||||||
} |
|
||||||
usedCommandLists.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 { |
|
||||||
configClass = class'Commands' |
|
||||||
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.") |
|
||||||
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.") |
|
||||||
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\".") |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -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 { |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -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 { |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -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 { |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -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 { |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -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 { |
|
||||||
} |
|
@ -1,494 +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 PlayersParser extends AcediaObject |
|
||||||
dependson(Parser); |
|
||||||
|
|
||||||
//! This parser is supposed to parse player set definitions as they |
|
||||||
//! are used in commands. |
|
||||||
//! |
|
||||||
//! Basic use is to specify one of the selectors: |
|
||||||
//! 1. Key selector: "#<integer>" (examples: "#1", "#5"). |
|
||||||
//! This one is used to specify players by their key, assigned to them when |
|
||||||
//! they enter the game. |
|
||||||
//! This type of selectors can be used when players have hard to type names. |
|
||||||
//! 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". |
|
||||||
//! "@", "@me", and "@self" are identical and can be used to specify player |
|
||||||
//! that called the command. |
|
||||||
//! "@admin" can be used to specify all admins in the game at once. |
|
||||||
//! "@all" specifies all current players. |
|
||||||
//! In future it is planned to make macros extendable by allowing to bind |
|
||||||
//! more names to specific groups of players. |
|
||||||
//! 3. Name selectors: quoted strings and any other types of string that do not |
|
||||||
//! start with either "#" or "@". |
|
||||||
//! These specify name prefixes: any player with specified prefix will be considered to match |
|
||||||
//! such selector. |
|
||||||
//! |
|
||||||
//! Negated selectors: "!<selector>". Specifying "!" in front of selector will |
|
||||||
//! select all players that do not match it instead. |
|
||||||
//! |
|
||||||
//! Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". |
|
||||||
//! Specified selectors are process in order: from left to right. |
|
||||||
//! First selector works as usual and selects a set of players. |
|
||||||
//! All the following selectors either expand that list (additive ones, without |
|
||||||
//! "!" prefix) or remove specific players from the list (the ones with "!" |
|
||||||
//! prefix). Examples of that: |
|
||||||
//! |
|
||||||
//! * "[@admin, !@self]" - selects all admins, except the one who called |
|
||||||
//! the command (whether he is admin or not). |
|
||||||
//! * "[dkanus, 'mate']" - will select players "dkanus" and "mate". |
|
||||||
//! Order also matters, since: |
|
||||||
//! - "[@admin, !@admin]" - won't select anyone, since it will first add all |
|
||||||
//! the admins and then remove them. |
|
||||||
//! - "[!@admin, @admin]" - will select everyone, since it will first select |
|
||||||
//! everyone who is not an admin and then adds everyone else. |
|
||||||
//! |
|
||||||
//! # Usage |
|
||||||
//! |
|
||||||
//! 1. Allocate `PlayerParser`; |
|
||||||
//! 2. Set caller player through `SetSelf()` method to make "@" and "@me" |
|
||||||
//! selectors usable; |
|
||||||
//! 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that |
|
||||||
//! starts with proper players selector; |
|
||||||
//! 4. Call `GetPlayers()` to obtain selected players array. |
|
||||||
//! |
|
||||||
//! # Implementation |
|
||||||
//! |
|
||||||
//! When created, `PlayersParser` takes a snapshot (array) of current players on |
|
||||||
//! the server. Then `currentSelection` is decided based on whether first |
|
||||||
//! selector is positive (initial selection is taken as empty array) or negative |
|
||||||
//! (initial selection is taken as full snapshot). |
|
||||||
//! |
|
||||||
//! After that `PlayersParser` simply goes through specified selectors |
|
||||||
//! (in case more than one is specified) and adds or removes appropriate players |
|
||||||
//! in `currentSelection`, assuming that `playersSnapshot` is a current full |
|
||||||
//! array of players. |
|
||||||
|
|
||||||
/// Player for which "@", "@me", and "@self" macros will refer |
|
||||||
var private EPlayer selfPlayer; |
|
||||||
/// Copy of the list of current players at the moment of allocation of |
|
||||||
/// this `PlayersParser`. |
|
||||||
var private array<EPlayer> playersSnapshot; |
|
||||||
/// Players, selected according to selectors we have parsed so far |
|
||||||
var private array<EPlayer> currentSelection; |
|
||||||
/// Have we parsed our first selector? |
|
||||||
/// We need this to know whether to start with the list of |
|
||||||
/// all players (if first selector removes them) or |
|
||||||
/// with empty list (if first selector adds them). |
|
||||||
var private bool parsedFirstSelector; |
|
||||||
/// Will be equal to a single-element array [","], used for parsing |
|
||||||
var private array<Text> selectorDelimiters; |
|
||||||
|
|
||||||
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; |
|
||||||
var const int TOPEN_BRACKET, TCLOSE_BRACKET; |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
// No need to deallocate `currentSelection`, |
|
||||||
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer` |
|
||||||
_.memory.Free(selfPlayer); |
|
||||||
_.memory.FreeMany(playersSnapshot); |
|
||||||
selfPlayer = none; |
|
||||||
parsedFirstSelector = false; |
|
||||||
playersSnapshot.length = 0; |
|
||||||
currentSelection.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
/// Set a player who will be referred to by "@", "@me" and "@self" macros. |
|
||||||
/// |
|
||||||
/// Passing `none` will make it so no one is referred by these macros. |
|
||||||
public final function SetSelf(EPlayer newSelfPlayer) { |
|
||||||
_.memory.Free(selfPlayer); |
|
||||||
selfPlayer = none; |
|
||||||
if (newSelfPlayer != none) { |
|
||||||
selfPlayer = EPlayer(newSelfPlayer.Copy()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// 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; |
|
||||||
} |
|
||||||
|
|
||||||
/// Parses players from according to the currently present players. |
|
||||||
/// |
|
||||||
/// Array of parsed players can be retrieved by `self.GetPlayers()` method. |
|
||||||
/// Returns `true` if parsing was successful and `false` otherwise. |
|
||||||
public final function bool Parse(BaseText toParse) { |
|
||||||
local bool wasSuccessful; |
|
||||||
local Parser parser; |
|
||||||
|
|
||||||
if (toParse == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
parser = _.text.Parse(toParse); |
|
||||||
wasSuccessful = ParseWith(parser); |
|
||||||
parser.FreeSelf(); |
|
||||||
return wasSuccessful; |
|
||||||
} |
|
||||||
|
|
||||||
// Insert a new player into currently selected list of players |
|
||||||
// (`currentSelection`) such that there will be no duplicates. |
|
||||||
// |
|
||||||
// `none` values are auto-discarded. |
|
||||||
private final function InsertPlayer(EPlayer toInsert) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (toInsert == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
for (i = 0; i < currentSelection.length; i += 1) { |
|
||||||
if (currentSelection[i] == toInsert) { |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
currentSelection[currentSelection.length] = toInsert; |
|
||||||
} |
|
||||||
|
|
||||||
// Adds all the players with specified key (`key`) to the current selection. |
|
||||||
private final function AddByKey(int key) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
|
||||||
if (playersSnapshot[i].GetIdentity().GetKey() == key) { |
|
||||||
InsertPlayer(playersSnapshot[i]); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Removes all the players with specified key (`key`) from |
|
||||||
// the current selection. |
|
||||||
private final function RemoveByKey(int key) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
while (i < currentSelection.length) { |
|
||||||
if (currentSelection[i].GetIdentity().GetKey() == key) { |
|
||||||
currentSelection.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Adds all the players with specified name (`name`) to the current selection. |
|
||||||
private final function AddByName(BaseText name) { |
|
||||||
local int i; |
|
||||||
local Text nextPlayerName; |
|
||||||
|
|
||||||
if (name == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
|
||||||
nextPlayerName = playersSnapshot[i].GetName(); |
|
||||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
|
||||||
InsertPlayer(playersSnapshot[i]); |
|
||||||
} |
|
||||||
nextPlayerName.FreeSelf(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Removes all the players with specified name (`name`) from |
|
||||||
// the current selection. |
|
||||||
private final function RemoveByName(BaseText name) { |
|
||||||
local int i; |
|
||||||
local Text nextPlayerName; |
|
||||||
|
|
||||||
while (i < currentSelection.length) { |
|
||||||
nextPlayerName = currentSelection[i].GetName(); |
|
||||||
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
|
||||||
currentSelection.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
nextPlayerName.FreeSelf(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Adds all the admins to the current selection. |
|
||||||
private final function AddAdmins() { |
|
||||||
local int i; |
|
||||||
|
|
||||||
for (i = 0; i < playersSnapshot.length; i += 1) { |
|
||||||
if (playersSnapshot[i].IsAdmin()) { |
|
||||||
InsertPlayer(playersSnapshot[i]); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Removes all the admins from the current selection. |
|
||||||
private final function RemoveAdmins() { |
|
||||||
local int i; |
|
||||||
|
|
||||||
while (i < currentSelection.length) { |
|
||||||
if (currentSelection[i].IsAdmin()) { |
|
||||||
currentSelection.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Add all the players specified by `macroText` (from macro "@<macroText>"). |
|
||||||
// Does nothing if there is no such macro. |
|
||||||
private final function AddByMacro(BaseText macroText) { |
|
||||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) { |
|
||||||
AddAdmins(); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) { |
|
||||||
currentSelection = playersSnapshot; |
|
||||||
return; |
|
||||||
} |
|
||||||
if ( macroText.IsEmpty() |
|
||||||
|| macroText.Compare(T(TSELF), SCASE_INSENSITIVE) |
|
||||||
|| macroText.Compare(T(TME), SCASE_INSENSITIVE)) { |
|
||||||
InsertPlayer(selfPlayer); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Removes all the players specified by `macroText` (from macro "@<macroText>"). |
|
||||||
// Does nothing if there is no such macro. |
|
||||||
private final function RemoveByMacro(BaseText macroText) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) { |
|
||||||
RemoveAdmins(); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) { |
|
||||||
currentSelection.length = 0; |
|
||||||
return; |
|
||||||
} |
|
||||||
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) { |
|
||||||
while (i < currentSelection.length) { |
|
||||||
if (currentSelection[i] == selfPlayer) { |
|
||||||
currentSelection.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Parses one selector from `parser`, while accordingly modifying current player |
|
||||||
// selection list. |
|
||||||
private final function ParseSelector(Parser parser) { |
|
||||||
local bool additiveSelector; |
|
||||||
local Parser.ParserState confirmedState; |
|
||||||
|
|
||||||
if (parser == none) return; |
|
||||||
if (!parser.Ok()) return; |
|
||||||
|
|
||||||
confirmedState = parser.GetCurrentState(); |
|
||||||
if (!parser.Match(T(TNOT)).Ok()) { |
|
||||||
additiveSelector = true; |
|
||||||
parser.RestoreState(confirmedState); |
|
||||||
} |
|
||||||
// Determine whether we stars with empty or full player list |
|
||||||
if (!parsedFirstSelector) { |
|
||||||
parsedFirstSelector = true; |
|
||||||
if (additiveSelector) { |
|
||||||
currentSelection.length = 0; |
|
||||||
} |
|
||||||
else { |
|
||||||
currentSelection = playersSnapshot; |
|
||||||
} |
|
||||||
} |
|
||||||
// Try all selector types |
|
||||||
confirmedState = parser.GetCurrentState(); |
|
||||||
if (parser.Match(T(TKEY)).Ok()) { |
|
||||||
ParseKeySelector(parser, additiveSelector); |
|
||||||
return; |
|
||||||
} |
|
||||||
parser.RestoreState(confirmedState); |
|
||||||
if (parser.Match(T(TMACRO)).Ok()) { |
|
||||||
ParseMacroSelector(parser, additiveSelector); |
|
||||||
return; |
|
||||||
} |
|
||||||
parser.RestoreState(confirmedState); |
|
||||||
ParseNameSelector(parser, additiveSelector); |
|
||||||
} |
|
||||||
|
|
||||||
// Parse key selector (assuming "#" is already consumed), while accordingly |
|
||||||
// modifying current player selection list. |
|
||||||
private final function ParseKeySelector(Parser parser, bool additiveSelector) { |
|
||||||
local int key; |
|
||||||
|
|
||||||
if (parser == none) return; |
|
||||||
if (!parser.Ok()) return; |
|
||||||
if (!parser.MInteger(key).Ok()) return; |
|
||||||
|
|
||||||
if (additiveSelector) { |
|
||||||
AddByKey(key); |
|
||||||
} else { |
|
||||||
RemoveByKey(key); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Parse macro selector (assuming "@" is already consumed), while accordingly |
|
||||||
// modifying current player selection list. |
|
||||||
private final function ParseMacroSelector(Parser parser, bool additiveSelector) { |
|
||||||
local MutableText macroName; |
|
||||||
local Parser.ParserState confirmedState; |
|
||||||
|
|
||||||
if (parser == none) return; |
|
||||||
if (!parser.Ok()) return; |
|
||||||
|
|
||||||
confirmedState = parser.GetCurrentState(); |
|
||||||
macroName = ParseLiteral(parser); |
|
||||||
if (!parser.Ok()) { |
|
||||||
_.memory.Free(macroName); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (additiveSelector) { |
|
||||||
AddByMacro(macroName); |
|
||||||
} |
|
||||||
else { |
|
||||||
RemoveByMacro(macroName); |
|
||||||
} |
|
||||||
_.memory.Free(macroName); |
|
||||||
} |
|
||||||
|
|
||||||
// Parse name selector, while accordingly modifying current player selection |
|
||||||
// list. |
|
||||||
private final function ParseNameSelector(Parser parser, bool additiveSelector) { |
|
||||||
local MutableText playerName; |
|
||||||
local Parser.ParserState confirmedState; |
|
||||||
|
|
||||||
if (parser == none) return; |
|
||||||
if (!parser.Ok()) return; |
|
||||||
|
|
||||||
confirmedState = parser.GetCurrentState(); |
|
||||||
playerName = ParseLiteral(parser); |
|
||||||
if (!parser.Ok() || playerName.IsEmpty()) { |
|
||||||
_.memory.Free(playerName); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (additiveSelector) { |
|
||||||
AddByName(playerName); |
|
||||||
} |
|
||||||
else { |
|
||||||
RemoveByName(playerName); |
|
||||||
} |
|
||||||
_.memory.Free(playerName); |
|
||||||
} |
|
||||||
|
|
||||||
// Reads a string that can either be a body of name selector (some player's |
|
||||||
// name prefix) or of a macro selector (what comes after "@"). |
|
||||||
// |
|
||||||
// This is different from `parser.MString()` because it also uses "," as |
|
||||||
// a separator. |
|
||||||
private final function MutableText ParseLiteral(Parser parser) { |
|
||||||
local MutableText literal; |
|
||||||
local Parser.ParserState confirmedState; |
|
||||||
|
|
||||||
if (parser == none) return none; |
|
||||||
if (!parser.Ok()) return none; |
|
||||||
|
|
||||||
confirmedState = parser.GetCurrentState(); |
|
||||||
if (!parser.MStringLiteral(literal).Ok()) { |
|
||||||
parser.RestoreState(confirmedState); |
|
||||||
parser.MUntilMany(literal, selectorDelimiters, true); |
|
||||||
} |
|
||||||
return literal; |
|
||||||
} |
|
||||||
|
|
||||||
// Resets this object to initial state before parsing and update |
|
||||||
// `playersSnapshot` to contain current players. |
|
||||||
private final function Reset() { |
|
||||||
parsedFirstSelector = false; |
|
||||||
currentSelection.length = 0; |
|
||||||
_.memory.FreeMany(playersSnapshot); |
|
||||||
playersSnapshot.length = 0; |
|
||||||
playersSnapshot = _.players.GetAll(); |
|
||||||
selectorDelimiters.length = 0; |
|
||||||
selectorDelimiters[0] = T(TCOMMA); |
|
||||||
selectorDelimiters[1] = T(TCLOSE_BRACKET); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
TSELF = 0 |
|
||||||
stringConstants(0) = "self" |
|
||||||
TADMIN = 1 |
|
||||||
stringConstants(1) = "admin" |
|
||||||
TALL = 2 |
|
||||||
stringConstants(2) = "all" |
|
||||||
TNOT = 3 |
|
||||||
stringConstants(3) = "!" |
|
||||||
TKEY = 4 |
|
||||||
stringConstants(4) = "#" |
|
||||||
TMACRO = 5 |
|
||||||
stringConstants(5) = "@" |
|
||||||
TCOMMA = 6 |
|
||||||
stringConstants(6) = "," |
|
||||||
TOPEN_BRACKET = 7 |
|
||||||
stringConstants(7) = "[" |
|
||||||
TCLOSE_BRACKET = 8 |
|
||||||
stringConstants(8) = "]" |
|
||||||
TME = 9 |
|
||||||
stringConstants(9) = "me" |
|
||||||
} |
|
@ -1,61 +0,0 @@ |
|||||||
/** |
|
||||||
* Mock command class for testing. |
|
||||||
* Copyright 2021 Anton Tarasenko |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* This file is part of Acedia. |
|
||||||
* |
|
||||||
* Acedia is free software: you can redistribute it and/or modify |
|
||||||
* it under the terms of the GNU General Public License as published by |
|
||||||
* the Free Software Foundation, version 3 of the License, or |
|
||||||
* (at your option) any later version. |
|
||||||
* |
|
||||||
* Acedia is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
* GNU General Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License |
|
||||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
||||||
*/ |
|
||||||
class MockCommandB extends Command; |
|
||||||
|
|
||||||
protected function BuildData(CommandDataBuilder builder) |
|
||||||
{ |
|
||||||
builder.ParamArray(P("just_array")); |
|
||||||
builder.ParamText(P("just_text")); |
|
||||||
|
|
||||||
builder.Option(P("values")); |
|
||||||
builder.ParamIntegerList(P("types")); |
|
||||||
|
|
||||||
builder.Option(P("long")); |
|
||||||
builder.ParamInteger(P("num")); |
|
||||||
builder.ParamNumberList(P("text")); |
|
||||||
builder.ParamBoolean(P("huh")); |
|
||||||
|
|
||||||
builder.Option(P("type"), P("t")); |
|
||||||
builder.ParamText(P("type")); |
|
||||||
|
|
||||||
builder.Option(P("Test")); |
|
||||||
builder.ParamText(P("to_test")); |
|
||||||
|
|
||||||
builder.Option(P("silent")); |
|
||||||
builder.Option(P("forced")); |
|
||||||
builder.Option(P("verbose"), P("V")); |
|
||||||
builder.Option(P("actual")); |
|
||||||
|
|
||||||
builder.SubCommand(P("do")); |
|
||||||
builder.OptionalParams(); |
|
||||||
builder.ParamNumberList(P("numeric list"), P("list")); |
|
||||||
builder.ParamBoolean(P("maybe")); |
|
||||||
|
|
||||||
builder.Option(P("remainder")); |
|
||||||
builder.ParamRemainder(P("everything")); |
|
||||||
|
|
||||||
builder.SubCommand(P("json")); |
|
||||||
builder.ParamJSON(P("first_json")); |
|
||||||
builder.ParamJSONList(P("other_json")); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties |
|
||||||
{ |
|
||||||
} |
|
@ -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" |
|
||||||
} |
|
@ -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.") |
|
||||||
} |
|
@ -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'; |
|
||||||
} |
|
@ -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.") |
|
||||||
} |
|
@ -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' |
|
||||||
} |
|
@ -1,863 +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 Voting extends AcediaObject |
|
||||||
dependsOn(VotingModel) |
|
||||||
dependson(CommandAPI); |
|
||||||
|
|
||||||
//! Class that describes a single voting option. |
|
||||||
//! |
|
||||||
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and |
|
||||||
//! shouldn't be used separately from it. |
|
||||||
//! You shouldn't allocate its instances directly unless you're working on |
|
||||||
//! the [`Commands_Feature`]'s or related code. |
|
||||||
//! |
|
||||||
//! Generally, [`Voting`] will only update whenever its methods are called. |
|
||||||
//! The only exception is when a time limit was specified, then |
|
||||||
//! `TryAnnounceTimer()` will be called every tick, voting emulating countdown. |
|
||||||
//! |
|
||||||
//! ## Usage |
|
||||||
//! |
|
||||||
//! This class takes care of the voting process by itself, one only needs to |
|
||||||
//! call [`Start()`] method. |
|
||||||
//! If you wish to prematurely end voting (e.g. forcing it to end), then call |
|
||||||
//! [`ForceEnding()`] method. |
|
||||||
//! |
|
||||||
//! When implementing your own voting: |
|
||||||
//! |
|
||||||
//! 1. You normally would override [`Execute()`] method to perform any |
|
||||||
//! actions you want upon the voting success. |
|
||||||
//! 2. If you want your voting to take any arguments, you should also |
|
||||||
//! overload [`AddInfo()`]. |
|
||||||
//! 3. You can also override [`HandleVotingStart()`] method to setup custom |
|
||||||
//! voting's messages inside `currentAnnouncements` or to reject |
|
||||||
//! starting this voting altogether. |
|
||||||
|
|
||||||
/// Describes possible results of [`ForceEnding()`] method. |
|
||||||
enum ForceEndingOutcome { |
|
||||||
/// Voting forcing was successful. |
|
||||||
FEO_Success, |
|
||||||
/// User is not allowed to force voting. |
|
||||||
FEO_Forbidden, |
|
||||||
/// No voting to end at this moment. |
|
||||||
FEO_NotApplicable |
|
||||||
}; |
|
||||||
|
|
||||||
/******************************************************************************* |
|
||||||
* Voting settings that should be specified for child classes. |
|
||||||
******************************************************************************/ |
|
||||||
|
|
||||||
/// During its lifecycle voting outputs several messages to players about its |
|
||||||
/// current state. |
|
||||||
/// This struct contains all such messages. |
|
||||||
struct VotingAnnouncementSet { |
|
||||||
/// Message that is displayed when voting starts, inviting others to vote |
|
||||||
/// (e.g. "Voting to end trader has started") |
|
||||||
var public Text started; |
|
||||||
/// Message that is displayed once voting succeeds |
|
||||||
/// (e.g. "{$TextPositive Voting to end trader was successful}") |
|
||||||
var public Text succeeded; |
|
||||||
/// Message that is displayed once voting succeeds |
|
||||||
/// (e.g. "{$TextNegative Voting to end trader has failed}") |
|
||||||
var public Text failed; |
|
||||||
/// Message that is displayed when voting info is displayed mid-voting. |
|
||||||
/// (e.g. "Voting to end trader currently active.") |
|
||||||
var public Text info; |
|
||||||
}; |
|
||||||
/// Variable that contains current messages that voting will use to communicate |
|
||||||
/// its status to the players. |
|
||||||
/// |
|
||||||
/// It is auto-filled with `string` values [`votingStartedLine`], |
|
||||||
/// [`votingSucceededLine`], [`votingFailedLine`], [`votingInfoLine`]. |
|
||||||
/// If you want to change/customize these values based on voting arguments, |
|
||||||
/// then override [`HandleVotingStart()`] method and set values inside of this |
|
||||||
/// variable directly (they will all be `none` at this point). |
|
||||||
var protected VotingAnnouncementSet currentAnnouncements; |
|
||||||
|
|
||||||
/// Preferred name of this voting. Actual name is decided by |
|
||||||
/// server owner/mod author. |
|
||||||
/// |
|
||||||
/// Has to satisfy limitations described in the `BaseText::IsValidName()` |
|
||||||
var protected const string preferredName; |
|
||||||
/// Text that should be displayed when voting starts. |
|
||||||
/// |
|
||||||
/// There isn't any hard limitations, but for the sake of uniformity try to |
|
||||||
/// mimic "Voting to end trader has started" line, avoid adding formatting and |
|
||||||
/// don't add comma/exclamation mark at the end. |
|
||||||
var protected const string votingStartedLine; |
|
||||||
/// Text that should be displayed when voting has ended with a success. |
|
||||||
/// |
|
||||||
/// There isn't any hard limitations, but for the sake of uniformity try to |
|
||||||
/// mimic "{$TextPositive Voting to end trader was successful}" line, coloring |
|
||||||
/// it in a positive color and adding comma/exclamation mark at the end. |
|
||||||
var protected const string votingSucceededLine; |
|
||||||
/// Text that should be displayed when voting has ended in a failure. |
|
||||||
/// |
|
||||||
/// There isn't any hard limitations, but for the sake of uniformity try to |
|
||||||
/// mimic "{$TextNegative Voting to end trader has failed}" line, coloring it in |
|
||||||
/// a negative color and adding comma/exclamation mark at the end. |
|
||||||
var protected const string votingFailedLine; |
|
||||||
/// Text that should be displayed when voting info is displayed mid-voting. |
|
||||||
/// |
|
||||||
/// There isn't any hard limitations, but for the sake of uniformity try to |
|
||||||
/// mimic "Voting to end trader is currently active." line, avoid adding |
|
||||||
/// formatting and don't add comma/exclamation mark at the end. |
|
||||||
var protected const string votingInfoLine; |
|
||||||
/// Settings variable that defines a class to be used for this [`Voting`]'s |
|
||||||
/// permissions config. |
|
||||||
var protected const class<VotingPermissions> permissionsConfigClass; |
|
||||||
|
|
||||||
/******************************************************************************* |
|
||||||
* Variables that describe current state of the voting. |
|
||||||
******************************************************************************/ |
|
||||||
|
|
||||||
/// Underlying voting model that does actual vote calculations. |
|
||||||
var private VotingModel model; |
|
||||||
/// How much time remains in the voting. |
|
||||||
/// Both negative and zero values mean that countdown either ended or wasn't |
|
||||||
/// started to begin with. |
|
||||||
/// This value can only be decreased inside [`TryAnnounceTimer()`] event method; |
|
||||||
/// voting end due to countdown is also expected to be handled there. |
|
||||||
var private float remainingVotingTime; |
|
||||||
/// Tracks index of the next timing inside [`announcementTimings`] to announce. |
|
||||||
var private int nextTimingToAnnounce; |
|
||||||
/// Records whether end of the voting announcement was already made. |
|
||||||
var private bool endingHandled; |
|
||||||
/// Arguments that this voting was called with. |
|
||||||
var private HashTable usedArguments; |
|
||||||
|
|
||||||
var private array<Text> policyAllowedToVoteGroups; |
|
||||||
var private array<Text> policyAllowedToSeeVotingGroups; |
|
||||||
var private array<Text> policyAllowedToForceVoting; |
|
||||||
var private bool policySpectatorsCanVote; |
|
||||||
|
|
||||||
/// Fake voters that are only used in debug mode to allow for simpler vote |
|
||||||
/// testing. |
|
||||||
var private array<UserID> debugVoters; |
|
||||||
|
|
||||||
// Timings at which to announce how much time is left for this voting |
|
||||||
var private const array<int> announcementTimings; |
|
||||||
|
|
||||||
/******************************************************************************* |
|
||||||
* Auxiliary variables (`string`s + templates from them) used for producing |
|
||||||
* output to the user. |
|
||||||
******************************************************************************/ |
|
||||||
|
|
||||||
/// Text that serves as a template for announcing current vote counts. |
|
||||||
var private const string voteSummaryTemplateString; |
|
||||||
/// Text that serves as a template for announcing player making a new vote. |
|
||||||
var private const string playerVotedTemplateString, playerVotedAnonymousTemplateString; |
|
||||||
var private const string timeRemaningAnnounceTemplateString; |
|
||||||
// [`TextTemplate`]s made from the above `string` templates. |
|
||||||
var private TextTemplate voteSummaryTemplate, playerVotedTemplate, playerVotedAnonymousTemplate; |
|
||||||
var private TextTemplate timeRemaningAnnounceTemplate; |
|
||||||
/// Text that is used instead of how to vote hint for players not allowed |
|
||||||
/// to vote |
|
||||||
var private const string cannotVoteHint; |
|
||||||
|
|
||||||
/******************************************************************************* |
|
||||||
* Signals. |
|
||||||
******************************************************************************/ |
|
||||||
var private CommandsAPI_OnVotingEnded_Signal onVotingEndedSignal; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
nextTimingToAnnounce = 0; |
|
||||||
voteSummaryTemplate = _.text.MakeTemplate_S(voteSummaryTemplateString); |
|
||||||
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedTemplateString); |
|
||||||
playerVotedAnonymousTemplate = _.text.MakeTemplate_S(playerVotedAnonymousTemplateString); |
|
||||||
timeRemaningAnnounceTemplate = _.text.MakeTemplate_S(timeRemaningAnnounceTemplateString); |
|
||||||
onVotingEndedSignal = CommandsAPI_OnVotingEnded_Signal( |
|
||||||
_.memory.Allocate(class'CommandsAPI_OnVotingEnded_Signal')); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_.memory.Free(model); |
|
||||||
model = none; |
|
||||||
endingHandled = false; |
|
||||||
policySpectatorsCanVote = false; |
|
||||||
|
|
||||||
_.memory.Free2(usedArguments, onVotingEndedSignal); |
|
||||||
usedArguments = none; |
|
||||||
onVotingEndedSignal = none; |
|
||||||
_server.unreal.OnTick(self).Disconnect(); |
|
||||||
|
|
||||||
_.memory.Free4(currentAnnouncements.started, |
|
||||||
currentAnnouncements.succeeded, |
|
||||||
currentAnnouncements.failed, |
|
||||||
currentAnnouncements.info); |
|
||||||
currentAnnouncements.started = none; |
|
||||||
currentAnnouncements.succeeded = none; |
|
||||||
currentAnnouncements.failed = none; |
|
||||||
currentAnnouncements.info = none; |
|
||||||
|
|
||||||
_.memory.Free4(voteSummaryTemplate, |
|
||||||
playerVotedTemplate, |
|
||||||
playerVotedAnonymousTemplate, |
|
||||||
timeRemaningAnnounceTemplate); |
|
||||||
voteSummaryTemplate = none; |
|
||||||
playerVotedTemplate = none; |
|
||||||
playerVotedAnonymousTemplate = none; |
|
||||||
timeRemaningAnnounceTemplate = none; |
|
||||||
|
|
||||||
_.memory.FreeMany(policyAllowedToVoteGroups); |
|
||||||
_.memory.FreeMany(policyAllowedToSeeVotingGroups); |
|
||||||
_.memory.FreeMany(policyAllowedToForceVoting); |
|
||||||
policyAllowedToVoteGroups.length = 0; |
|
||||||
policyAllowedToSeeVotingGroups.length = 0; |
|
||||||
policyAllowedToForceVoting.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
/// Signal that will be emitted when voting ends. |
|
||||||
/// |
|
||||||
/// # Slot description |
|
||||||
/// |
|
||||||
/// bool <slot>(bool success, HashTable arguments) |
|
||||||
/// |
|
||||||
/// ## Parameters |
|
||||||
/// |
|
||||||
/// * [`success`]: `true` if voting ended successfully and `false` otherwise. |
|
||||||
/// * [`arguments`]: Arguments with which voting was called. |
|
||||||
public /*signal*/ function CommandsAPI_OnVotingEnded_Slot OnVotingEnded(AcediaObject receiver) { |
|
||||||
return CommandsAPI_OnVotingEnded_Slot(onVotingEndedSignal.NewSlot(receiver)); |
|
||||||
} |
|
||||||
|
|
||||||
/// Override this to specify arguments for your voting command. |
|
||||||
/// |
|
||||||
/// This method is for adding arguments only. |
|
||||||
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or |
|
||||||
/// [`CommandDataBuilder::Option()`] methods, otherwise you'll cause unexpected |
|
||||||
/// behavior for your mod's users. |
|
||||||
public static function AddInfo(CommandDataBuilder builder) { |
|
||||||
} |
|
||||||
|
|
||||||
/// Loads permissions config with a given name for the caller [`Voting`] class. |
|
||||||
/// |
|
||||||
/// Permission configs describe allowed usage of the [`Voting`]. |
|
||||||
/// Basic settings are contained inside [`VotingPermissions`], but votings |
|
||||||
/// should derive their own child classes for storing their settings. |
|
||||||
/// |
|
||||||
/// Returns `none` if caller [`Voting`] 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 VotingPermissions LoadConfig(BaseText configName) { |
|
||||||
if (configName == none) return none; |
|
||||||
if (default.permissionsConfigClass == none) return none; |
|
||||||
|
|
||||||
default.permissionsConfigClass.static.Initialize(); |
|
||||||
// This creates default config if it is missing |
|
||||||
default.permissionsConfigClass.static.NewConfig(configName); |
|
||||||
return VotingPermissions(default.permissionsConfigClass.static |
|
||||||
.GetConfigInstance(configName)); |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns name of this voting in the lower case. |
|
||||||
/// |
|
||||||
/// If voting class was configured incorrectly (with a `preferredName` |
|
||||||
/// that doesn't satisfy limitations, described in `BaseText::IsValidName()`), |
|
||||||
/// then this method will return `none`. |
|
||||||
public final static function Text GetPreferredName() { |
|
||||||
local Text result; |
|
||||||
|
|
||||||
result = __().text.FromString(Locs(default.preferredName)); |
|
||||||
if (result.IsValidName()) { |
|
||||||
return result; |
|
||||||
} |
|
||||||
__().memory.Free(result); |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Forcibly ends the voting, deciding winner depending on the argument. |
|
||||||
/// By default decides result by the votes that already have been cast. |
|
||||||
/// |
|
||||||
/// Only does anything if voting is currently in progress |
|
||||||
/// (in `VPM_InProgress` state). |
|
||||||
public final function ForceEndingOutcome ForceEnding( |
|
||||||
EPlayer instigator, |
|
||||||
VotingModel.ForceEndingType type |
|
||||||
) { |
|
||||||
local int i; |
|
||||||
local UserID id; |
|
||||||
local bool canForce; |
|
||||||
|
|
||||||
if (model == none) return FEO_NotApplicable; |
|
||||||
if (instigator == none) return FEO_Forbidden; |
|
||||||
id = instigator.GetUserID(); |
|
||||||
if (id == none) return FEO_Forbidden; |
|
||||||
|
|
||||||
for (i = 0; i < policyAllowedToForceVoting.length; i += 1) { |
|
||||||
if (_.users.IsUserIDInGroup(id, policyAllowedToForceVoting[i])) { |
|
||||||
canForce = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (canForce) { |
|
||||||
if (model.ForceEnding(type)) { |
|
||||||
TryEnding(instigator); |
|
||||||
return FEO_Success; |
|
||||||
} |
|
||||||
return FEO_NotApplicable; |
|
||||||
} |
|
||||||
_.memory.Free(id); |
|
||||||
return FEO_Forbidden; |
|
||||||
} |
|
||||||
|
|
||||||
/// Starts caller [`Voting`] using policies, loaded from the given config. |
|
||||||
/// |
|
||||||
/// Provided config instance must not be `none`, otherwise method is guaranteed |
|
||||||
/// to fail with `SVR_InvalidState`. |
|
||||||
/// Method will also fail if voting was already started (even if it already |
|
||||||
/// ended), there is no one eligible to vote or [`Voting`] itself has decided to |
|
||||||
/// reject being started at this moment, with given arguments. |
|
||||||
public final function CommandAPI.StartVotingResult Start( |
|
||||||
VotingPermissions config, |
|
||||||
HashTable arguments |
|
||||||
) { |
|
||||||
local bool hasDebugVoters; |
|
||||||
local array<EPlayer> voters; |
|
||||||
|
|
||||||
if (model != none) return SVR_InvalidState; |
|
||||||
if (config == none) return SVR_InvalidState; |
|
||||||
|
|
||||||
// Check whether we even have enough voters |
|
||||||
ReadConfigIntoPolicies(config); // we need to know permission policies |
|
||||||
voters = FindAllVotingPlayers(); |
|
||||||
hasDebugVoters = _.environment.IsDebugging() |
|
||||||
&& class'ACommandFakers'.static.BorrowDebugVoters().length > 0; |
|
||||||
if (voters.length == 0 && !hasDebugVoters) { |
|
||||||
return SVR_NoVoters; |
|
||||||
} |
|
||||||
|
|
||||||
// Check if voting even wants to start with these arguments |
|
||||||
if (HandleVotingStart(config, arguments)) { |
|
||||||
// ^ this was supposed to pre-fill `currentAnnouncements` struct if |
|
||||||
// it wanted to change any messages, so now is the good time to fill |
|
||||||
// the rest with defaults/fallback |
|
||||||
FillAnnouncementGaps(); |
|
||||||
if (arguments != none) { |
|
||||||
arguments.NewRef(); |
|
||||||
usedArguments = arguments; |
|
||||||
} |
|
||||||
} else { |
|
||||||
_.memory.FreeMany(voters); |
|
||||||
return SVR_Rejected; |
|
||||||
} |
|
||||||
|
|
||||||
// Actually start voting |
|
||||||
model = VotingModel(_.memory.Allocate(class'VotingModel')); |
|
||||||
model.Start(config.drawEqualsSuccess); |
|
||||||
// Inform new voting about fake voters, in case we're debugging |
|
||||||
if (hasDebugVoters) { |
|
||||||
// This method will call also `UpdateVoters()` |
|
||||||
SetDebugVoters(class'ACommandFakers'.static.BorrowDebugVoters()); |
|
||||||
} else { |
|
||||||
UpdateVoters(voters); |
|
||||||
} |
|
||||||
SetupCountdownTimer(config); |
|
||||||
AnnounceStart(); |
|
||||||
_.memory.FreeMany(voters); |
|
||||||
return SVR_Success; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if the [`Voting`] process has reached its conclusion. |
|
||||||
/// |
|
||||||
/// Please note that this differs from determining whether voting is currently |
|
||||||
// active. Even voting that hasn't started is not considered concluded. |
|
||||||
public final function bool HasEnded() { |
|
||||||
if (model == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
return model.HasEnded(); |
|
||||||
} |
|
||||||
|
|
||||||
/// Retrieves the current voting status for the specified voter. |
|
||||||
/// |
|
||||||
/// If the voter was previously eligible to vote, cast a vote, but later had |
|
||||||
/// their voting rights revoked, their vote will not be counted, and this method |
|
||||||
/// will return [`PVS_NoVote`]. |
|
||||||
/// |
|
||||||
/// In case the voter regains their voting rights while the voting process is |
|
||||||
/// still ongoing, their previous vote will be automatically reinstated by |
|
||||||
/// the caller [`Voting`]. |
|
||||||
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) { |
|
||||||
if (model != none) { |
|
||||||
return model.GetVote(voter); |
|
||||||
} |
|
||||||
return PVS_NoVote; |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds specified [`UserID`]s as additional voters in debug mode. |
|
||||||
/// |
|
||||||
/// This method is intended for debugging purposes and only functions when |
|
||||||
/// the game is running in debug mode. |
|
||||||
public final function SetDebugVoters(array<UserID> newDebugVoters) { |
|
||||||
local int i; |
|
||||||
local array<EPlayer> realVoters; |
|
||||||
|
|
||||||
if(!_.environment.IsDebugging()) { |
|
||||||
return; |
|
||||||
} |
|
||||||
_.memory.FreeMany(debugVoters); |
|
||||||
debugVoters.length = 0; |
|
||||||
for (i = 0; i < newDebugVoters.length; i += 1) { |
|
||||||
if (newDebugVoters[i] != none) { |
|
||||||
debugVoters[debugVoters.length] = newDebugVoters[i]; |
|
||||||
newDebugVoters[i].NewRef(); |
|
||||||
} |
|
||||||
} |
|
||||||
realVoters = FindAllVotingPlayers(); |
|
||||||
UpdateVoters(realVoters); |
|
||||||
_.memory.FreeMany(realVoters); |
|
||||||
TryEnding(); |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds a new vote by a given [`UserID`]. |
|
||||||
/// |
|
||||||
/// NOTE: this method is intended for use only in debug mode, and is will not do |
|
||||||
/// anything otherwise. This method silently adds a vote using the provided |
|
||||||
/// [`UserID`], without any prompt or notification of updated voting status. |
|
||||||
/// It was added to facilitate testing with fake [`UserID`]s, and is limited |
|
||||||
/// to debug mode to prevent misuse and unintended behavior in production code. |
|
||||||
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) { |
|
||||||
local array<EPlayer> realVoters; |
|
||||||
local VotingModel.VotingResult result; |
|
||||||
|
|
||||||
if (model == none) return VFR_NotAllowed; |
|
||||||
if (voter == none) return VFR_NotAllowed; |
|
||||||
if (!_.environment.IsDebugging()) return VFR_NotAllowed; |
|
||||||
|
|
||||||
realVoters = FindAllVotingPlayers(); |
|
||||||
UpdateVoters(realVoters); |
|
||||||
result = model.CastVote(voter, voteForSuccess); |
|
||||||
if (result == VFR_Success) { |
|
||||||
AnnounceNewVote(none, voteForSuccess); |
|
||||||
} |
|
||||||
TryEnding(); |
|
||||||
_.memory.FreeMany(realVoters); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Registers a vote on behalf of the specified player. |
|
||||||
/// |
|
||||||
/// This method updates the voting status for the specified player and may |
|
||||||
/// initiate the conclusion of the voting process. |
|
||||||
/// After a vote is registered, the updated voting status is broadcast to all |
|
||||||
/// players. |
|
||||||
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) { |
|
||||||
local UserID voterID; |
|
||||||
local array<EPlayer> realVoters; |
|
||||||
local VotingModel.VotingResult result; |
|
||||||
|
|
||||||
if (model == none) return VFR_NotAllowed; |
|
||||||
if (voter == none) return VFR_NotAllowed; |
|
||||||
|
|
||||||
voterID = voter.GetUserID(); |
|
||||||
realVoters = FindAllVotingPlayers(); |
|
||||||
UpdateVoters(realVoters); |
|
||||||
result = model.CastVote(voterID, voteForSuccess); |
|
||||||
switch (result) { |
|
||||||
case VFR_Success: |
|
||||||
AnnounceNewVote(voter, voteForSuccess); |
|
||||||
break; |
|
||||||
case VFR_NotAllowed: |
|
||||||
voter |
|
||||||
.BorrowConsole() |
|
||||||
.WriteLine(F("You are {$TextNegative not allowed} to vote right now.")); |
|
||||||
break; |
|
||||||
case VFR_CannotChangeVote: |
|
||||||
voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}.")); |
|
||||||
break; |
|
||||||
case VFR_VotingEnded: |
|
||||||
voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!")); |
|
||||||
break; |
|
||||||
default: |
|
||||||
} |
|
||||||
TryEnding(); |
|
||||||
_.memory.Free(voterID); |
|
||||||
_.memory.FreeMany(realVoters); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Prints information about caller [`Voting`] to the given player. |
|
||||||
public final function PrintVotingInfoFor(EPlayer requester) { |
|
||||||
local ConsoleWriter writer; |
|
||||||
local MutableText summaryPart, timeRemaining; |
|
||||||
|
|
||||||
if (requester == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
voteSummaryTemplate.Reset(); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesFor()); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesAgainst()); |
|
||||||
summaryPart = voteSummaryTemplate.CollectFormattedM(); |
|
||||||
writer = requester.BorrowConsole(); |
|
||||||
writer.Write(currentAnnouncements.info); |
|
||||||
writer.Write(P(". ")); |
|
||||||
writer.Write(summaryPart); |
|
||||||
writer.WriteLine(P(".")); |
|
||||||
if (remainingVotingTime > 0) { |
|
||||||
timeRemaining = _.text.FromIntM(int(Ceil(remainingVotingTime))); |
|
||||||
writer.Write(P("Time remaining: ")); |
|
||||||
writer.Write(timeRemaining); |
|
||||||
writer.WriteLine(P(" seconds.")); |
|
||||||
_.memory.Free(timeRemaining); |
|
||||||
} |
|
||||||
_.memory.Free(summaryPart); |
|
||||||
} |
|
||||||
|
|
||||||
/// Override this to perform necessary logic after voting has succeeded. |
|
||||||
protected function Execute(HashTable arguments) {} |
|
||||||
|
|
||||||
/// Override this method to: |
|
||||||
/// |
|
||||||
/// 1. Specify any of the messages inside `currentAnnouncements` to fit passed |
|
||||||
/// [`arguments`]. |
|
||||||
/// 2. Optionally reject starting this voting altogether by returning `false` |
|
||||||
/// (returning `true` will allow voting to proceed). |
|
||||||
protected function bool HandleVotingStart(VotingPermissions config, HashTable arguments) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint. |
|
||||||
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases |
|
||||||
// aren't properly setup. |
|
||||||
private final function MutableText MakeHowToVoteHint() { |
|
||||||
local Text resolvedAlias; |
|
||||||
local MutableText result; |
|
||||||
|
|
||||||
result = P("Say ").MutableCopy(); |
|
||||||
resolvedAlias = _.alias.ResolveCommand(P("yes")); |
|
||||||
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) { |
|
||||||
result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
|
||||||
} else { |
|
||||||
result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
|
||||||
} |
|
||||||
_.memory.Free(resolvedAlias); |
|
||||||
result.Append(P(" or ")); |
|
||||||
resolvedAlias = _.alias.ResolveCommand(P("no")); |
|
||||||
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) { |
|
||||||
result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative)); |
|
||||||
} else { |
|
||||||
result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative)); |
|
||||||
} |
|
||||||
_.memory.Free(resolvedAlias); |
|
||||||
result.Append(P(" to vote")); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
private final function ReadConfigIntoPolicies(VotingPermissions config) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (config != none) { |
|
||||||
policySpectatorsCanVote = config.allowSpectatorVoting; |
|
||||||
for (i = 0; i < config.allowedToVoteGroup.length; i += 1) { |
|
||||||
policyAllowedToVoteGroups[i] = _.text.FromString(config.allowedToVoteGroup[i]); |
|
||||||
} |
|
||||||
for (i = 0; i < config.allowedToSeeVotesGroup.length; i += 1) { |
|
||||||
policyAllowedToSeeVotingGroups[i] = _.text.FromString(config.allowedToSeeVotesGroup[i]); |
|
||||||
} |
|
||||||
for (i = 0; i < config.allowedToForceGroup.length; i += 1) { |
|
||||||
policyAllowedToForceVoting[i] = _.text.FromString(config.allowedToForceGroup[i]); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function SetupCountdownTimer(VotingPermissions config) { |
|
||||||
if (config != none && config.votingTime > 0) { |
|
||||||
remainingVotingTime = config.votingTime; |
|
||||||
_server.unreal.OnTick(self).connect = TryAnnounceTimer; |
|
||||||
nextTimingToAnnounce = 0; |
|
||||||
while (nextTimingToAnnounce < announcementTimings.length) { |
|
||||||
if (announcementTimings[nextTimingToAnnounce] <= remainingVotingTime) { |
|
||||||
break; |
|
||||||
} |
|
||||||
nextTimingToAnnounce += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private function TryAnnounceTimer(float delta, float dilationCoefficient) { |
|
||||||
local MutableText message; |
|
||||||
local ConsoleWriter writer; |
|
||||||
|
|
||||||
if (remainingVotingTime <= 0) { |
|
||||||
return; |
|
||||||
} |
|
||||||
remainingVotingTime -= delta / dilationCoefficient; |
|
||||||
if (remainingVotingTime <= 0) { |
|
||||||
model.ForceEnding(); |
|
||||||
TryEnding(); |
|
||||||
return; |
|
||||||
} |
|
||||||
if (nextTimingToAnnounce >= announcementTimings.length) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (announcementTimings[nextTimingToAnnounce] > int(remainingVotingTime)) { |
|
||||||
writer = _.console.ForAll(); |
|
||||||
timeRemaningAnnounceTemplate.Reset(); |
|
||||||
timeRemaningAnnounceTemplate.ArgInt(announcementTimings[nextTimingToAnnounce]); |
|
||||||
message = timeRemaningAnnounceTemplate.CollectFormattedM(); |
|
||||||
writer.WriteLine(message); |
|
||||||
_.memory.Free(writer); |
|
||||||
nextTimingToAnnounce += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Outputs message about new vote being submitted to all relevant voters. |
|
||||||
private final function AnnounceNewVote(EPlayer voter, bool voteForSuccess) { |
|
||||||
local int i, j; |
|
||||||
local bool playerAllowedToSee; |
|
||||||
local Text voterName; |
|
||||||
local array<EPlayer> allPlayers; |
|
||||||
local UserID nextID; |
|
||||||
local MutableText playerVotedPart, playerVotedAnonymousPart, summaryPart; |
|
||||||
|
|
||||||
voteSummaryTemplate.Reset(); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesFor()); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesAgainst()); |
|
||||||
summaryPart = voteSummaryTemplate.CollectFormattedM(); |
|
||||||
|
|
||||||
playerVotedTemplate.Reset(); |
|
||||||
playerVotedAnonymousTemplate.Reset(); |
|
||||||
if (voter != none) { |
|
||||||
voterName = voter.GetName(); |
|
||||||
} else { |
|
||||||
voterName = P("DEBUG:FAKER").Copy(); |
|
||||||
} |
|
||||||
playerVotedTemplate.TextArg(P("player_name"), voterName, true); |
|
||||||
_.memory.Free(voterName); |
|
||||||
if (voteForSuccess) { |
|
||||||
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}")); |
|
||||||
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextPositive for}")); |
|
||||||
} else { |
|
||||||
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}")); |
|
||||||
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextNegative against}")); |
|
||||||
} |
|
||||||
playerVotedPart = playerVotedTemplate.CollectFormattedM(); |
|
||||||
playerVotedAnonymousPart = playerVotedAnonymousTemplate.CollectFormattedM(); |
|
||||||
allPlayers = _.players.GetAll(); |
|
||||||
for (i = 0; i < allPlayers.length; i += 1) { |
|
||||||
nextID = allPlayers[i].GetUserID(); |
|
||||||
playerAllowedToSee = false; |
|
||||||
for (j = 0; j < policyAllowedToSeeVotingGroups.length; j += 1) { |
|
||||||
if (_.users.IsUserIDInGroup(nextID, policyAllowedToSeeVotingGroups[j])) { |
|
||||||
playerAllowedToSee = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (playerAllowedToSee) { |
|
||||||
allPlayers[i].BorrowConsole().Write(playerVotedPart); |
|
||||||
} else { |
|
||||||
allPlayers[i].BorrowConsole().Write(playerVotedAnonymousPart); |
|
||||||
} |
|
||||||
allPlayers[i].BorrowConsole().Write(P(". ")).Write(summaryPart).WriteLine(P(".")); |
|
||||||
_.memory.Free(nextID); |
|
||||||
} |
|
||||||
_.memory.Free3(playerVotedPart, playerVotedAnonymousPart, summaryPart); |
|
||||||
_.memory.FreeMany(allPlayers); |
|
||||||
} |
|
||||||
|
|
||||||
/// Tries to end voting. |
|
||||||
/// |
|
||||||
/// Returns `true` iff this method was called for the first time after |
|
||||||
/// the voting concluded. |
|
||||||
private final function bool TryEnding(optional EPlayer forcedBy) { |
|
||||||
local Text outcomeMessage; |
|
||||||
|
|
||||||
if (model == none) return false; |
|
||||||
if (endingHandled) return false; |
|
||||||
if (!HasEnded()) return false; |
|
||||||
|
|
||||||
endingHandled = true; |
|
||||||
if (model.GetStatus() == VPM_Success) { |
|
||||||
outcomeMessage = currentAnnouncements.succeeded; |
|
||||||
} else { |
|
||||||
outcomeMessage = currentAnnouncements.failed; |
|
||||||
} |
|
||||||
onVotingEndedSignal.Emit(model.GetStatus() == VPM_Success, usedArguments); |
|
||||||
AnnounceOutcome(outcomeMessage, forcedBy); |
|
||||||
if (model.GetStatus() == VPM_Success) { |
|
||||||
Execute(usedArguments); |
|
||||||
} |
|
||||||
_server.unreal.OnTick(self).Disconnect(); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
private final function FillAnnouncementGaps() { |
|
||||||
if (currentAnnouncements.started == none) { |
|
||||||
currentAnnouncements.started = _.text.FromFormattedString(votingStartedLine); |
|
||||||
} |
|
||||||
if (currentAnnouncements.succeeded == none) { |
|
||||||
currentAnnouncements.succeeded = _.text.FromFormattedString(votingSucceededLine); |
|
||||||
} |
|
||||||
if (currentAnnouncements.failed == none) { |
|
||||||
currentAnnouncements.failed = _.text.FromFormattedString(votingFailedLine); |
|
||||||
} |
|
||||||
if (currentAnnouncements.info == none) { |
|
||||||
currentAnnouncements.info = _.text.FromFormattedString(votingInfoLine); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function array<EPlayer> FindAllVotingPlayers() { |
|
||||||
local int i, j; |
|
||||||
local bool userAllowedToVote; |
|
||||||
local UserID nextID; |
|
||||||
local array<EPlayer> currentPlayers, voterPlayers; |
|
||||||
|
|
||||||
currentPlayers = _.players.GetAll(); |
|
||||||
for (i = 0; i < currentPlayers.length; i += 1) { |
|
||||||
if (!policySpectatorsCanVote && currentPlayers[i].IsSpectator()) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
nextID = currentPlayers[i].GetUserID(); |
|
||||||
userAllowedToVote = false; |
|
||||||
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) { |
|
||||||
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) { |
|
||||||
userAllowedToVote = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (userAllowedToVote) { |
|
||||||
currentPlayers[i].NewRef(); |
|
||||||
voterPlayers[voterPlayers.length] = currentPlayers[i]; |
|
||||||
} |
|
||||||
_.memory.Free(nextID); |
|
||||||
} |
|
||||||
_.memory.FreeMany(currentPlayers); |
|
||||||
return voterPlayers; |
|
||||||
} |
|
||||||
|
|
||||||
/// Updates the inner voting model with current list of players allowed to vote. |
|
||||||
/// Also returns said list. |
|
||||||
private final function UpdateVoters(array<EPlayer> votingPlayers) { |
|
||||||
local int i; |
|
||||||
local array<UserID> votersIDs; |
|
||||||
|
|
||||||
if (model == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
for (i = 0; i < votingPlayers.length; i += 1) { |
|
||||||
votersIDs[votersIDs.length] = votingPlayers[i].GetUserID(); |
|
||||||
} |
|
||||||
for (i = 0; i < debugVoters.length; i += 1) { |
|
||||||
debugVoters[i].NewRef(); |
|
||||||
votersIDs[votersIDs.length] = debugVoters[i]; |
|
||||||
} |
|
||||||
model.UpdatePotentialVoters(votersIDs); |
|
||||||
_.memory.FreeMany(votersIDs); |
|
||||||
} |
|
||||||
|
|
||||||
/// Prints given voting outcome message in console and publishes it as |
|
||||||
/// a notification. |
|
||||||
private final function AnnounceStart() { |
|
||||||
local int i, j; |
|
||||||
local bool playerAllowedToSee; |
|
||||||
local UserID nextID; |
|
||||||
local MutableText howToVoteHint; |
|
||||||
local array<EPlayer> currentPlayers; |
|
||||||
|
|
||||||
howToVoteHint = MakeHowToVoteHint(); |
|
||||||
currentPlayers = _.players.GetAll(); |
|
||||||
for (i = 0; i < currentPlayers.length; i += 1) { |
|
||||||
nextID = currentPlayers[i].GetUserID(); |
|
||||||
playerAllowedToSee = false; |
|
||||||
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) { |
|
||||||
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) { |
|
||||||
playerAllowedToSee = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
_.memory.Free(nextID); |
|
||||||
if (playerAllowedToSee) { |
|
||||||
currentPlayers[i].Notify(currentAnnouncements.started, howToVoteHint,, P("voting")); |
|
||||||
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started); |
|
||||||
currentPlayers[i].BorrowConsole().WriteLine(howToVoteHint); |
|
||||||
} else { |
|
||||||
currentPlayers[i].Notify(currentAnnouncements.started, F(cannotVoteHint),, P("voting")); |
|
||||||
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started); |
|
||||||
} |
|
||||||
} |
|
||||||
_.memory.Free(howToVoteHint); |
|
||||||
_.memory.FreeMany(currentPlayers); |
|
||||||
} |
|
||||||
|
|
||||||
/// Prints given voting outcome message in console and publishes it as |
|
||||||
/// a notification. |
|
||||||
private final function AnnounceOutcome(BaseText outcomeMessage, optional EPlayer forcedBy) { |
|
||||||
local int i; |
|
||||||
local Text playerName; |
|
||||||
local MutableText editedOutcomeMessage, summaryLine; |
|
||||||
local ConsoleWriter writer; |
|
||||||
local array<EPlayer> currentPlayers; |
|
||||||
|
|
||||||
if (model == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
if (outcomeMessage != none) { |
|
||||||
editedOutcomeMessage = outcomeMessage.MutableCopy(); |
|
||||||
} |
|
||||||
if (editedOutcomeMessage != none && forcedBy != none) { |
|
||||||
editedOutcomeMessage.Append(F(" {$TextEmphasis (forced by }")); |
|
||||||
playerName = forcedBy.GetName(); |
|
||||||
editedOutcomeMessage.Append(playerName, _.text.FormattingFromColor(_.color.Gray)); |
|
||||||
_.memory.Free(playerName); |
|
||||||
editedOutcomeMessage.Append(F("{$TextEmphasis )}")); |
|
||||||
} |
|
||||||
voteSummaryTemplate.Reset(); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesFor()); |
|
||||||
voteSummaryTemplate.ArgInt(model.GetVotesAgainst()); |
|
||||||
summaryLine = voteSummaryTemplate.CollectFormattedM(); |
|
||||||
currentPlayers = _.players.GetAll(); |
|
||||||
for (i = 0; i < currentPlayers.length; i += 1) { |
|
||||||
writer = currentPlayers[i].BorrowConsole(); |
|
||||||
writer.Write(editedOutcomeMessage); |
|
||||||
writer.Write(P(" / ")); |
|
||||||
writer.WriteLine(summaryLine); |
|
||||||
currentPlayers[i].Notify(editedOutcomeMessage, summaryLine,, P("voting")); |
|
||||||
} |
|
||||||
_.memory.FreeMany(currentPlayers); |
|
||||||
_.memory.Free(summaryLine); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
// You can override these |
|
||||||
preferredName = "test" |
|
||||||
votingInfoLine = "Debug voting is running" |
|
||||||
votingStartedLine = "Test voting has started" |
|
||||||
votingSucceededLine = "{$TextPositive Test voting passed}" |
|
||||||
votingFailedLine = "{$TextNegative Test voting has failed}" |
|
||||||
permissionsConfigClass = class'VotingPermissions' |
|
||||||
// You cannot override these |
|
||||||
voteSummaryTemplateString = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}" |
|
||||||
playerVotedTemplateString = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting" |
|
||||||
playerVotedAnonymousTemplateString = "Someone has voted %%vote_type%% passing test voting" |
|
||||||
timeRemaningAnnounceTemplateString = "Time remaining for voting: %1 seconds" |
|
||||||
cannotVoteHint = "{$TextNegative You aren't allowed to vote :(}" |
|
||||||
announcementTimings(0) = 60 |
|
||||||
announcementTimings(1) = 30 |
|
||||||
announcementTimings(2) = 15 |
|
||||||
announcementTimings(3) = 10 |
|
||||||
announcementTimings(4) = 5 |
|
||||||
announcementTimings(5) = 4 |
|
||||||
announcementTimings(6) = 3 |
|
||||||
announcementTimings(7) = 2 |
|
||||||
announcementTimings(8) = 1 |
|
||||||
} |
|
@ -1,440 +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 VotingModel extends AcediaObject |
|
||||||
dependsOn(MathApi); |
|
||||||
|
|
||||||
//! This class counts votes according to the configured voting policies. |
|
||||||
//! |
|
||||||
//! Its main purpose is to separate the voting logic from the voting interface, |
|
||||||
//! making the implementation simpler and the logic easier to test. |
|
||||||
//! |
|
||||||
//! # Usage |
|
||||||
//! |
|
||||||
//! 1. Allocate an instance of the [`VotingModel`] class. |
|
||||||
//! 2. Call [`Start()`] to start voting with required policies. |
|
||||||
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote. |
|
||||||
//! You can change this set at any time before the voting has concluded. |
|
||||||
//! The method used to recount the votes will depend on the policies set |
|
||||||
//! during the previous [`Initialize()`] call. |
|
||||||
//! 4. Use [`CastVote()`] to add a vote from a user. |
|
||||||
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], |
|
||||||
//! check [`GetStatus()`] to see if the voting has concluded. |
|
||||||
//! Once voting has concluded, the result cannot be changed, so you can |
|
||||||
//! release the reference to the [`VotingModel`] object. |
|
||||||
//! 6. Alternatively, before voting has concluded naturally, you can use |
|
||||||
//! [`ForceEnding()`] method to immediately end voting with result being |
|
||||||
//! determined by provided [`ForceEndingType`] argument. |
|
||||||
|
|
||||||
/// Current state of voting for this model. |
|
||||||
enum VotingModelStatus { |
|
||||||
/// Voting hasn't even started, waiting for [`Initialize()`] call |
|
||||||
VPM_Uninitialized, |
|
||||||
/// Voting is currently in progress |
|
||||||
VPM_InProgress, |
|
||||||
/// Voting has ended with majority for its success |
|
||||||
VPM_Success, |
|
||||||
/// Voting has ended with majority for its failure |
|
||||||
VPM_Failure |
|
||||||
}; |
|
||||||
|
|
||||||
/// A result of user trying to make a vote |
|
||||||
enum VotingResult { |
|
||||||
/// Vote accepted |
|
||||||
VFR_Success, |
|
||||||
/// Voting is not allowed for this particular user |
|
||||||
VFR_NotAllowed, |
|
||||||
/// User already made a vote and changing votes isn't allowed |
|
||||||
VFR_CannotChangeVote, |
|
||||||
/// User has already voted the same way |
|
||||||
VFR_AlreadyVoted, |
|
||||||
/// Voting has already ended and doesn't accept new votes |
|
||||||
VFR_VotingEnded |
|
||||||
}; |
|
||||||
|
|
||||||
/// Checks how given user has voted |
|
||||||
enum PlayerVoteStatus { |
|
||||||
/// User hasn't voted yet |
|
||||||
PVS_NoVote, |
|
||||||
/// User voted for the change |
|
||||||
PVS_VoteFor, |
|
||||||
/// User voted against the change |
|
||||||
PVS_VoteAgainst |
|
||||||
}; |
|
||||||
|
|
||||||
/// 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; |
|
||||||
|
|
||||||
/// Specifies whether draw would count as a victory for corresponding voting. |
|
||||||
var private bool policyDrawWinsVoting; |
|
||||||
|
|
||||||
var private array<UserID> votesFor, votesAgainst; |
|
||||||
/// Votes of people that voted before, but then were forbidden to vote |
|
||||||
/// (either because they have left or simply lost the right to vote) |
|
||||||
var private array<UserID> storedVotesFor, storedVotesAgainst; |
|
||||||
/// List of users currently allowed to vote |
|
||||||
var private array<UserID> allowedVoters; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
status = VPM_Uninitialized; |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_.memory.FreeMany(allowedVoters); |
|
||||||
_.memory.FreeMany(votesFor); |
|
||||||
_.memory.FreeMany(votesAgainst); |
|
||||||
_.memory.FreeMany(storedVotesFor); |
|
||||||
_.memory.FreeMany(storedVotesAgainst); |
|
||||||
allowedVoters.length = 0; |
|
||||||
votesFor.length = 0; |
|
||||||
votesAgainst.length = 0; |
|
||||||
storedVotesFor.length = 0; |
|
||||||
storedVotesAgainst.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
/// Initializes voting by providing it with a set of policies to follow. |
|
||||||
/// |
|
||||||
/// The only available policy is configuring whether draw means victory or loss |
|
||||||
/// in voting. |
|
||||||
/// |
|
||||||
/// Can only be called once, after that will do nothing. |
|
||||||
public final function Start(bool drawWinsVoting) { |
|
||||||
if (status != VPM_Uninitialized) { |
|
||||||
return; |
|
||||||
} |
|
||||||
policyDrawWinsVoting = drawWinsVoting; |
|
||||||
status = VPM_InProgress; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns whether voting has already concluded. |
|
||||||
/// |
|
||||||
/// This method should be checked after both [`CastVote()`] and |
|
||||||
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to |
|
||||||
/// conclude the voting result. |
|
||||||
public final function bool HasEnded() { |
|
||||||
return (status != VPM_Uninitialized && status != VPM_InProgress); |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns current status of voting. |
|
||||||
/// |
|
||||||
/// This method should be checked after both [`CastVote()`] and |
|
||||||
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to |
|
||||||
/// conclude the voting result. |
|
||||||
public final function VotingModelStatus GetStatus() { |
|
||||||
return status; |
|
||||||
} |
|
||||||
|
|
||||||
/// Changes set of [`User`]s that are allowed to vote. |
|
||||||
/// |
|
||||||
/// Generally you want to provide this method with a list of current players, |
|
||||||
/// optionally filtered from spectators, users not in priviledged group or any |
|
||||||
/// other relevant criteria. |
|
||||||
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
_.memory.FreeMany(allowedVoters); |
|
||||||
allowedVoters.length = 0; |
|
||||||
for (i = 0; i < potentialVoters.length; i += 1) { |
|
||||||
potentialVoters[i].NewRef(); |
|
||||||
allowedVoters[i] = potentialVoters[i]; |
|
||||||
} |
|
||||||
RestoreStoredVoters(potentialVoters); |
|
||||||
FilterCurrentVoters(potentialVoters); |
|
||||||
RecountVotes(); |
|
||||||
} |
|
||||||
|
|
||||||
/// Attempts to add a vote from specified user. |
|
||||||
/// |
|
||||||
/// Adding a vote can fail if [`voter`] isn't allowed to vote. |
|
||||||
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) { |
|
||||||
local bool votesSameWay; |
|
||||||
local PlayerVoteStatus currentVote; |
|
||||||
|
|
||||||
if (status != VPM_InProgress) { |
|
||||||
return VFR_VotingEnded; |
|
||||||
} |
|
||||||
if (!IsVotingAllowedFor(voter)) { |
|
||||||
return VFR_NotAllowed; |
|
||||||
} |
|
||||||
currentVote = GetVote(voter); |
|
||||||
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) |
|
||||||
|| (!voteForSuccess && currentVote == PVS_VoteAgainst); |
|
||||||
if (votesSameWay) { |
|
||||||
return VFR_AlreadyVoted; |
|
||||||
} |
|
||||||
EraseVote(voter); |
|
||||||
voter.NewRef(); |
|
||||||
if (voteForSuccess) { |
|
||||||
votesFor[votesFor.length] = voter; |
|
||||||
} else { |
|
||||||
votesAgainst[votesAgainst.length] = voter; |
|
||||||
} |
|
||||||
RecountVotes(); |
|
||||||
return VFR_Success; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if the provided user is allowed to vote based on the current list of |
|
||||||
/// potential voters. |
|
||||||
/// |
|
||||||
/// The right to vote is decided solely by the list of potential voters set |
|
||||||
/// using [`UpdatePotentialVoters()`]. |
|
||||||
/// |
|
||||||
/// Returns true if the user is allowed to vote, false otherwise. |
|
||||||
public final function bool IsVotingAllowedFor(UserID voter) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (voter == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (i = 0; i < allowedVoters.length; i += 1) { |
|
||||||
if (voter.IsEqual(allowedVoters[i])) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the current vote status for the given voter. |
|
||||||
/// |
|
||||||
/// If the voter was previously allowed to vote, voted, and had their right to |
|
||||||
/// vote revoked, their vote won't count. |
|
||||||
public final function PlayerVoteStatus GetVote(UserID voter) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (voter == none) { |
|
||||||
return PVS_NoVote; |
|
||||||
} |
|
||||||
for (i = 0; i < votesFor.length; i += 1) { |
|
||||||
if (voter.IsEqual(votesFor[i])) { |
|
||||||
return PVS_VoteFor; |
|
||||||
} |
|
||||||
} |
|
||||||
for (i = 0; i < votesAgainst.length; i += 1) { |
|
||||||
if (voter.IsEqual(votesAgainst[i])) { |
|
||||||
return PVS_VoteAgainst; |
|
||||||
} |
|
||||||
} |
|
||||||
return PVS_NoVote; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns amount of current valid votes for the success of this voting. |
|
||||||
public final function int GetVotesFor() { |
|
||||||
return votesFor.length; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns amount of current valid votes against the success of this voting. |
|
||||||
public final function int GetVotesAgainst() { |
|
||||||
return votesAgainst.length; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns amount of users that are currently allowed to vote in this voting. |
|
||||||
public final function int GetTotalPossibleVotes() { |
|
||||||
return allowedVoters.length; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks whether, if stopped now, voting will win. |
|
||||||
public final function bool IsVotingWinning() { |
|
||||||
if (status == VPM_Success) return true; |
|
||||||
if (status == VPM_Failure) return false; |
|
||||||
if (GetVotesFor() > GetVotesAgainst()) return true; |
|
||||||
if (GetVotesFor() < GetVotesAgainst()) return false; |
|
||||||
|
|
||||||
return policyDrawWinsVoting; |
|
||||||
} |
|
||||||
|
|
||||||
/// Forcibly ends the voting, deciding winner depending on the argument. |
|
||||||
/// |
|
||||||
/// 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) { |
|
||||||
case FET_CurrentLeader: |
|
||||||
if (IsVotingWinning()) { |
|
||||||
status = VPM_Success; |
|
||||||
} else { |
|
||||||
status = VPM_Failure; |
|
||||||
} |
|
||||||
break; |
|
||||||
case FET_Success: |
|
||||||
status = VPM_Success; |
|
||||||
break; |
|
||||||
case FET_Failure: |
|
||||||
default: |
|
||||||
status = VPM_Failure; |
|
||||||
break; |
|
||||||
} |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
private final function RecountVotes() { |
|
||||||
local MathApi.IntegerDivisionResult divisionResult; |
|
||||||
local int winningScore, losingScore; |
|
||||||
local int totalPossibleVotes; |
|
||||||
|
|
||||||
if (status != VPM_InProgress) { |
|
||||||
return; |
|
||||||
} |
|
||||||
totalPossibleVotes = GetTotalPossibleVotes(); |
|
||||||
divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2); |
|
||||||
if (divisionResult.remainder == 1) { |
|
||||||
// For odd amount of voters winning is simply majority |
|
||||||
winningScore = divisionResult.quotient + 1; |
|
||||||
} else { |
|
||||||
if (policyDrawWinsVoting) { |
|
||||||
// For even amount of voters, exactly half is enough if draw means victory |
|
||||||
winningScore = divisionResult.quotient; |
|
||||||
} else { |
|
||||||
// Otherwise - majority |
|
||||||
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) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (voter == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
while (i < votesFor.length) { |
|
||||||
if (voter.IsEqual(votesFor[i])) { |
|
||||||
_.memory.Free(votesFor[i]); |
|
||||||
votesFor.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
i = 0; |
|
||||||
while (i < votesAgainst.length) { |
|
||||||
if (voter.IsEqual(votesAgainst[i])) { |
|
||||||
_.memory.Free(votesAgainst[i]); |
|
||||||
votesAgainst.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function RestoreStoredVoters(array<UserID> potentialVoters) { |
|
||||||
local int i, j; |
|
||||||
local bool isPotentialVoter; |
|
||||||
|
|
||||||
while (i < storedVotesFor.length) { |
|
||||||
isPotentialVoter = false; |
|
||||||
for (j = 0; j < potentialVoters.length; j += 1) { |
|
||||||
if (storedVotesFor[i].IsEqual(potentialVoters[j])) { |
|
||||||
isPotentialVoter = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (isPotentialVoter) { |
|
||||||
votesFor[votesFor.length] = storedVotesFor[i]; |
|
||||||
storedVotesFor.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
i = 0; |
|
||||||
while (i < storedVotesAgainst.length) { |
|
||||||
isPotentialVoter = false; |
|
||||||
for (j = 0; j < potentialVoters.length; j += 1) { |
|
||||||
if (storedVotesAgainst[i].IsEqual(potentialVoters[j])) { |
|
||||||
isPotentialVoter = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (isPotentialVoter) { |
|
||||||
votesAgainst[votesAgainst.length] = storedVotesAgainst[i]; |
|
||||||
storedVotesAgainst.Remove(i, 1); |
|
||||||
} else { |
|
||||||
i += 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private final function FilterCurrentVoters(array<UserID> potentialVoters) { |
|
||||||
local int i, j; |
|
||||||
local bool isPotentialVoter; |
|
||||||
|
|
||||||
while (i < votesFor.length) { |
|
||||||
isPotentialVoter = false; |
|
||||||
for (j = 0; j < potentialVoters.length; j += 1) { |
|
||||||
if (votesFor[i].IsEqual(potentialVoters[j])) { |
|
||||||
isPotentialVoter = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (isPotentialVoter) { |
|
||||||
i += 1; |
|
||||||
} else { |
|
||||||
storedVotesFor[storedVotesFor.length] = votesFor[i]; |
|
||||||
votesFor.Remove(i, 1); |
|
||||||
} |
|
||||||
} |
|
||||||
i = 0; |
|
||||||
while (i < votesAgainst.length) { |
|
||||||
isPotentialVoter = false; |
|
||||||
for (j = 0; j < potentialVoters.length; j += 1) { |
|
||||||
if (votesAgainst[i].IsEqual(potentialVoters[j])) { |
|
||||||
isPotentialVoter = true; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
if (isPotentialVoter) { |
|
||||||
i += 1; |
|
||||||
} else { |
|
||||||
storedVotesAgainst[storedVotesAgainst.length] = votesAgainst[i]; |
|
||||||
votesAgainst.Remove(i, 1); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -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" |
|
||||||
} |
|
@ -1,277 +0,0 @@ |
|||||||
/** |
|
||||||
* Author: dkanus |
|
||||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
|
||||||
* License: GPL |
|
||||||
* Copyright 2022-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 SideEffect extends AcediaObject; |
|
||||||
|
|
||||||
//! Defines the concept of "side effects" in the context of the Acedia and its derivative mods. |
|
||||||
//! |
|
||||||
//! Side effects are changes that are not part of the mod's main functionality, but rather something |
|
||||||
//! necessary to enable that functionality, while also possibly affecting how other mods work. |
|
||||||
//! Documenting these side effects helps developers and server admins understand changes performed |
|
||||||
//! by Acedia or mods based on it, and anticipate any potential conflicts or issues that may arise. |
|
||||||
//! |
|
||||||
//! It should be noted that what constitutes a side effect is loosely defined, and it is simply |
|
||||||
//! a tool to inform others that something unexpected has happened, possibly breaking other mods. |
|
||||||
//! AcediaCore aims to leave a minimal footprint, but still needs to make some changes |
|
||||||
//! (e.g., adding GameRules, patching code of some functions), and [`SideEffects`] can be used to |
|
||||||
//! document them. |
|
||||||
//! Similarly, [`SideEffect`]s can be used to document changes made by AcediaFixes, a package meant |
|
||||||
//! only for fixing bugs that inevitably needs to make many under-the-hood changes to achieve |
|
||||||
//! that goal. |
|
||||||
//! |
|
||||||
//! On the other hand gameplay mods like Futility or Ire can make a lot of changes, but they can all |
|
||||||
//! be just expected part of its direct functionality: we expect feature that shares dosh of leavers |
|
||||||
//! to alter players' dosh values, so this is not a side effect. |
|
||||||
//! Such mods are likely not going to have to specify any side effects whatsoever. |
|
||||||
|
|
||||||
var private Text name; |
|
||||||
var private Text description; |
|
||||||
var private Text package; |
|
||||||
var private Text source; |
|
||||||
var private Text status; |
|
||||||
var private bool initialized; |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_.memory.Free(name); |
|
||||||
_.memory.Free(description); |
|
||||||
_.memory.Free(package); |
|
||||||
_.memory.Free(source); |
|
||||||
_.memory.Free(status); |
|
||||||
name = none; |
|
||||||
description = none; |
|
||||||
package = none; |
|
||||||
source = none; |
|
||||||
status = none; |
|
||||||
initialized = false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks whether caller [`SideEffect`] was initialized. |
|
||||||
/// |
|
||||||
/// Initialization must happen directly after creation and only initialized instances should |
|
||||||
/// ever be used. |
|
||||||
public final function bool IsInitialized() { |
|
||||||
return initialized; |
|
||||||
} |
|
||||||
|
|
||||||
/// This function is used to set the initial values of the [`SideEffect`] object properties when it |
|
||||||
/// is first created. |
|
||||||
/// |
|
||||||
/// All arguments must be not `none`. |
|
||||||
/// |
|
||||||
/// Returns `true` if the initialization was successful, `false` otherwise (including the case where |
|
||||||
/// the [`SideEffect`] object has already been initialized). |
|
||||||
public final function bool Initialize( |
|
||||||
BaseText sideEffectName, |
|
||||||
BaseText sideEffectDescription, |
|
||||||
BaseText sideEffectPackage, |
|
||||||
BaseText sideEffectSource, |
|
||||||
BaseText sideEffectStatus |
|
||||||
) { |
|
||||||
if (initialized) return false; |
|
||||||
if (sideEffectName == none) return false; |
|
||||||
if (sideEffectDescription == none) return false; |
|
||||||
if (sideEffectPackage == none) return false; |
|
||||||
if (sideEffectSource == none) return false; |
|
||||||
if (sideEffectStatus == none) return false; |
|
||||||
|
|
||||||
name = sideEffectName.Copy(); |
|
||||||
description = sideEffectDescription.Copy(); |
|
||||||
package = sideEffectPackage.Copy(); |
|
||||||
source = sideEffectSource.Copy(); |
|
||||||
status = sideEffectStatus.Copy(); |
|
||||||
initialized = true; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/// This function is used to set the initial values of the [`SideEffect`] object properties when it |
|
||||||
/// is first created. |
|
||||||
/// |
|
||||||
/// Returns `true` if the initialization was successful, `false` otherwise (including the case where |
|
||||||
/// the [`SideEffect`] object has already been initialized). |
|
||||||
public final function bool Initialize_S( |
|
||||||
string sideEffectName, |
|
||||||
string sideEffectDescription, |
|
||||||
string sideEffectPackage, |
|
||||||
string sideEffectSource, |
|
||||||
string sideEffectStatus |
|
||||||
) { |
|
||||||
name = _.text.FromString(sideEffectName); |
|
||||||
description = _.text.FromString(sideEffectDescription); |
|
||||||
package = _.text.FromString(sideEffectPackage); |
|
||||||
source = _.text.FromString(sideEffectSource); |
|
||||||
status = _.text.FromString(sideEffectStatus); |
|
||||||
initialized = true; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in |
|
||||||
/// a clear and concise manner. |
|
||||||
/// |
|
||||||
/// While there is no hard limit on the length of this value, it is recommended to keep it under 80 |
|
||||||
/// characters for readability. |
|
||||||
/// |
|
||||||
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is |
|
||||||
/// a required property of the [`SideEffect`] object. |
|
||||||
public final function Text GetName() { |
|
||||||
if (initialized) { |
|
||||||
return name.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns a brief summary that conveys the purpose of the caller [SideEffect] to the user in |
|
||||||
/// a clear and concise manner. |
|
||||||
/// |
|
||||||
/// While there is no hard limit on the length of this value, it is recommended to keep it under 80 |
|
||||||
/// characters for readability. |
|
||||||
public final function string GetName_S() { |
|
||||||
if (initialized && name != none) { |
|
||||||
return name.ToString(); |
|
||||||
} |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the detailed description of the caller [`SideEffect`], which describes what was done |
|
||||||
/// and why the relevant change was necessary. |
|
||||||
/// |
|
||||||
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is |
|
||||||
/// a required property of the [`SideEffect`] object. |
|
||||||
public final function Text GetDescription() { |
|
||||||
if (initialized) { |
|
||||||
return description.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the detailed description of the caller [`SideEffect`], which describes what was done |
|
||||||
/// and why the relevant change was necessary. |
|
||||||
public final function string GetDescription_S() { |
|
||||||
if (initialized && description != none) { |
|
||||||
return description.ToString(); |
|
||||||
} |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the name of the package ("*.u" file) that introduced the changes |
|
||||||
/// represented by the caller `SideEffect`. |
|
||||||
/// |
|
||||||
/// It should be noted that even if a different package requested the functionality that led to |
|
||||||
/// the changes being made, the package responsible for the side effect is the one that performed |
|
||||||
/// the changes. |
|
||||||
/// |
|
||||||
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is |
|
||||||
/// a required property of the [`SideEffect`] object. |
|
||||||
public final function Text GetPackage() { |
|
||||||
if (initialized) { |
|
||||||
return package.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns the name of the package ("*.u" file) that introduced the changes |
|
||||||
/// represented by the caller `SideEffect`. |
|
||||||
/// |
|
||||||
/// It should be noted that even if a different package requested the functionality that led to |
|
||||||
/// the changes being made, the package responsible for the side effect is the one that performed |
|
||||||
/// the changes. |
|
||||||
public final function string GetPackage_S() { |
|
||||||
if (initialized && package != none) { |
|
||||||
return package.ToString(); |
|
||||||
} |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
/// The origin of this change within the package is specified, and for larger packages, additional |
|
||||||
/// details can be provided to clarify the cause of the change. |
|
||||||
/// |
|
||||||
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is |
|
||||||
/// a required property of the [`SideEffect`] object. |
|
||||||
public final function Text GetSource() { |
|
||||||
if (initialized) { |
|
||||||
return source.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// The origin of this change within the package is specified, and for larger packages, additional |
|
||||||
/// details can be provided to clarify the cause of the change. |
|
||||||
public final function string GetSource_S() { |
|
||||||
if (initialized && source != none) { |
|
||||||
return source.ToString(); |
|
||||||
} |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
/// The status of the caller [`SideEffect`], that is used to differentiate between different ways |
|
||||||
/// that a side effect may have been introduced, allowing for better tracking and management of |
|
||||||
/// the effect. |
|
||||||
/// |
|
||||||
/// Returned value for initialized [`SideEffect`] is guaranteed to not be `none`, as it is |
|
||||||
/// a required property of the [`SideEffect`] object. |
|
||||||
public final function Text GetStatus() { |
|
||||||
if (initialized) { |
|
||||||
return status.Copy(); |
|
||||||
} |
|
||||||
return none; |
|
||||||
} |
|
||||||
|
|
||||||
/// The status of the caller [`SideEffect`], that is used to differentiate between different ways |
|
||||||
/// that a side effect may have been introduced, allowing for better tracking and management of |
|
||||||
/// the effect. |
|
||||||
public final function string GetStatus_S() { |
|
||||||
if (initialized && status != none) { |
|
||||||
return status.ToString(); |
|
||||||
} |
|
||||||
return ""; |
|
||||||
} |
|
||||||
|
|
||||||
public function bool IsEqual(Object other) { |
|
||||||
local SideEffect otherSideEffect; |
|
||||||
|
|
||||||
if (self == other) return true; |
|
||||||
otherSideEffect = SideEffect(other); |
|
||||||
if (otherSideEffect == none) return false; |
|
||||||
if (!otherSideEffect.initialized) return false; |
|
||||||
if (GetHashCode() != otherSideEffect.GetHashCode()) return false; |
|
||||||
if (!name.Compare(otherSideEffect.name,, SFORM_SENSITIVE)) return false; |
|
||||||
if (!package.Compare(otherSideEffect.package,, SFORM_SENSITIVE)) return false; |
|
||||||
if (!source.Compare(otherSideEffect.source,, SFORM_SENSITIVE)) return false; |
|
||||||
if (!status.Compare(otherSideEffect.status,, SFORM_SENSITIVE)) return false; |
|
||||||
if (!description.Compare(otherSideEffect.description,, SFORM_SENSITIVE)) return false; |
|
||||||
|
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
protected function int CalculateHashCode() { |
|
||||||
local int result; |
|
||||||
|
|
||||||
if (initialized) { |
|
||||||
result = name.GetHashCode(); |
|
||||||
result = CombineHash(result, description.GetHashCode()); |
|
||||||
result = CombineHash(result, package.GetHashCode()); |
|
||||||
result = CombineHash(result, source.GetHashCode()); |
|
||||||
result = CombineHash(result, status.GetHashCode()); |
|
||||||
return result; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,212 +0,0 @@ |
|||||||
/** |
|
||||||
* Author: dkanus |
|
||||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
|
||||||
* License: GPL |
|
||||||
* Copyright 2022-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 SideEffectAPI extends AcediaObject; |
|
||||||
|
|
||||||
var private array<SideEffect> activeSideEffects; |
|
||||||
|
|
||||||
/// Returns an array containing all SideEffect objects that have been registered up to this point. |
|
||||||
/// |
|
||||||
/// The order of the elements in the array is not guaranteed. |
|
||||||
public function array<SideEffect> GetAll() { |
|
||||||
local int i; |
|
||||||
|
|
||||||
for (i = 0; i < activeSideEffects.length; i += 1) { |
|
||||||
activeSideEffects[i].NewRef(); |
|
||||||
} |
|
||||||
return activeSideEffects; |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns all registered [`SideEffects`] that are associated with the specified package name |
|
||||||
/// (case-insensitive). |
|
||||||
public function array<SideEffect> GetFromPackage(BaseText packageName) { |
|
||||||
local int i; |
|
||||||
local Text nextPackage; |
|
||||||
local array<SideEffect> result; |
|
||||||
|
|
||||||
if (packageName == none) { |
|
||||||
return result; |
|
||||||
} |
|
||||||
for (i = 0; i < activeSideEffects.length; i += 1) { |
|
||||||
nextPackage = activeSideEffects[i].GetPackage(); |
|
||||||
if (packageName.Compare(nextPackage, SCASE_INSENSITIVE)) { |
|
||||||
activeSideEffects[i].NewRef(); |
|
||||||
result[result.length] = activeSideEffects[i]; |
|
||||||
} |
|
||||||
_.memory.Free(nextPackage); |
|
||||||
} |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds a new side effect to the list of active side effects. |
|
||||||
/// |
|
||||||
/// This method will fail if any of its arguments are `none` or a side effect with that exact |
|
||||||
/// contents was already added. |
|
||||||
public function SideEffect Add( |
|
||||||
BaseText sideEffectName, |
|
||||||
BaseText sideEffectDescription, |
|
||||||
BaseText sideEffectPackage, |
|
||||||
BaseText sideEffectSource, |
|
||||||
BaseText sideEffectStatus |
|
||||||
) { |
|
||||||
local bool initialized; |
|
||||||
local SideEffect newSideEffect; |
|
||||||
|
|
||||||
newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect')); |
|
||||||
initialized = newSideEffect.Initialize( |
|
||||||
sideEffectName, |
|
||||||
sideEffectDescription, |
|
||||||
sideEffectPackage, |
|
||||||
sideEffectSource, |
|
||||||
sideEffectStatus); |
|
||||||
if (initialized) { |
|
||||||
if (!AddInstance(newSideEffect)) { |
|
||||||
_.memory.Free(newSideEffect); |
|
||||||
return none; |
|
||||||
} |
|
||||||
} else { |
|
||||||
_.memory.Free(newSideEffect); |
|
||||||
return none; |
|
||||||
} |
|
||||||
return newSideEffect; |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds a new side effect to the list of active side effects. |
|
||||||
/// |
|
||||||
/// This method will fail if a side effect with that exact contents was already added. |
|
||||||
public function SideEffect Add_S( |
|
||||||
string sideEffectName, |
|
||||||
string sideEffectDescription, |
|
||||||
string sideEffectPackage, |
|
||||||
string sideEffectSource, |
|
||||||
string sideEffectStatus |
|
||||||
) { |
|
||||||
local bool initialized; |
|
||||||
local SideEffect newSideEffect; |
|
||||||
|
|
||||||
newSideEffect = SideEffect(_.memory.Allocate(class'SideEffect')); |
|
||||||
initialized = newSideEffect.Initialize_S( |
|
||||||
sideEffectName, |
|
||||||
sideEffectDescription, |
|
||||||
sideEffectPackage, |
|
||||||
sideEffectSource, |
|
||||||
sideEffectStatus); |
|
||||||
if (initialized) { |
|
||||||
if (!AddInstance(newSideEffect)) { |
|
||||||
_.memory.Free(newSideEffect); |
|
||||||
return none; |
|
||||||
} |
|
||||||
} else { |
|
||||||
return none; |
|
||||||
} |
|
||||||
return newSideEffect; |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks whether specified [`SideEffect`] is currently active. |
|
||||||
/// |
|
||||||
/// Check is done via contents and not instance equality. |
|
||||||
/// Returns `true` if specified [`SideEffect`] is currently active and `false` otherwise. |
|
||||||
public function bool IsRegistered(SideEffect sideEffectToCheck) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (sideEffectToCheck == none) return false; |
|
||||||
if (!sideEffectToCheck.IsInitialized()) return false; |
|
||||||
|
|
||||||
for (i = 0; i < activeSideEffects.length; i += 1) { |
|
||||||
if (activeSideEffects[i].IsEqual(sideEffectToCheck)) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
/// Adds a new side effect to the list of active side effects. |
|
||||||
/// |
|
||||||
/// This method will fail if its argument is `none`, non-initialized or a side effect with that |
|
||||||
/// exact contents was already added. |
|
||||||
public function bool AddInstance(SideEffect newSideEffect) { |
|
||||||
local int i; |
|
||||||
|
|
||||||
if (newSideEffect == none) return false; |
|
||||||
if (!newSideEffect.IsInitialized()) return false; |
|
||||||
|
|
||||||
for (i = 0; i < activeSideEffects.length; i += 1) { |
|
||||||
if (activeSideEffects[i].IsEqual(newSideEffect)) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
newSideEffect.NewRef(); |
|
||||||
activeSideEffects[activeSideEffects.length] = newSideEffect; |
|
||||||
LogAddingSideEffectChange(newSideEffect, true); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
/// Removes a side effect from the list of active side effects. |
|
||||||
/// |
|
||||||
/// This method will fail if its argument is `none`, non-initialized or a side effect with its |
|
||||||
/// contents isn't in the records. |
|
||||||
public function bool RemoveInstance(SideEffect inactiveSideEffect) { |
|
||||||
local int i; |
|
||||||
local bool foundInstance; |
|
||||||
|
|
||||||
if (inactiveSideEffect == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (i = 0; i < activeSideEffects.length; i += 1) { |
|
||||||
if (activeSideEffects[i].IsEqual(inactiveSideEffect)) { |
|
||||||
LogAddingSideEffectChange(activeSideEffects[i], false); |
|
||||||
_.memory.Free(activeSideEffects[i]); |
|
||||||
activeSideEffects.Remove(i, 1); |
|
||||||
foundInstance = true; |
|
||||||
} |
|
||||||
} |
|
||||||
return foundInstance; |
|
||||||
} |
|
||||||
|
|
||||||
private function LogAddingSideEffectChange(SideEffect effect, bool added) { |
|
||||||
local MutableText builder; |
|
||||||
local Text sideEffectData; |
|
||||||
|
|
||||||
if (effect == none) { |
|
||||||
return; |
|
||||||
} |
|
||||||
builder = _.text.Empty(); |
|
||||||
if (added) { |
|
||||||
builder.Append(P("NEW SIDE EFFECT: ")); |
|
||||||
} else { |
|
||||||
builder.Append(P("REMOVED SIDE EFFECT: ")); |
|
||||||
} |
|
||||||
sideEffectData = effect.GetName(); |
|
||||||
builder.Append(sideEffectData); |
|
||||||
_.memory.Free(sideEffectData); |
|
||||||
sideEffectData = effect.GetStatus(); |
|
||||||
if (sideEffectData != none) { |
|
||||||
builder.Append(P(" {")); |
|
||||||
builder.Append(sideEffectData); |
|
||||||
_.memory.Free(sideEffectData); |
|
||||||
builder.Append(P("}")); |
|
||||||
} |
|
||||||
_.logger.Info(builder); |
|
||||||
builder.FreeSelf(); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,76 +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 FunctionReplacement extends AcediaObject; |
|
||||||
|
|
||||||
var private Text replaced; |
|
||||||
var private Text replacer; |
|
||||||
var private SideEffect effect; |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_.memory.Free(replaced); |
|
||||||
_.memory.Free(replacer); |
|
||||||
_.memory.Free(effect); |
|
||||||
replaced = none; |
|
||||||
replacer = none; |
|
||||||
effect = none; |
|
||||||
} |
|
||||||
|
|
||||||
public static final function FunctionReplacement Make( |
|
||||||
BaseText oldFunction, |
|
||||||
BaseText newFunction, |
|
||||||
SideEffect sideEffect |
|
||||||
) { |
|
||||||
local FunctionReplacement newReplacement; |
|
||||||
|
|
||||||
if (oldFunction == none) return none; |
|
||||||
if (newFunction == none) return none; |
|
||||||
if (sideEffect == none) return none; |
|
||||||
|
|
||||||
newReplacement = FunctionReplacement(__().memory.Allocate(class'FunctionReplacement')); |
|
||||||
newReplacement.replaced = oldFunction.Copy(); |
|
||||||
newReplacement.replacer = newFunction.Copy(); |
|
||||||
sideEffect.NewRef(); |
|
||||||
newReplacement.effect = sideEffect; |
|
||||||
return newReplacement; |
|
||||||
} |
|
||||||
|
|
||||||
public final function Text GetReplacedFunctionName() { |
|
||||||
return replaced.Copy(); |
|
||||||
} |
|
||||||
|
|
||||||
public final function string GetReplacedFunctionName_S() { |
|
||||||
return replaced.ToString(); |
|
||||||
} |
|
||||||
|
|
||||||
public final function Text GetReplacerFunctionName() { |
|
||||||
return replacer.Copy(); |
|
||||||
} |
|
||||||
|
|
||||||
public final function string GetReplacerFunctionName_S() { |
|
||||||
return replacer.ToString(); |
|
||||||
} |
|
||||||
|
|
||||||
public final function SideEffect GetSideEffect() { |
|
||||||
effect.NewRef(); |
|
||||||
return effect; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +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 MockInitialClass extends AcediaObject; |
|
||||||
|
|
||||||
var public int counter; |
|
||||||
|
|
||||||
public final function int DoIt() { |
|
||||||
counter += 1; |
|
||||||
return counter; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,33 +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 MockReplacerClass extends MockInitialClass; |
|
||||||
|
|
||||||
public final function int DoIt2() { |
|
||||||
counter += 2; |
|
||||||
return -counter; |
|
||||||
} |
|
||||||
|
|
||||||
public final function int DoIt3() { |
|
||||||
counter -= 1; |
|
||||||
return 7; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,87 +0,0 @@ |
|||||||
/** |
|
||||||
* Set of tests for `Command` class. |
|
||||||
* 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_Unflect extends TestCase |
|
||||||
abstract; |
|
||||||
|
|
||||||
protected static function TESTS() { |
|
||||||
local MockInitialClass obj; |
|
||||||
|
|
||||||
obj = MockInitialClass(__().memory.Allocate(class'MockInitialClass')); |
|
||||||
Context("Replacing functions with `UnflectApi`"); |
|
||||||
Test_InitialReplacement(obj); |
|
||||||
Test_SecondReplacement(obj); |
|
||||||
Test_ReplacementWithSelf(obj); |
|
||||||
Test_RevertingReplacement(obj); |
|
||||||
} |
|
||||||
|
|
||||||
protected static function Test_InitialReplacement(MockInitialClass obj) { |
|
||||||
Issue("Functions aren't being replaced correctly the first time."); |
|
||||||
TEST_ExpectTrue(__().unflect.ReplaceFunction_S( |
|
||||||
"AcediaCore.MockInitialClass.DoIt", |
|
||||||
"AcediaCore.MockReplacerClass.DoIt2", |
|
||||||
"testing")); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == -2); |
|
||||||
TEST_ExpectTrue(obj.counter == 2); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == -4); |
|
||||||
TEST_ExpectTrue(obj.counter == 4); |
|
||||||
} |
|
||||||
|
|
||||||
protected static function Test_SecondReplacement(MockInitialClass obj) { |
|
||||||
Issue("Functions aren't being replaced correctly in case they were already replaced."); |
|
||||||
TEST_ExpectTrue(__().unflect.ReplaceFunction_S( |
|
||||||
"AcediaCore.MockInitialClass.DoIt", |
|
||||||
"AcediaCore.MockReplacerClass.DoIt3", |
|
||||||
"testing")); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 7); |
|
||||||
TEST_ExpectTrue(obj.counter == 3); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 7); |
|
||||||
TEST_ExpectTrue(obj.counter == 2); |
|
||||||
} |
|
||||||
|
|
||||||
protected static function Test_ReplacementWithSelf(MockInitialClass obj) { |
|
||||||
Issue("Attempting to replacing function with itself makes unexpected change."); |
|
||||||
TEST_ExpectFalse(__().unflect.ReplaceFunction_S( |
|
||||||
"AcediaCore.MockInitialClass.DoIt", |
|
||||||
"AcediaCore.MockInitialClass.DoIt", |
|
||||||
"testing")); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 7); |
|
||||||
TEST_ExpectTrue(obj.counter == 1); |
|
||||||
} |
|
||||||
|
|
||||||
protected static function Test_RevertingReplacement(MockInitialClass obj) { |
|
||||||
Issue("Reverting replaced function doesn't work."); |
|
||||||
TEST_ExpectTrue(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt")); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 2); |
|
||||||
TEST_ExpectTrue(obj.counter == 2); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 3); |
|
||||||
TEST_ExpectTrue(obj.counter == 3); |
|
||||||
|
|
||||||
Issue("Reverting already reverted function ends in success."); |
|
||||||
TEST_ExpectFalse(__().unflect.RevertFunction_S("AcediaCore.MockInitialClass.DoIt")); |
|
||||||
|
|
||||||
Issue("Reverting already reverted function leads to unexpected results."); |
|
||||||
TEST_ExpectTrue(obj.DoIt() == 4); |
|
||||||
TEST_ExpectTrue(obj.counter == 4); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
caseName = "Function replacement" |
|
||||||
caseGroup = "Unflect" |
|
||||||
} |
|
@ -1,29 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 TypeCast extends Object; |
|
||||||
|
|
||||||
var Object nativeType; |
|
||||||
|
|
||||||
final function NativeCast(Object type) { |
|
||||||
nativeType = type; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,49 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UClass extends UState within Package; |
|
||||||
|
|
||||||
var int classFlags; |
|
||||||
var int classUnique; |
|
||||||
var Guid classGuid; |
|
||||||
var UClass classWithin; |
|
||||||
var name classConfigName; |
|
||||||
|
|
||||||
var array<struct RepRecord { |
|
||||||
var UProperty property; |
|
||||||
var int index; |
|
||||||
}> classReps; |
|
||||||
|
|
||||||
var array<UField> netFields; |
|
||||||
|
|
||||||
var array<struct Dependency { |
|
||||||
var UClass class; |
|
||||||
var int deep; |
|
||||||
var int scriptTextCRC; |
|
||||||
}> dependencies; |
|
||||||
|
|
||||||
var array<name> packageImports; |
|
||||||
var array<byte> defaults; |
|
||||||
var array<name> hideCategories; |
|
||||||
var array<name> dependentOn; |
|
||||||
|
|
||||||
var string defaultPropText; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UClassCast extends Object; |
|
||||||
|
|
||||||
var UClass nativeType; |
|
||||||
|
|
||||||
final function UClass Cast(Class type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UField extends Object |
|
||||||
abstract; |
|
||||||
|
|
||||||
var UField superField; |
|
||||||
var UField next; |
|
||||||
var UField hashNext; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UFieldCast extends Object; |
|
||||||
|
|
||||||
var UField nativeType; |
|
||||||
|
|
||||||
final function UField Cast(Field type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,35 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UFunction extends UStruct within UState |
|
||||||
dependson(Unflect); |
|
||||||
|
|
||||||
var byte functionMD5Digest[16]; |
|
||||||
|
|
||||||
var int functionFlags; |
|
||||||
var Unflect.Int16 nativeIndex; |
|
||||||
var Unflect.Int16 repOffset; |
|
||||||
var byte operPrecedence; |
|
||||||
|
|
||||||
var byte numParms; |
|
||||||
var Unflect.Int16 parmsSize; |
|
||||||
var Unflect.Int16 returnValueOffset; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UFunctionCast extends Object; |
|
||||||
|
|
||||||
var UFunction nativeType; |
|
||||||
|
|
||||||
final function UFunction Cast(Function type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,41 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UProperty extends UField within UField |
|
||||||
abstract; |
|
||||||
|
|
||||||
var int arrayDim; |
|
||||||
var int elementSize; |
|
||||||
var int propertyFlags; |
|
||||||
var name category; |
|
||||||
|
|
||||||
var byte repOffset[2]; |
|
||||||
var byte repIndex[2]; |
|
||||||
|
|
||||||
var transient int offset; |
|
||||||
var transient UProperty propertyLinkNext; |
|
||||||
var transient UProperty configLinkNext; |
|
||||||
var transient UProperty constructorLinkNext; |
|
||||||
var transient UProperty nextRef; |
|
||||||
var transient UProperty repOwner; |
|
||||||
|
|
||||||
var string commentString; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UPropertyCast extends Object; |
|
||||||
|
|
||||||
var UProperty nativeType; |
|
||||||
|
|
||||||
final function UProperty Cast(Property type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UState extends UStruct |
|
||||||
dependson(Unflect); |
|
||||||
|
|
||||||
var Unflect.Int64 probeMask; |
|
||||||
var Unflect.Int64 ignoreMask; |
|
||||||
var int stateFlags; |
|
||||||
var Unflect.Int16 labelTableOffset; |
|
||||||
var UField vfHash[256]; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UStateCast extends Object; |
|
||||||
|
|
||||||
var UState nativeType; |
|
||||||
|
|
||||||
final function UState Cast(State type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,48 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UStruct extends UField; |
|
||||||
|
|
||||||
var UTextBuffer scriptText; |
|
||||||
var UTextBuffer cppText; |
|
||||||
var UField children; |
|
||||||
var int propertiesSize; |
|
||||||
var name friendlyName; |
|
||||||
var array<byte> script; |
|
||||||
|
|
||||||
var int textPos; |
|
||||||
var int line; |
|
||||||
var struct EStructFlags { |
|
||||||
var bool native; |
|
||||||
var bool export; |
|
||||||
var bool long; |
|
||||||
var bool init; |
|
||||||
var bool unused1; |
|
||||||
var bool unused2; |
|
||||||
var bool unused3; |
|
||||||
var bool unused4; |
|
||||||
} StructFlags; |
|
||||||
|
|
||||||
var Property refLink; |
|
||||||
var Property propertyLink; |
|
||||||
var Property configLink; |
|
||||||
var Property constructorLink; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UStructCast extends Object; |
|
||||||
|
|
||||||
var UStruct nativeType; |
|
||||||
|
|
||||||
final function UStruct Cast(/*Core.Struct*/ Object type) { |
|
||||||
super(TypeCast).NativeCast(type); |
|
||||||
return nativeType; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 UTextBuffer extends Object; |
|
||||||
|
|
||||||
var private native const pointer outputDeviceVtbl; |
|
||||||
|
|
||||||
var int pos, top; |
|
||||||
var string text; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,32 +0,0 @@ |
|||||||
/** |
|
||||||
* One of the original Unflect files. |
|
||||||
* Copyright 2022-2023 EliotVU |
|
||||||
*------------------------------------------------------------------------------ |
|
||||||
* 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 Unflect extends Object |
|
||||||
abstract; |
|
||||||
|
|
||||||
struct Int16 { |
|
||||||
var byte h, l; |
|
||||||
}; |
|
||||||
|
|
||||||
struct Int64 { |
|
||||||
var int h, l; |
|
||||||
}; |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,432 +0,0 @@ |
|||||||
/** |
|
||||||
* Config class for storing map lists. |
|
||||||
* Copyright 2020 bibibi |
|
||||||
* 2020-2023 Shtoyan |
|
||||||
* 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 UnflectApi extends AcediaObject; |
|
||||||
|
|
||||||
//! This API offers advanced reflection capabilities for Unreal Script. |
|
||||||
//! |
|
||||||
//! Currently, the API supports the ability to replace the code of existing functions with |
|
||||||
//! custom code. |
|
||||||
//! This can greatly simplify the process of bug fixing and hooking into game events. |
|
||||||
|
|
||||||
/// A variable responsible for replacing function code in real-time. |
|
||||||
/// This variable is used to support dynamic function replacement/patching and event interception |
|
||||||
/// at runtime. |
|
||||||
var private UFunctionCast functionCaster; |
|
||||||
|
|
||||||
/// Maps lower case function name (specifies by the full path "package.class.functionName") |
|
||||||
/// to a `FunctionRule` that completely describes how it was replaced |
|
||||||
var private HashTable completedReplacements; |
|
||||||
/// Maps lower case function name (specifies by the full path "package.class.functionName") |
|
||||||
/// to the `ByteArrayBox` with that function's original code. |
|
||||||
var private HashTable originalScriptCodes; |
|
||||||
|
|
||||||
var private LoggerAPI.Definition warnSameFunction; |
|
||||||
var private LoggerAPI.Definition warnOverridingReplacement, errFailedToFindFunction; |
|
||||||
var private LoggerAPI.Definition errReplacementWithoutSources, errCannotCreateReplacementRule; |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
functionCaster = new class'UFunctionCast'; |
|
||||||
completedReplacements = _.collections.EmptyHashTable(); |
|
||||||
originalScriptCodes = _.collections.EmptyHashTable(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
_drop(); |
|
||||||
_.memory.Free(completedReplacements); |
|
||||||
_.memory.Free(originalScriptCodes); |
|
||||||
completedReplacements = none; |
|
||||||
originalScriptCodes = none; |
|
||||||
functionCaster = none; |
|
||||||
} |
|
||||||
|
|
||||||
public final function _drop() { |
|
||||||
local UFunction nextFunctionInstance; |
|
||||||
local Text nextFunctionName; |
|
||||||
local HashTableIterator iter; |
|
||||||
local ByteArrayBox nextSources; |
|
||||||
|
|
||||||
// Drop is called when Acedia is shutting down, so releasing references isn't necessary |
|
||||||
iter = HashTableIterator(completedReplacements.Iterate()); |
|
||||||
while (!iter.HasFinished()) { |
|
||||||
nextFunctionInstance = none; |
|
||||||
nextFunctionName = Text(iter.GetKey()); |
|
||||||
nextSources = ByteArrayBox(originalScriptCodes.GetItem(nextFunctionName)); |
|
||||||
if (nextSources != none ) { |
|
||||||
nextFunctionInstance = FindFunction(nextFunctionName); |
|
||||||
} |
|
||||||
if (nextFunctionInstance != none) { |
|
||||||
nextFunctionInstance.script = nextSources.Get(); |
|
||||||
} |
|
||||||
iter.Next(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Reverts the replacement of the function's code, restoring its original behavior. |
|
||||||
/// |
|
||||||
/// The function to be reverted should be specified using its full path, in the format |
|
||||||
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). |
|
||||||
/// |
|
||||||
/// It's worth noting that several function replacements do not stack. |
|
||||||
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function, |
|
||||||
/// this method will cancel all the changes at once. |
|
||||||
/// |
|
||||||
/// This method returns true if the specified function was previously replaced and has now been |
|
||||||
/// successfully reverted. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if |
|
||||||
/// UnflectApi has not yet replaced it with any other function, this method will log an error. |
|
||||||
public final function bool RevertFunction(BaseText functionName) { |
|
||||||
local bool result; |
|
||||||
local FunctionReplacement storedReplacement; |
|
||||||
local ByteArrayBox storedSources; |
|
||||||
local Text functionNameLowerCase; |
|
||||||
local UFunction functionInstance; |
|
||||||
local SideEffect sideEffect; |
|
||||||
|
|
||||||
if (functionName == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
functionNameLowerCase = functionName.LowerCopy(); |
|
||||||
storedReplacement = FunctionReplacement(completedReplacements.GetItem(functionNameLowerCase)); |
|
||||||
if (storedReplacement != none) { |
|
||||||
storedSources = ByteArrayBox(originalScriptCodes.GetItem(functionNameLowerCase)); |
|
||||||
if (storedSources == none) { |
|
||||||
_.logger.Auto(errReplacementWithoutSources).Arg(functionNameLowerCase.Copy()); |
|
||||||
} else { |
|
||||||
functionInstance = FindFunction(functionNameLowerCase); |
|
||||||
if (functionInstance != none) { |
|
||||||
functionInstance.script = storedSources.Get(); |
|
||||||
completedReplacements.RemoveItem(functionNameLowerCase); |
|
||||||
sideEffect = storedReplacement.GetSideEffect(); |
|
||||||
_.sideEffects.RemoveInstance(sideEffect); |
|
||||||
result = true; |
|
||||||
} else { |
|
||||||
_.logger.Auto(errFailedToFindFunction).Arg(functionNameLowerCase.Copy()); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
_.memory.Free4(storedReplacement, functionNameLowerCase, storedSources, sideEffect); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Reverts the replacement of the function's code, restoring its original behavior. |
|
||||||
/// |
|
||||||
/// The function to be reverted should be specified using its full path, in the format |
|
||||||
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). |
|
||||||
/// |
|
||||||
/// It's worth noting that several function replacements do not stack. |
|
||||||
/// Even if [`ReplaceFunction()`] was called multiple times in a row to replace the same function, |
|
||||||
/// this method will cancel all the changes at once. |
|
||||||
/// |
|
||||||
/// This method returns true if the specified function was previously replaced and has now been |
|
||||||
/// successfully reverted. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// If the specified function cannot be found (but [`functionName`] isn't `none`), or if |
|
||||||
/// UnflectApi has not yet replaced it with any other function, this method will log an error. |
|
||||||
public final function bool RevertFunction_S(string functionName) { |
|
||||||
local bool result; |
|
||||||
local MutableText wrapper; |
|
||||||
|
|
||||||
wrapper = _.text.FromStringM(functionName); |
|
||||||
result = RevertFunction(wrapper); |
|
||||||
_.memory.Free(wrapper); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Determines whether the specified function has been replaced by UnflectApi. |
|
||||||
/// |
|
||||||
/// The function to be checked should be specified using its full path, in the format |
|
||||||
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). |
|
||||||
/// |
|
||||||
/// If the function has been replaced, this method will return `true`; |
|
||||||
/// otherwise, it will return `false`. |
|
||||||
public final function bool IsFunctionReplaced(BaseText functionName) { |
|
||||||
local bool result; |
|
||||||
local Text functionNameLowerCase; |
|
||||||
|
|
||||||
if (functionName == none) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
functionNameLowerCase = functionName.LowerCopy(); |
|
||||||
result = completedReplacements.HasKey(functionNameLowerCase); |
|
||||||
_.memory.Free(functionNameLowerCase); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Determines whether the specified function has been replaced by UnflectApi. |
|
||||||
/// |
|
||||||
/// The function to be checked should be specified using its full path, in the format |
|
||||||
/// "package.class.functionName" (e.g. "KFMod.KFPawn.TossCash"). |
|
||||||
/// |
|
||||||
/// If the function has been replaced, this method will return `true`; |
|
||||||
/// otherwise, it will return `false`. |
|
||||||
public final function bool IsFunctionReplaced_S(string functionName) { |
|
||||||
local bool result; |
|
||||||
local MutableText wrapper; |
|
||||||
|
|
||||||
wrapper = _.text.FromStringM(functionName); |
|
||||||
result = IsFunctionReplaced(wrapper); |
|
||||||
_.memory.Free(wrapper); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Replaces one function with another by modifying its script code in real-time. |
|
||||||
/// |
|
||||||
/// The reason for replacement must be specified and will serve as a human-readable explanation |
|
||||||
/// for a [SideEffect] associated with the replacement. |
|
||||||
/// |
|
||||||
/// If you need to replace a function in a class, follow these steps: |
|
||||||
/// |
|
||||||
/// 1. Create a new class that extends the class in which the function you want to replace is |
|
||||||
/// located. |
|
||||||
/// 2. Declare that function in the created class. |
|
||||||
/// 3. **DO NOT** change the function declaration and argument types/amount. |
|
||||||
/// 4. **DO NOT** create new local variables, as this can cause random crashes. |
|
||||||
/// If you need additional variables, make them global and access them using the |
|
||||||
/// `class'myNewClass'.default.myNewVariable` syntax. |
|
||||||
/// 5. If you want to call or override parent code, make sure to always specify the desired parent |
|
||||||
/// class name. |
|
||||||
/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`. |
|
||||||
/// This will prevent runaway loop crashes. |
|
||||||
/// 6. Make your edits to the function's code, and then call the replacement function: |
|
||||||
/// ```unrealscript |
|
||||||
/// _.unflect.ReplaceFunction( |
|
||||||
/// "package.class.targetFunction", |
|
||||||
/// "myNewPackage.myNewClass.newFunction"); |
|
||||||
/// ``` |
|
||||||
/// |
|
||||||
/// Following these steps will help ensure that your code changes are compatible with the rest of |
|
||||||
/// the codebase and do not cause unexpected crashes. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// This method can log error messages in cases where: |
|
||||||
/// |
|
||||||
/// * The specified function(s) cannot be found. |
|
||||||
/// * An attempt is made to replace a function with itself. |
|
||||||
/// * An attempt is made to replace a function that has already been replaced. |
|
||||||
public final function bool ReplaceFunction( |
|
||||||
BaseText oldFunction, |
|
||||||
BaseText newFunction, |
|
||||||
BaseText replacementReason |
|
||||||
) { |
|
||||||
local bool result; |
|
||||||
local Text oldFunctionLowerCase, newFunctionLowerCase; |
|
||||||
|
|
||||||
if (oldFunction == none) return false; |
|
||||||
if (newFunction == none) return false; |
|
||||||
|
|
||||||
oldFunctionLowerCase = oldFunction.LowerCopy(); |
|
||||||
newFunctionLowerCase = newFunction.LowerCopy(); |
|
||||||
result = _replaceFunction(oldFunctionLowerCase, newFunctionLowerCase); |
|
||||||
if (result) { |
|
||||||
RecordNewReplacement(oldFunctionLowerCase, newFunctionLowerCase, replacementReason); |
|
||||||
} |
|
||||||
_.memory.Free2(oldFunctionLowerCase, newFunctionLowerCase); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
/// Replaces one function with another by modifying its script code in real-time. |
|
||||||
/// |
|
||||||
/// The reason for replacement must be specified and will serve as a human-readable explanation |
|
||||||
/// for a [SideEffect] associated with the replacement. |
|
||||||
/// |
|
||||||
/// If you need to replace a function in a class, follow these steps: |
|
||||||
/// |
|
||||||
/// 1. Create a new class that extends the class in which the function you want to replace is |
|
||||||
/// located. |
|
||||||
/// 2. Declare that function in the created class. |
|
||||||
/// 3. **DO NOT** change the function declaration and argument types/amount. |
|
||||||
/// 4. **DO NOT** create new local variables, as this can cause random crashes. |
|
||||||
/// If you need additional variables, make them global and access them using the |
|
||||||
/// `class'myNewClass'.default.myNewVariable` syntax. |
|
||||||
/// 5. If you want to call or override parent code, make sure to always specify the desired parent |
|
||||||
/// class name. |
|
||||||
/// For example, use `super(TargetClass).PostBeginPlay()` instead of `super.PostBeginPlay()`. |
|
||||||
/// This will prevent runaway loop crashes. |
|
||||||
/// 6. Make your edits to the function's code, and then call the replacement function: |
|
||||||
/// ```unrealscript |
|
||||||
/// _.unflect.ReplaceFunction( |
|
||||||
/// "package.class.targetFunction", |
|
||||||
/// "myNewPackage.myNewClass.newFunction"); |
|
||||||
/// ``` |
|
||||||
/// |
|
||||||
/// Following these steps will help ensure that your code changes are compatible with the rest of |
|
||||||
/// the codebase and do not cause unexpected crashes. |
|
||||||
/// |
|
||||||
/// # Errors |
|
||||||
/// |
|
||||||
/// This method can log error messages in cases where: |
|
||||||
/// |
|
||||||
/// * The specified function(s) cannot be found. |
|
||||||
/// * An attempt is made to replace a function with itself. |
|
||||||
/// * An attempt is made to replace a function that has already been replaced. |
|
||||||
public final function bool ReplaceFunction_S( |
|
||||||
string oldFunction, |
|
||||||
string newFunction, |
|
||||||
string replacementReason |
|
||||||
) { |
|
||||||
local Text oldWrapper, newWrapper, reasonWrapper; |
|
||||||
local bool result; |
|
||||||
|
|
||||||
oldWrapper = _.text.FromString(oldFunction); |
|
||||||
newWrapper = _.text.FromString(newFunction); |
|
||||||
reasonWrapper = _.text.FromString(replacementReason); |
|
||||||
result = ReplaceFunction(oldWrapper, newWrapper, reasonWrapper); |
|
||||||
_.memory.Free3(oldWrapper, newWrapper, reasonWrapper); |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
// Does actual work for function replacement. |
|
||||||
// Arguments are assumed to be not `none` and in lower case. |
|
||||||
private final function bool _replaceFunction(Text oldFunctionLowerCase, Text newFunctionLowerCase) { |
|
||||||
local ByteArrayBox initialCode; |
|
||||||
local UFunction replace, with; |
|
||||||
|
|
||||||
replace = FindFunction(oldFunctionLowerCase); |
|
||||||
if (replace == none) { |
|
||||||
_.logger.Auto(errFailedToFindFunction).Arg(oldFunctionLowerCase.Copy()); |
|
||||||
return false; |
|
||||||
} |
|
||||||
with = FindFunction(newFunctionLowerCase); |
|
||||||
if (with == none) { |
|
||||||
_.logger.Auto(errFailedToFindFunction).Arg(newFunctionLowerCase.Copy()); |
|
||||||
return false; |
|
||||||
} |
|
||||||
if (replace == with) { |
|
||||||
_.logger.Auto(warnSameFunction).Arg(oldFunctionLowerCase.Copy()); |
|
||||||
return false; |
|
||||||
} |
|
||||||
// Remember old code, if haven't done so yet. |
|
||||||
// Since we attempt it on each replacement, the first recorded `script` value will be |
|
||||||
// the initial code. |
|
||||||
if (!originalScriptCodes.HasKey(oldFunctionLowerCase)) { |
|
||||||
initialCode = _.box.ByteArray(replace.script); |
|
||||||
originalScriptCodes.SetItem(oldFunctionLowerCase, initialCode); |
|
||||||
_.memory.Free(initialCode); |
|
||||||
} |
|
||||||
replace.script = with.script; |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// Arguments assumed to be not `none` and in lower case. |
|
||||||
private final function UFunction FindFunction(Text functionNameLowerCase) { |
|
||||||
local string stringName; |
|
||||||
|
|
||||||
stringName = functionNameLowerCase.ToString(); |
|
||||||
// We need to make sure functions are loaded before performing the replacement. |
|
||||||
DynamicLoadObject(GetClassName(stringName), class'class', true); |
|
||||||
return functionCaster.Cast(function(FindObject(stringName, class'Function'))); |
|
||||||
} |
|
||||||
|
|
||||||
// Arguments are assumed to be not `none`. |
|
||||||
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case. |
|
||||||
private final function RecordNewReplacement( |
|
||||||
Text oldFunctionLowerCase, |
|
||||||
Text newFunctionLowerCase, |
|
||||||
BaseText replacementReason |
|
||||||
) { |
|
||||||
local SideEffect oldSideEffect, newSideEffect; |
|
||||||
local FunctionReplacement oldRule, newRule; |
|
||||||
|
|
||||||
// Remove old `FunctionReplacement`, if there is any |
|
||||||
oldRule = FunctionReplacement(completedReplacements.GetItem(oldFunctionLowerCase)); |
|
||||||
if (oldRule != none) { |
|
||||||
_.logger |
|
||||||
.Auto(warnOverridingReplacement) |
|
||||||
.Arg(oldFunctionLowerCase.Copy()) |
|
||||||
.Arg(oldRule.GetReplacerFunctionName()) |
|
||||||
.Arg(newFunctionLowerCase.Copy()); |
|
||||||
oldSideEffect = oldRule.GetSideEffect(); |
|
||||||
_.sideEffects.RemoveInstance(oldSideEffect); |
|
||||||
_.memory.Free2(oldRule, oldSideEffect); |
|
||||||
} |
|
||||||
// Create replacement instance |
|
||||||
newSideEffect = MakeSideEffect(oldFunctionLowerCase, newFunctionLowerCase, replacementReason); |
|
||||||
newRule = class'FunctionReplacement'.static |
|
||||||
.Make(oldFunctionLowerCase, newFunctionLowerCase, newSideEffect); |
|
||||||
completedReplacements.SetItem(oldFunctionLowerCase, newRule); |
|
||||||
if (newRule == none) { |
|
||||||
_.logger |
|
||||||
.Auto(errCannotCreateReplacementRule) |
|
||||||
.Arg(oldFunctionLowerCase.Copy()) |
|
||||||
.Arg(newFunctionLowerCase.Copy()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Arguments are assumed to be not `none`. |
|
||||||
// `oldFunctionLowerCase` and `newFunctionLowerCase` are assumed to be in lower case. |
|
||||||
private final function SideEffect MakeSideEffect( |
|
||||||
Text oldFunctionLowerCase, |
|
||||||
Text newFunctionLowerCase, |
|
||||||
BaseText replacementReason |
|
||||||
) { |
|
||||||
local SideEffect sideEffect; |
|
||||||
local MutableText status; |
|
||||||
|
|
||||||
// Add side effect |
|
||||||
status = oldFunctionLowerCase.MutableCopy(); |
|
||||||
status.Append(P(" -> ")); |
|
||||||
status.Append(newFunctionLowerCase); |
|
||||||
sideEffect = _.sideEffects.Add( |
|
||||||
P("Changed function's code"), |
|
||||||
replacementReason, |
|
||||||
P("AcediaCore"), |
|
||||||
P("UnflectAPI"), |
|
||||||
status |
|
||||||
); |
|
||||||
_.memory.Free(status); |
|
||||||
return sideEffect; |
|
||||||
} |
|
||||||
|
|
||||||
// Get the "package + dot + class" string for DynamicLoadObject() |
|
||||||
private final static function string GetClassName(string input) { |
|
||||||
local array<string> parts; |
|
||||||
|
|
||||||
// create an array |
|
||||||
Split(input, ".", parts); |
|
||||||
|
|
||||||
// state functions |
|
||||||
if (parts.length < 3) { |
|
||||||
return ""; |
|
||||||
} |
|
||||||
if (parts.length == 4) { |
|
||||||
ReplaceText(input, "." $ parts[2], ""); |
|
||||||
ReplaceText(input, "." $ parts[3], ""); |
|
||||||
} else { |
|
||||||
ReplaceText(input, "." $ parts[2], ""); |
|
||||||
} |
|
||||||
return input; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
warnOverridingReplacement = (l=LOG_Error,m="Attempt to replace a function `%1` with function `%3` after it has already been replaced with `%2`.") |
|
||||||
warnSameFunction = (l=LOG_Error,m="Attempt to replace a function `%1` with itself.") |
|
||||||
errFailedToFindFunction = (l=LOG_Error,m="`UnflectApi` has failed to find function `%1`.") |
|
||||||
errReplacementWithoutSources = (l=LOG_Error,m="Cannot restore function `%1` - its initial source code wasn't preserved. This most likely means that it wasn't yet replaced.") |
|
||||||
errCannotCreateReplacementRule = (l=LOG_Error,m="`Cannot create new rule for replacing function `%1` with `%2` even though code was successfully replaces. This should happen, please report this.") |
|
||||||
} |
|
@ -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 ChatAPI_OnVoiceMessage_Signal extends Signal |
|
||||||
dependsOn(ChatApi); |
|
||||||
|
|
||||||
public final function Emit(EPlayer sender, ChatApi.BuiltInVoiceMessage message) { |
|
||||||
local Slot nextSlot; |
|
||||||
|
|
||||||
StartIterating(); |
|
||||||
nextSlot = GetNextSlot(); |
|
||||||
while (nextSlot != none) { |
|
||||||
ChatAPI_OnVoiceMessage_Slot(nextSlot).connect(sender, message); |
|
||||||
nextSlot = GetNextSlot(); |
|
||||||
} |
|
||||||
CleanEmptySlots(); |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
relatedSlotClass = class'ChatAPI_OnVoiceMessage_Slot' |
|
||||||
} |
|
@ -1,41 +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 ChatAPI_OnVoiceMessage_Slot extends Slot; |
|
||||||
|
|
||||||
delegate connect( |
|
||||||
EPlayer sender, |
|
||||||
ChatApi.BuiltInVoiceMessage message |
|
||||||
) { |
|
||||||
DummyCall(); |
|
||||||
} |
|
||||||
|
|
||||||
protected function Constructor() { |
|
||||||
connect = none; |
|
||||||
} |
|
||||||
|
|
||||||
protected function Finalizer() { |
|
||||||
super.Finalizer(); |
|
||||||
connect = none; |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -1,81 +0,0 @@ |
|||||||
/** |
|
||||||
* Set of tests for `Command` class. |
|
||||||
* 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_VoiceMessages extends TestCase |
|
||||||
abstract |
|
||||||
dependsOn(ChatApi); |
|
||||||
|
|
||||||
private static function ChatApi.NativeVoiceMessage Make(name type, int index) { |
|
||||||
local ChatApi.NativeVoiceMessage result; |
|
||||||
|
|
||||||
result.type = type; |
|
||||||
result.index = index; |
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
private static function TestEquality(name type, int index, ChatApi.BuiltInVoiceMessage builtIn) { |
|
||||||
local ChatApi.NativeVoiceMessage nativeMessage; |
|
||||||
|
|
||||||
nativeMessage.type = type; |
|
||||||
nativeMessage.index = index; |
|
||||||
|
|
||||||
Issue("Native voice messages are incorrectly converted into `BuiltInVoiceMessage`."); |
|
||||||
TEST_ExpectTrue(__().chat._nativeVoiceMessageIntoEnum(nativeMessage) == builtIn); |
|
||||||
|
|
||||||
Issue("`BuiltInVoiceMessage`s are incorrectly converted into native voice messages."); |
|
||||||
TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(builtIn).type == type); |
|
||||||
TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(builtIn).index == index); |
|
||||||
} |
|
||||||
|
|
||||||
protected static function TESTS() { |
|
||||||
Context("Testing internal conversion methods between voice messages."); |
|
||||||
Test_VoiceMessageConversion(); |
|
||||||
} |
|
||||||
protected static function Test_VoiceMessageConversion() { |
|
||||||
local ChatApi.NativeVoiceMessage nothingMessage; |
|
||||||
|
|
||||||
nothingMessage.type = 'TRADER'; |
|
||||||
nothingMessage.index = 5; |
|
||||||
|
|
||||||
TestEquality('SUPPORT', 2, BIVM_SupportAskForMoney); |
|
||||||
TestEquality('ACK', 0, BIVM_AckYes); |
|
||||||
TestEquality('ALERT', 5, BIVM_AlretFollowMe); |
|
||||||
TestEquality('DIRECTION', 2, BIVM_DirectionGoDownstairs); |
|
||||||
TestEquality('INSULT', 1, BIVM_InsultPlayers); |
|
||||||
TestEquality('TRADER', 2, BIVM_TraderShopOpen); |
|
||||||
TestEquality('TRADER', 4, BIVM_Trader30SecondsUntilShopCloses); |
|
||||||
TestEquality('TRADER', 6, BIVM_TraderShopClosed); |
|
||||||
TestEquality('TRADER', 7, BIVM_TraderCompliment); |
|
||||||
TestEquality('TRADER', 11, BIVM_TraderHurryUp2); |
|
||||||
TestEquality('AUTO', 0, BIVM_AutoWelding); |
|
||||||
TestEquality('AUTO', 16, BIVM_AutoSirenScream); |
|
||||||
TestEquality('AUTO', 24, BIVM_AutoEquipFireaxe); |
|
||||||
|
|
||||||
// Test unknown separately |
|
||||||
Issue("Native voice messages are incorrectly converted into `BuiltInVoiceMessage`."); |
|
||||||
TEST_ExpectTrue(__().chat._nativeVoiceMessageIntoEnum(nothingMessage) == BIVM_Unknown); |
|
||||||
|
|
||||||
Issue("`BuiltInVoiceMessage`s are incorrectly converted into native voice messages."); |
|
||||||
TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(BIVM_Unknown).type == ''); |
|
||||||
TEST_ExpectTrue(__().chat._enumIntoNativeVoiceMessage(BIVM_Unknown).index == 0); |
|
||||||
} |
|
||||||
defaultproperties { |
|
||||||
caseName = "Voice messages" |
|
||||||
caseGroup = "ChatAPI" |
|
||||||
} |
|
@ -1,59 +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 Unflect_ChatApi_Controller extends KFPlayerController |
|
||||||
dependsOn(ChatAPI); |
|
||||||
|
|
||||||
function SendVoiceMessage( |
|
||||||
PlayerReplicationInfo sender, |
|
||||||
PlayerReplicationInfo statedRecipient, |
|
||||||
name messageType, |
|
||||||
byte messageID, |
|
||||||
name broadcastType, |
|
||||||
optional Pawn soundSender, |
|
||||||
optional vector senderLocation |
|
||||||
) { |
|
||||||
local Controller recepient; |
|
||||||
local KFPlayerController kfPlayerRecepient; |
|
||||||
|
|
||||||
// Don't allow dead people to talk |
|
||||||
if (pawn == none) return; |
|
||||||
if (!AllowVoiceMessage(messageType)) return; |
|
||||||
|
|
||||||
class'Global'.static.GetInstance().chat._EmitOnVoiceMessage(self, messageType, messageID); |
|
||||||
recepient = level.controllerList; |
|
||||||
while (recepient != none) { |
|
||||||
kfPlayerRecepient = KFPlayerController(recepient); |
|
||||||
if (kfPlayerRecepient != none) { |
|
||||||
kfPlayerRecepient.ClientLocationalVoiceMessage( |
|
||||||
sender, |
|
||||||
statedRecipient, |
|
||||||
messagetype, |
|
||||||
messageID, |
|
||||||
soundSender, |
|
||||||
senderLocation); |
|
||||||
} |
|
||||||
recepient = recepient.nextController; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
defaultproperties { |
|
||||||
} |
|
@ -0,0 +1,115 @@ |
|||||||
|
/** |
||||||
|
* Standard implementation for simple API for managing a list of |
||||||
|
* `SideEffect` info objects: can add, remove, return all and by package. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class KF1_SideEffectAPI extends SideEffectAPI; |
||||||
|
|
||||||
|
var private array<SideEffect> activeSideEffects; |
||||||
|
|
||||||
|
public function array<SideEffect> GetAll() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
for (i = 0; i < activeSideEffects.length; i += 1) { |
||||||
|
activeSideEffects[i].NewRef(); |
||||||
|
} |
||||||
|
return activeSideEffects; |
||||||
|
} |
||||||
|
|
||||||
|
public function SideEffect GetClass(class<SideEffect> sideEffectClass) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (sideEffectClass == none) { |
||||||
|
return none; |
||||||
|
} |
||||||
|
for (i = 0; i < activeSideEffects.length; i += 1) |
||||||
|
{ |
||||||
|
if (activeSideEffects[i].class == sideEffectClass) |
||||||
|
{ |
||||||
|
activeSideEffects[i].NewRef(); |
||||||
|
return activeSideEffects[i]; |
||||||
|
} |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
public function array<SideEffect> GetFromPackage(BaseText packageName) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local Text nextPackage; |
||||||
|
local array<SideEffect> result; |
||||||
|
|
||||||
|
if (packageName == none) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
for (i = 0; i < activeSideEffects.length; i += 1) |
||||||
|
{ |
||||||
|
nextPackage = activeSideEffects[i].GetPackage(); |
||||||
|
if (nextPackage.Compare(packageName, SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
activeSideEffects[i].NewRef(); |
||||||
|
result[result.length] = activeSideEffects[i]; |
||||||
|
} |
||||||
|
_.memory.Free(nextPackage); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public function bool Add(SideEffect newSideEffect) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (newSideEffect == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (i = 0; i < activeSideEffects.length; i += 1) |
||||||
|
{ |
||||||
|
if (activeSideEffects[i].class == newSideEffect.class) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
newSideEffect.NewRef(); |
||||||
|
activeSideEffects[activeSideEffects.length] = newSideEffect; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public function bool RemoveClass( |
||||||
|
class<SideEffect> sideEffectClass) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (sideEffectClass == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (i = 0; i < activeSideEffects.length; i += 1) |
||||||
|
{ |
||||||
|
if (activeSideEffects[i].class == sideEffectClass) |
||||||
|
{ |
||||||
|
_.memory.Free(activeSideEffects[i]); |
||||||
|
activeSideEffects.Remove(i, 1); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
/** |
||||||
|
* Object representing a side effect introduced into the game/server. |
||||||
|
* Side effects in Acedia refer to changes that aren't a part of mod's main |
||||||
|
* functionality, but rather something necessary to make that functionality |
||||||
|
* possible that might also affect how other mods work. |
||||||
|
* This is a simple data container that is meant to describe relevant |
||||||
|
* changes to the human user. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class BroadcastSideEffect extends SideEffect |
||||||
|
dependson(BroadcastAPI); |
||||||
|
|
||||||
|
public final function Initialize(BroadcastAPI.InjectionLevel usedInjectionLevel) |
||||||
|
{ |
||||||
|
sideEffectName = |
||||||
|
_.text.FromString("AcediaCore's `BroadcastHandler` injected"); |
||||||
|
sideEffectDescription = |
||||||
|
_.text.FromString("Handling text and localized messages between server" |
||||||
|
@ "and clients requires AcediaCore to add its own `BroadcastHandler`" |
||||||
|
@ "into their linked list." |
||||||
|
@ "This is normal, since `BroadcastHandler` class was designed to allow" |
||||||
|
@ "mods to do that, however, for full functionality Acedia requires to" |
||||||
|
@ "inject it as the very first element (`BHIJ_Root` level injection)," |
||||||
|
@ "since some of the events become otherwise inaccessible." |
||||||
|
@ "This can result in incompatibility with other mods that are trying" |
||||||
|
@ "to do the same." |
||||||
|
@ "For that reason AcediaCore can also inject its `BroadcastHandler` as" |
||||||
|
@ "`BHIJ_Registered`."); |
||||||
|
sideEffectPackage = _.text.FromString("AcediaCore"); |
||||||
|
sideEffectSource = _.text.FromString("UnrealAPI"); |
||||||
|
if (usedInjectionLevel == BHIJ_Root) |
||||||
|
{ |
||||||
|
sideEffectStatus = |
||||||
|
_.text.FromFormattedString("{$TextPositive BHIJ_Root}"); |
||||||
|
} |
||||||
|
else if (usedInjectionLevel == BHIJ_Registered) |
||||||
|
{ |
||||||
|
sideEffectStatus = |
||||||
|
_.text.FromFormattedString("{$TextNetutral BHIJ_Registered}"); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
sideEffectStatus = |
||||||
|
_.text.FromFormattedString("{$TextNegative BHIJ_None (???)}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/** |
||||||
|
* Object representing a side effect introduced into the game/server. |
||||||
|
* Side effects in Acedia refer to changes that aren't a part of mod's main |
||||||
|
* functionality, but rather something necessary to make that functionality |
||||||
|
* possible that might also affect how other mods work. |
||||||
|
* This is a simple data container that is meant to describe relevant |
||||||
|
* changes to the human user. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class GameRulesSideEffect extends SideEffect; |
||||||
|
|
||||||
|
public final function Initialize() |
||||||
|
{ |
||||||
|
sideEffectName = |
||||||
|
_.text.FromString("AcediaCore's `AcediaGameRules` added"); |
||||||
|
sideEffectDescription = |
||||||
|
_.text.FromString("`GameRule`s is one of the main ways to get notified" |
||||||
|
@ "about various gameplay-related events in Unreal Engine." |
||||||
|
@ "Of course AcediaCore would require handling some of those events," |
||||||
|
@ "depending on how it's used."); |
||||||
|
sideEffectPackage = _.text.FromString("AcediaCore"); |
||||||
|
sideEffectSource = _.text.FromString("UnrealAPI"); |
||||||
|
sideEffectStatus = _.text.FromFormattedString("{$TextPositive active}"); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,164 @@ |
|||||||
|
/** |
||||||
|
* Object representing a side effect introduced into the game/server. |
||||||
|
* Side effects in Acedia refer to changes that aren't a part of mod's main |
||||||
|
* functionality, but rather something necessary to make that functionality |
||||||
|
* possible that might also affect how other mods work. |
||||||
|
* This is a simple data container that is meant to describe relevant |
||||||
|
* changes to the human user. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class SideEffect extends AcediaObject |
||||||
|
abstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* # Side effects |
||||||
|
* |
||||||
|
* In Acedia "side effect" refers to changes that aren't a part of mod's |
||||||
|
* main functionality, but rather something necessary to make that |
||||||
|
* functionality possible that might also affect how other mods work. Their |
||||||
|
* purpose is to help developers and server admins figure out what changes were |
||||||
|
* performed by Acedia or mods based on it. |
||||||
|
* What needs to be considered a side effect is loosely defined and they |
||||||
|
* are simply a tool to inform others that something they might not have |
||||||
|
* expected has happened, that can possibly break other (their) mods. |
||||||
|
* AcediaCore, for example, tried to leave a minimal footprint, avoiding |
||||||
|
* making any changes to the game classes unless requested, but it still has to |
||||||
|
* do some changes (adding `GameRules`, replacing damage types for some zeds, |
||||||
|
* etc.) and `SideEffect`s can be used to document these changes. They can be |
||||||
|
* used in a similar way for AcediaFixes - a package that is only meant for |
||||||
|
* fixing bugs, but inevitably has to make a lot of under the hood changes to |
||||||
|
* achieve that. |
||||||
|
* On the other hand gameplay mods like Futility can make a lot of changes, |
||||||
|
* but they can all be just expected part of its direct functionality: we |
||||||
|
* expect feature that shares dosh of leavers to alter players' dosh values, so |
||||||
|
* this is not a side effect. Such mods are likely not going to have to specify |
||||||
|
* any side effects whatsoever. |
||||||
|
* |
||||||
|
* ## Implementing your own `SideEffect`s |
||||||
|
* |
||||||
|
* Simply make a non-abstract child class based on `SideEffect`, create its |
||||||
|
* instance filled with necessary data and register it in `SideEffectAPI` once |
||||||
|
* side effect is introduced. If you revert introduced side effect, you should |
||||||
|
* remove registered object through the same API. |
||||||
|
* Each class of the `SideEffect` is supposed to represent a particular |
||||||
|
* side effect introduced into the game engine. Whether side effect is active |
||||||
|
* is decided by whether it is currently registered in `SideEffectAPI`. |
||||||
|
* |
||||||
|
* NOTE: `SideEffect` should not have any logic and should serve as |
||||||
|
* an immutable data container. |
||||||
|
*/ |
||||||
|
|
||||||
|
var protected Text sideEffectName; |
||||||
|
var protected Text sideEffectDescription; |
||||||
|
var protected Text sideEffectPackage; |
||||||
|
var protected Text sideEffectSource; |
||||||
|
var protected Text sideEffectStatus; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns name (short description) of the caller `SideEffect`. User need to |
||||||
|
* be able to tell what this `SideEffect` is generally about from the glance at |
||||||
|
* this name. |
||||||
|
* |
||||||
|
* Guideline is for this value to not exceed `80` characters, but this is not |
||||||
|
* enforced. |
||||||
|
* |
||||||
|
* Must not be `none`. |
||||||
|
* |
||||||
|
* @return Name (short description) of the caller `SideEffect`. |
||||||
|
* Guaranteed to not be `none`. |
||||||
|
*/ |
||||||
|
public function Text GetName() |
||||||
|
{ |
||||||
|
if (sideEffectName != none) { |
||||||
|
return sideEffectName.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns description of the caller `SideEffect`. This should describe what |
||||||
|
* was done and why relevant change was necessary. |
||||||
|
* |
||||||
|
* Must not be `none`. |
||||||
|
* |
||||||
|
* @return Description of the caller `SideEffect`. |
||||||
|
* Guaranteed to not be `none`. |
||||||
|
*/ |
||||||
|
public function Text GetDescription() |
||||||
|
{ |
||||||
|
if (sideEffectDescription != none) { |
||||||
|
return sideEffectDescription.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns name of the package ("*.u" file) that introduced this change. |
||||||
|
* |
||||||
|
* Note that if package "A" actually performed the change because another |
||||||
|
* package "B" requested certain functionality, it is still package "A" that is |
||||||
|
* responsible for the side effect. |
||||||
|
* |
||||||
|
* Must not be `none`. |
||||||
|
* |
||||||
|
* @return Name of the package ("*.u" file) that introduced this change. |
||||||
|
* Guaranteed to not be `none`. |
||||||
|
*/ |
||||||
|
public function Text GetPackage() |
||||||
|
{ |
||||||
|
if (sideEffectPackage != none) { |
||||||
|
return sideEffectPackage.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* What part of package caused this change. For huge packages can be used to |
||||||
|
* further specify what introduced the change. |
||||||
|
* |
||||||
|
* Returned value can be `none` (e.g. when it is unnecessary for small |
||||||
|
* packages). |
||||||
|
* |
||||||
|
* @return Name (short description) of the part of the package that caused |
||||||
|
* caller `SideEffect`. |
||||||
|
*/ |
||||||
|
public function Text GetSource() |
||||||
|
{ |
||||||
|
if (sideEffectSource != none) { |
||||||
|
return sideEffectSource.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Status of the caller `SideEffect`. Some side effects can be introduced in |
||||||
|
* several different ways - this value needs to help distinguish between them. |
||||||
|
* |
||||||
|
* @return Status of the caller `SideEffect`. |
||||||
|
*/ |
||||||
|
public function Text GetStatus() |
||||||
|
{ |
||||||
|
if (sideEffectStatus != none) { |
||||||
|
return sideEffectStatus.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
/** |
||||||
|
* Base class for simple API for managing a list of `SideEffect` info |
||||||
|
* objects: can add, remove, return all and by package. |
||||||
|
* Copyright 2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class SideEffectAPI extends AcediaObject |
||||||
|
abstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns all so far registered `SideEffect`s. |
||||||
|
* |
||||||
|
* @return Array of all registered `SideEffect`s. |
||||||
|
*/ |
||||||
|
public function array<SideEffect> GetAll(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns active `SideEffect` instance of the specified class |
||||||
|
* `sideEffectClass`. |
||||||
|
* |
||||||
|
* @param sideEffectClass Class of side effect to return active instance of. |
||||||
|
* @return Active `SideEffect` instance of the specified class |
||||||
|
* `sideEffectClass`. |
||||||
|
* `none` if either `sideEffectClass` is `none` or side effect of such |
||||||
|
* class is not currently active. |
||||||
|
*/ |
||||||
|
public function SideEffect GetClass(class<SideEffect> sideEffectClass); |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns all so far registered `SideEffect`s from a package `packageName` |
||||||
|
* (case insensitive). |
||||||
|
* |
||||||
|
* @param packageName Name of the package, in `SideEffect`s from which we are |
||||||
|
* interested. Must be not `none`. |
||||||
|
* @return Array of all registered `SideEffect`s from a package `packageName`. |
||||||
|
* If `none`, returns an empty array. |
||||||
|
*/ |
||||||
|
public function array<SideEffect> GetFromPackage(BaseText packageName); |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers a new `SideEffect` object as active. |
||||||
|
* |
||||||
|
* @param newSideEffect Instance of some `SideEffect` class to register as |
||||||
|
* active side effect. Must not be `none`. |
||||||
|
* @return `true` if new side effect was added and `false` otherwise. |
||||||
|
*/ |
||||||
|
public function bool Add(SideEffect newSideEffect); |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes `SideEffect` of the specified sub-class from the list of active |
||||||
|
* side effects. |
||||||
|
* |
||||||
|
* @param sideEffectClass Class of the side effect to remove. |
||||||
|
* @return `true` if some side effect was removed as a result of this operation |
||||||
|
* and `false` otherwise (even if there was no side effect of specified |
||||||
|
* class to begin with). |
||||||
|
*/ |
||||||
|
public function bool RemoveClass(class<SideEffect> sideEffectClass); |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
@ -0,0 +1,697 @@ |
|||||||
|
/** |
||||||
|
* This class is meant to represent a command type: to create new command |
||||||
|
* one should extend it, then simply define required sub-commands/options and |
||||||
|
* parameters in `BuildData()` and overload `Executed()` / `ExecutedFor()` |
||||||
|
* to perform required actions when command is executed by a player. |
||||||
|
* `Executed()` is called first, whenever command is executed and |
||||||
|
* `ExecuteFor()` is called only for targeted commands, once for each |
||||||
|
* targeted player. |
||||||
|
* Copyright 2021-2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Command extends AcediaObject |
||||||
|
dependson(BaseText); |
||||||
|
|
||||||
|
/** |
||||||
|
* # `Command` |
||||||
|
* |
||||||
|
* Command class provides an automated way to add a command to a server through |
||||||
|
* AcediaCore's features. It takes care of: |
||||||
|
* |
||||||
|
* 1. Verifying that player has passed correct (expected parameters); |
||||||
|
* 2. Parsing these parameters into usable values (both standard, built-in |
||||||
|
* types like `bool`, `int`, `float`, etc. and more advanced types such |
||||||
|
* as players lists and JSON values); |
||||||
|
* 3. Allowing you to easily specify a set of players you are targeting by |
||||||
|
* supporting several ways to refer to them, such as *by name*, *by id* |
||||||
|
* and *by selector* (@ and @self refer to caller player, @all refers |
||||||
|
* to all players). |
||||||
|
* 4. It can be registered inside AcediaCore's commands feature and be |
||||||
|
* automatically called through the unified system that supports *chat* |
||||||
|
* and *mutate* inputs (as well as allowing you to hook in any other |
||||||
|
* input source); |
||||||
|
* 5. Will also automatically provide a help page through built-in "help" |
||||||
|
* command; |
||||||
|
* 6. Subcommand support - when one command can have several distinct |
||||||
|
* functions, depending on how its called (e.g. "inventory add" vs |
||||||
|
* "inventory remove"). These subcommands have a special treatment in |
||||||
|
* help pages, which makes them more preferable, compared to simply |
||||||
|
* matching first `Text` argument; |
||||||
|
* 7. Add support for "options" - additional flags that can modify commands |
||||||
|
* behavior and behave like usual command options "--force"/"-f". |
||||||
|
* Their short versions can even be combined: |
||||||
|
* "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af". |
||||||
|
* And they can have their own parameters: "give@all --list sharp". |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* To create a custom command you need to simply: |
||||||
|
* |
||||||
|
* 1. Create a custom command class derived from `Command`; |
||||||
|
* 2. Define `BuildData()` function and use given `CommandDataBuilder` to |
||||||
|
* fill-in data about what parameters your command takes. You can also |
||||||
|
* add optional descriptions that would appear in your command's |
||||||
|
* help page. |
||||||
|
* 3. Overload `Executed()` or `ExecutedFor()` (or both) method and add |
||||||
|
* whatever logic you want to execute once your command was called. |
||||||
|
* All parameters and options will be listed inside passed `CallData` |
||||||
|
* parameter. These methods will only be called if all necessary |
||||||
|
* parameters were correctly specified. |
||||||
|
* |
||||||
|
* ## Implementation |
||||||
|
* |
||||||
|
* The idea of `Command`'s implementation is simple: command is basically |
||||||
|
* the `Command.Data` struct that is filled via `CommandDataBuilder`. |
||||||
|
* Whenever command is called it uses `CommandParser` to parse user's input |
||||||
|
* based on its `Command.Data` and either report error (in case of failure) or |
||||||
|
* pass make `Executed()`/`ExecutedFor()` calls (in case of success). |
||||||
|
* When command is called is decided by `Commands_Feature` that tracks |
||||||
|
* possible user inputs (and provides `HandleInput()`/`HandleInputWith()` |
||||||
|
* methods for adding custom command inputs). That feature basically parses |
||||||
|
* first part of the command: its name (not the subcommand's names) and target |
||||||
|
* players (using `PlayersParser`, but only if command is targeted). |
||||||
|
* |
||||||
|
* Majority of the command-related code either serves to build |
||||||
|
* `Command.Data` or to parse command input by using it (`CommandParser`). |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Possible errors that can arise when parsing command parameters from |
||||||
|
* user input |
||||||
|
*/ |
||||||
|
enum ErrorType |
||||||
|
{ |
||||||
|
// No error |
||||||
|
CET_None, |
||||||
|
// Bad parser was provided to parse user input |
||||||
|
// (this should not be possible) |
||||||
|
CET_BadParser, |
||||||
|
// Sub-command name was not specified or was incorrect |
||||||
|
// (this should not be possible) |
||||||
|
CET_NoSubCommands, |
||||||
|
// Specified sub-command does not exist |
||||||
|
// (only relevant when it is enforced for parser, e.g. by an alias) |
||||||
|
CET_BadSubCommand, |
||||||
|
// Required param for command / option was not specified |
||||||
|
CET_NoRequiredParam, |
||||||
|
CET_NoRequiredParamForOption, |
||||||
|
// Unknown option key was specified |
||||||
|
CET_UnknownOption, |
||||||
|
CET_UnknownShortOption, |
||||||
|
// Same option appeared twice in one command call |
||||||
|
CET_RepeatedOption, |
||||||
|
// Part of user's input could not be interpreted as a part of |
||||||
|
// command's call |
||||||
|
CET_UnusedCommandParameters, |
||||||
|
// In one short option specification (e.g. '-lah') several options |
||||||
|
// require parameters: this introduces ambiguity and is not allowed |
||||||
|
CET_MultipleOptionsWithParams, |
||||||
|
// (For targeted commands only) |
||||||
|
// Targets are specified incorrectly (or none actually specified) |
||||||
|
CET_IncorrectTargetList, |
||||||
|
CET_EmptyTargetList |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Structure that contains all the information about how `Command` was called. |
||||||
|
*/ |
||||||
|
struct CallData |
||||||
|
{ |
||||||
|
// Targeted players (if applicable) |
||||||
|
var public array<EPlayer> targetPlayers; |
||||||
|
// Specified sub-command and parameters/options |
||||||
|
var public Text subCommandName; |
||||||
|
// Provided parameters and specified options |
||||||
|
var public HashTable parameters; |
||||||
|
var public HashTable options; |
||||||
|
// Errors that occurred during command call processing are described by |
||||||
|
// error type and optional error textual name of the object |
||||||
|
// (parameter, option, etc.) that caused it. |
||||||
|
var public ErrorType parsingError; |
||||||
|
var public Text errorCause; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Possible types of parameters. |
||||||
|
*/ |
||||||
|
enum ParameterType |
||||||
|
{ |
||||||
|
// Parses into `BoolBox` |
||||||
|
CPT_Boolean, |
||||||
|
// Parses into `IntBox` |
||||||
|
CPT_Integer, |
||||||
|
// Parses into `FloatBox` |
||||||
|
CPT_Number, |
||||||
|
// Parses into `Text` |
||||||
|
CPT_Text, |
||||||
|
// Special parameter that consumes the rest of the input into `Text` |
||||||
|
CPT_Remainder, |
||||||
|
// Parses into `HashTable` |
||||||
|
CPT_Object, |
||||||
|
// Parses into `ArrayList` |
||||||
|
CPT_Array, |
||||||
|
// Parses into any JSON value |
||||||
|
CPT_JSON, |
||||||
|
// Parses into an array of specified players |
||||||
|
CPT_Players |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Possible forms a boolean variable can be used as. |
||||||
|
* Boolean parameter can define it's preferred format, which will be used |
||||||
|
* for help page generation. |
||||||
|
*/ |
||||||
|
enum PreferredBooleanFormat |
||||||
|
{ |
||||||
|
PBF_TrueFalse, |
||||||
|
PBF_EnableDisable, |
||||||
|
PBF_OnOff, |
||||||
|
PBF_YesNo |
||||||
|
}; |
||||||
|
|
||||||
|
// Defines a singular command parameter |
||||||
|
struct Parameter |
||||||
|
{ |
||||||
|
// Display name (for the needs of help page displaying) |
||||||
|
var Text displayName; |
||||||
|
// Type of value this parameter would store |
||||||
|
var ParameterType type; |
||||||
|
// Does it take only a singular value or can it contain several of them, |
||||||
|
// written in a list |
||||||
|
var bool allowsList; |
||||||
|
// Variable name that will be used as a key to store parameter's value |
||||||
|
var Text variableName; |
||||||
|
// (For `CPT_Boolean` type variables only) - preferred boolean format, |
||||||
|
// used in help pages |
||||||
|
var PreferredBooleanFormat booleanFormat; |
||||||
|
// `CPT_Text` can be attempted to be auto-resolved as an alias from |
||||||
|
/// some source during parsing. For command to attempt that, this field must |
||||||
|
// be not-`none` and contain the name of the alias source (either "weapon", |
||||||
|
// "color", "feature", "entity" or some kind of custom alias source name). |
||||||
|
// Only relevant when given value is prefixed with "$" character. |
||||||
|
var Text aliasSourceName; |
||||||
|
}; |
||||||
|
|
||||||
|
// Defines a sub-command of a this command (specified as |
||||||
|
// "<command> <sub_command>"). |
||||||
|
// Using sub-command is not optional, but if none defined |
||||||
|
// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`) |
||||||
|
// one is automatically created / used. |
||||||
|
struct SubCommand |
||||||
|
{ |
||||||
|
// Cannot be `none` |
||||||
|
var Text name; |
||||||
|
// Can be `none` |
||||||
|
var Text description; |
||||||
|
var array<Parameter> required; |
||||||
|
var array<Parameter> optional; |
||||||
|
}; |
||||||
|
|
||||||
|
// Defines command's option (options are specified by "--long" or "-l"). |
||||||
|
// Options are independent from sub-commands. |
||||||
|
struct Option |
||||||
|
{ |
||||||
|
var BaseText.Character shortName; |
||||||
|
var Text longName; |
||||||
|
var Text description; |
||||||
|
// Option can also have their own parameters |
||||||
|
var array<Parameter> required; |
||||||
|
var array<Parameter> optional; |
||||||
|
}; |
||||||
|
|
||||||
|
// Structure that defines what sub-commands and options command has |
||||||
|
// (and what parameters they take) |
||||||
|
struct Data |
||||||
|
{ |
||||||
|
// Default command name that will be used unless Acedia is configured to |
||||||
|
// do otherwise |
||||||
|
var protected Text name; |
||||||
|
// Command group this command belongs to |
||||||
|
var protected Text group; |
||||||
|
// Short summary of what command does (recommended to |
||||||
|
// keep it to 80 characters) |
||||||
|
var protected Text summary; |
||||||
|
var protected array<SubCommand> subCommands; |
||||||
|
var protected array<Option> options; |
||||||
|
var protected bool requiresTarget; |
||||||
|
}; |
||||||
|
var private Data commandData; |
||||||
|
|
||||||
|
// We do not really ever need to create more than one instance of each class |
||||||
|
// of `Command`, so we will simply store and reuse one created instance. |
||||||
|
var private Command mainInstance; |
||||||
|
|
||||||
|
/** |
||||||
|
* When command is being executed we create several instances of |
||||||
|
* `ConsoleWriter` that can be used for command output. They will also be |
||||||
|
* automatically deallocated once command is executed. |
||||||
|
* DO NOT modify them or deallocate any of them manually. |
||||||
|
* This should make output more convenient and standardized. |
||||||
|
* |
||||||
|
* 1. `publicConsole` - sends messages to all present players; |
||||||
|
* 2. `callerConsole` - sends messages to the player that |
||||||
|
* called the command; |
||||||
|
* 3. `targetConsole` - sends messages to the player that is currently |
||||||
|
* being targeted (different each call of `ExecutedFor()` and |
||||||
|
* `none` during `Executed()` call); |
||||||
|
* 4. `othersConsole` - sends messaged to every player that is |
||||||
|
* neither "caller" or "target". |
||||||
|
*/ |
||||||
|
var protected ConsoleWriter publicConsole, othersConsole; |
||||||
|
var protected ConsoleWriter callerConsole, targetConsole; |
||||||
|
|
||||||
|
protected function Constructor() |
||||||
|
{ |
||||||
|
local CommandDataBuilder dataBuilder; |
||||||
|
dataBuilder = |
||||||
|
CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
||||||
|
BuildData(dataBuilder); |
||||||
|
commandData = dataBuilder.BorrowData(); |
||||||
|
dataBuilder.FreeSelf(); |
||||||
|
dataBuilder = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<SubCommand> subCommands; |
||||||
|
local array<Option> options; |
||||||
|
|
||||||
|
DeallocateConsoles(); |
||||||
|
_.memory.Free(commandData.name); |
||||||
|
_.memory.Free(commandData.summary); |
||||||
|
subCommands = commandData.subCommands; |
||||||
|
for (i = 0; i < options.length; i += 1) |
||||||
|
{ |
||||||
|
_.memory.Free(subCommands[i].name); |
||||||
|
_.memory.Free(subCommands[i].description); |
||||||
|
CleanParameters(subCommands[i].required); |
||||||
|
CleanParameters(subCommands[i].optional); |
||||||
|
subCommands[i].required.length = 0; |
||||||
|
subCommands[i].optional.length = 0; |
||||||
|
} |
||||||
|
commandData.subCommands.length = 0; |
||||||
|
options = commandData.options; |
||||||
|
for (i = 0; i < options.length; i += 1) |
||||||
|
{ |
||||||
|
_.memory.Free(options[i].longName); |
||||||
|
_.memory.Free(options[i].description); |
||||||
|
CleanParameters(options[i].required); |
||||||
|
CleanParameters(options[i].optional); |
||||||
|
options[i].required.length = 0; |
||||||
|
options[i].optional.length = 0; |
||||||
|
} |
||||||
|
commandData.options.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
private final function CleanParameters(array<Parameter> parameters) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
for (i = 0; i < parameters.length; i += 1) |
||||||
|
{ |
||||||
|
_.memory.Free(parameters[i].displayName); |
||||||
|
_.memory.Free(parameters[i].variableName); |
||||||
|
_.memory.Free(parameters[i].aliasSourceName); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Overload this method to use `builder` to define parameters and options for |
||||||
|
* your command. |
||||||
|
* |
||||||
|
* @param builder Builder that can be used to define your commands parameters |
||||||
|
* and options. Do not deallocate. |
||||||
|
*/ |
||||||
|
protected function BuildData(CommandDataBuilder builder){} |
||||||
|
|
||||||
|
/** |
||||||
|
* Overload this method to perform required actions when |
||||||
|
* your command is called. |
||||||
|
* |
||||||
|
* @param arguments `struct` filled with parameters that your command |
||||||
|
* has been called with. Guaranteed to not be in error state. |
||||||
|
* @param instigator Player that instigated this execution. |
||||||
|
*/ |
||||||
|
protected function Executed(CallData arguments, EPlayer instigator){} |
||||||
|
|
||||||
|
/** |
||||||
|
* Overload this method to perform required actions when your command is called |
||||||
|
* with a given player as a target. If several players have been specified - |
||||||
|
* this method will be called once for each. |
||||||
|
* |
||||||
|
* If your command does not require a target - this method will not be called. |
||||||
|
* |
||||||
|
* @param target Player that this command must perform an action on. |
||||||
|
* @param arguments `struct` filled with parameters that your command |
||||||
|
* has been called with. Guaranteed to not be in error state and contain |
||||||
|
* all the required data. |
||||||
|
* @param instigator Player that instigated this call. |
||||||
|
*/ |
||||||
|
protected function ExecutedFor( |
||||||
|
EPlayer target, |
||||||
|
CallData arguments, |
||||||
|
EPlayer instigator){} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns an instance of command (of particular class) that is stored |
||||||
|
* "as a singleton" in command's class itself. Do not deallocate it. |
||||||
|
*/ |
||||||
|
public final static function Command GetInstance() |
||||||
|
{ |
||||||
|
if (default.mainInstance == none) { |
||||||
|
default.mainInstance = Command(__().memory.Allocate(default.class)); |
||||||
|
} |
||||||
|
return default.mainInstance; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Forces command to process (parse) player's input, producing a structure |
||||||
|
* with parsed data in Acedia's format instead. |
||||||
|
* |
||||||
|
* @see `Execute()` for actually performing command's actions. |
||||||
|
* |
||||||
|
* @param parser Parser that contains command input. |
||||||
|
* @param callerPlayer Player that initiated this command's call, |
||||||
|
* necessary for parsing player list (since it can point at |
||||||
|
* the caller player). |
||||||
|
* @param subCommandName This method can optionally specify sub-command to |
||||||
|
* caller command to use. If this argument's value is `none` - sub-command |
||||||
|
* name will be parsed from the `parser`'s data. |
||||||
|
* @return `CallData` structure that contains all the information about |
||||||
|
* parameters specified in `parser`'s contents. |
||||||
|
* Returned structure contains objects that must be deallocated, |
||||||
|
* which can easily be done by the auxiliary `DeallocateCallData()` method. |
||||||
|
*/ |
||||||
|
public final function CallData ParseInputWith( |
||||||
|
Parser parser, |
||||||
|
EPlayer callerPlayer, |
||||||
|
optional BaseText subCommandName) |
||||||
|
{ |
||||||
|
local array<EPlayer> targetPlayers; |
||||||
|
local CommandParser commandParser; |
||||||
|
local CallData callData; |
||||||
|
|
||||||
|
if (parser == none || !parser.Ok()) |
||||||
|
{ |
||||||
|
callData.parsingError = CET_BadParser; |
||||||
|
return callData; |
||||||
|
} |
||||||
|
// Parse targets and handle errors that can arise here |
||||||
|
if (commandData.requiresTarget) |
||||||
|
{ |
||||||
|
targetPlayers = ParseTargets(parser, callerPlayer); |
||||||
|
if (!parser.Ok()) |
||||||
|
{ |
||||||
|
callData.parsingError = CET_IncorrectTargetList; |
||||||
|
return callData; |
||||||
|
} |
||||||
|
if (targetPlayers.length <= 0) |
||||||
|
{ |
||||||
|
callData.parsingError = CET_EmptyTargetList; |
||||||
|
return callData; |
||||||
|
} |
||||||
|
} |
||||||
|
// Parse parameters themselves |
||||||
|
commandParser = CommandParser(_.memory.Allocate(class'CommandParser')); |
||||||
|
callData = commandParser.ParseWith( |
||||||
|
parser, |
||||||
|
commandData, |
||||||
|
callerPlayer, |
||||||
|
subCommandName); |
||||||
|
callData.targetPlayers = targetPlayers; |
||||||
|
commandParser.FreeSelf(); |
||||||
|
return callData; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes caller `Command` with data provided by `callData` if it is in |
||||||
|
* a correct state and reports error to `callerPlayer` if |
||||||
|
* `callData` is invalid. |
||||||
|
* |
||||||
|
* @param callData Data about parameters, options, etc. with which |
||||||
|
* caller `Command` is to be executed. |
||||||
|
* @param callerPlayer Player that should be considered responsible for |
||||||
|
* executing this `Command`. |
||||||
|
* @return `true` if command was successfully executed and `false` otherwise. |
||||||
|
* Execution is considered successful if `Execute()` call was made, |
||||||
|
* regardless of whether `Command` can actually perform required action. |
||||||
|
* For example, giving a weapon to a player can fail because he does not |
||||||
|
* have enough space in his inventory, but it will still be considered |
||||||
|
* a successful execution as far as return value is concerned. |
||||||
|
*/ |
||||||
|
public final function bool Execute(CallData callData, EPlayer callerPlayer) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<EPlayer> targetPlayers; |
||||||
|
|
||||||
|
if (callerPlayer == none) return false; |
||||||
|
if (!callerPlayer.IsExistent()) return false; |
||||||
|
|
||||||
|
// Report or execute |
||||||
|
if (callData.parsingError != CET_None) |
||||||
|
{ |
||||||
|
ReportError(callData, callerPlayer); |
||||||
|
return false; |
||||||
|
} |
||||||
|
targetPlayers = callData.targetPlayers; |
||||||
|
publicConsole = _.console.ForAll(); |
||||||
|
callerConsole = _.console.For(callerPlayer); |
||||||
|
callerConsole |
||||||
|
.Write(P("Executing command `")) |
||||||
|
.Write(commandData.name) |
||||||
|
.Say(P("`")); |
||||||
|
// `othersConsole` should also exist in time for `Executed()` call |
||||||
|
othersConsole = _.console.ForAll().ButPlayer(callerPlayer); |
||||||
|
Executed(callData, callerPlayer); |
||||||
|
_.memory.Free(othersConsole); |
||||||
|
if (commandData.requiresTarget) |
||||||
|
{ |
||||||
|
for (i = 0; i < targetPlayers.length; i += 1) |
||||||
|
{ |
||||||
|
targetConsole = _.console.For(targetPlayers[i]); |
||||||
|
othersConsole = _.console |
||||||
|
.ForAll() |
||||||
|
.ButPlayer(callerPlayer) |
||||||
|
.ButPlayer(targetPlayers[i]); |
||||||
|
ExecutedFor(targetPlayers[i], callData, callerPlayer); |
||||||
|
_.memory.Free(othersConsole); |
||||||
|
_.memory.Free(targetConsole); |
||||||
|
} |
||||||
|
} |
||||||
|
othersConsole = none; |
||||||
|
targetConsole = none; |
||||||
|
DeallocateConsoles(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
private final function DeallocateConsoles() |
||||||
|
{ |
||||||
|
if (publicConsole != none && publicConsole.IsAllocated()) { |
||||||
|
_.memory.Free(publicConsole); |
||||||
|
} |
||||||
|
if (callerConsole != none && callerConsole.IsAllocated()) { |
||||||
|
_.memory.Free(callerConsole); |
||||||
|
} |
||||||
|
if (targetConsole != none && targetConsole.IsAllocated()) { |
||||||
|
_.memory.Free(targetConsole); |
||||||
|
} |
||||||
|
if (othersConsole != none && othersConsole.IsAllocated()) { |
||||||
|
_.memory.Free(othersConsole); |
||||||
|
} |
||||||
|
publicConsole = none; |
||||||
|
callerConsole = none; |
||||||
|
targetConsole = none; |
||||||
|
othersConsole = none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Auxiliary method that cleans up all data and deallocates all objects inside |
||||||
|
* provided `callData` structure. |
||||||
|
* |
||||||
|
* @param callData Structure to clean. All stored data will be cleared, |
||||||
|
* meaning that `DeallocateCallData()` method takes ownership of |
||||||
|
* this parameter. |
||||||
|
*/ |
||||||
|
public final static function DeallocateCallData(/* take */ CallData callData) |
||||||
|
{ |
||||||
|
__().memory.Free(callData.subCommandName); |
||||||
|
__().memory.Free(callData.parameters); |
||||||
|
__().memory.Free(callData.options); |
||||||
|
__().memory.Free(callData.errorCause); |
||||||
|
__().memory.FreeMany(callData.targetPlayers); |
||||||
|
if (callData.targetPlayers.length > 0) { |
||||||
|
callData.targetPlayers.length = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Reports given error to the `callerPlayer`, appropriately picking |
||||||
|
// message color |
||||||
|
private final function ReportError(CallData callData, EPlayer callerPlayer) |
||||||
|
{ |
||||||
|
local Text errorMessage; |
||||||
|
local ConsoleWriter console; |
||||||
|
|
||||||
|
if (callerPlayer == none) return; |
||||||
|
if (!callerPlayer.IsExistent()) return; |
||||||
|
|
||||||
|
// Setup console color |
||||||
|
console = callerPlayer.BorrowConsole(); |
||||||
|
if (callData.parsingError == CET_EmptyTargetList) { |
||||||
|
console.UseColor(_.color.textWarning); |
||||||
|
} |
||||||
|
else { |
||||||
|
console.UseColor(_.color.textFailure); |
||||||
|
} |
||||||
|
// Send message |
||||||
|
errorMessage = PrintErrorMessage(callData); |
||||||
|
console.Say(errorMessage); |
||||||
|
errorMessage.FreeSelf(); |
||||||
|
// Restore console color |
||||||
|
console.ResetColor().Flush(); |
||||||
|
} |
||||||
|
|
||||||
|
private final function Text PrintErrorMessage(CallData callData) |
||||||
|
{ |
||||||
|
local Text result; |
||||||
|
local MutableText builder; |
||||||
|
|
||||||
|
builder = _.text.Empty(); |
||||||
|
switch (callData.parsingError) |
||||||
|
{ |
||||||
|
case CET_BadParser: |
||||||
|
builder.Append(P("Internal error occurred: invalid parser")); |
||||||
|
break; |
||||||
|
case CET_NoSubCommands: |
||||||
|
builder.Append(P("Ill defined command: no subcommands")); |
||||||
|
break; |
||||||
|
case CET_BadSubCommand: |
||||||
|
builder.Append(P("Ill defined sub-command: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_NoRequiredParam: |
||||||
|
builder.Append(P("Missing required parameter: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_NoRequiredParamForOption: |
||||||
|
builder.Append(P("Missing required parameter for option: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_UnknownOption: |
||||||
|
builder.Append(P("Invalid option specified: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_UnknownShortOption: |
||||||
|
builder.Append(P("Invalid short option specified")); |
||||||
|
break; |
||||||
|
case CET_RepeatedOption: |
||||||
|
builder.Append(P("Option specified several times: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_UnusedCommandParameters: |
||||||
|
builder.Append(P("Part of command could not be parsed: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_MultipleOptionsWithParams: |
||||||
|
builder.Append(P( "Multiple short options in one declarations" |
||||||
|
@ "require parameters: ")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_IncorrectTargetList: |
||||||
|
builder.Append(P("Target players are incorrectly specified.")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
case CET_EmptyTargetList: |
||||||
|
builder.Append(P("List of target players is empty")) |
||||||
|
.Append(callData.errorCause); |
||||||
|
break; |
||||||
|
default: |
||||||
|
} |
||||||
|
result = builder.Copy(); |
||||||
|
builder.FreeSelf(); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
// Auxiliary method for parsing list of targeted players. |
||||||
|
// Assumes given parser is not `none` and not in a failed state. |
||||||
|
// If parsing failed, guaranteed to return an empty array. |
||||||
|
private final function array<EPlayer> ParseTargets( |
||||||
|
Parser parser, |
||||||
|
EPlayer callerPlayer) |
||||||
|
{ |
||||||
|
local array<EPlayer> targetPlayers; |
||||||
|
local PlayersParser targetsParser; |
||||||
|
|
||||||
|
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); |
||||||
|
targetsParser.SetSelf(callerPlayer); |
||||||
|
targetsParser.ParseWith(parser); |
||||||
|
if (parser.Ok()) { |
||||||
|
targetPlayers = targetsParser.GetPlayers(); |
||||||
|
} |
||||||
|
targetsParser.FreeSelf(); |
||||||
|
return targetPlayers; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns name (in lower case) of the caller command class. |
||||||
|
* |
||||||
|
* @return Name (in lower case) of the caller command class. |
||||||
|
* Guaranteed to be not `none`. |
||||||
|
*/ |
||||||
|
public final function Text GetName() |
||||||
|
{ |
||||||
|
if (commandData.name == none) { |
||||||
|
return P("").Copy(); |
||||||
|
} |
||||||
|
return commandData.name.LowerCopy(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns group name (in lower case) of the caller command class. |
||||||
|
* |
||||||
|
* @return Group name (in lower case) of the caller command class. |
||||||
|
* Guaranteed to be not `none`. |
||||||
|
*/ |
||||||
|
public final function Text GetGroupName() |
||||||
|
{ |
||||||
|
if (commandData.group == none) { |
||||||
|
return P("").Copy(); |
||||||
|
} |
||||||
|
return commandData.group.LowerCopy(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns `Command.Data` struct that describes caller `Command`. |
||||||
|
* |
||||||
|
* @return `Command.Data` that describes caller `Command`. Returned struct |
||||||
|
* contains `Text` references that are used internally by the `Command` |
||||||
|
* and not their copies. |
||||||
|
* Generally this is undesired approach and leaves `Command` more |
||||||
|
* vulnerable to modification, but copying all the data inside would not |
||||||
|
* only introduce a largely pointless computational overhead, but also |
||||||
|
* would require some cumbersome logic. This might change in the future, |
||||||
|
* so deallocating any objects in the returned `struct` would lead to |
||||||
|
* undefined behavior. |
||||||
|
*/ |
||||||
|
public final function Data BorrowData() |
||||||
|
{ |
||||||
|
return commandData; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@ |
|||||||
|
/** |
||||||
|
* Config object for `Commands_Feature`. |
||||||
|
* Copyright 2021-2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Commands extends FeatureConfig |
||||||
|
perobjectconfig |
||||||
|
config(AcediaSystem); |
||||||
|
|
||||||
|
var public config bool useChatInput; |
||||||
|
var public config bool useMutateInput; |
||||||
|
var public config string chatCommandPrefix; |
||||||
|
var public config array<string> allowedPlayers; |
||||||
|
|
||||||
|
protected function HashTable ToData() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local HashTable data; |
||||||
|
local ArrayList playerList; |
||||||
|
|
||||||
|
data = __().collections.EmptyHashTable(); |
||||||
|
data.SetBool(P("useChatInput"), useChatInput, true); |
||||||
|
data.SetBool(P("useMutateInput"), useMutateInput, true); |
||||||
|
data.SetString(P("chatCommandPrefix"), chatCommandPrefix); |
||||||
|
playerList = _.collections.EmptyArrayList(); |
||||||
|
for (i = 0; i < allowedPlayers.length; i += 1) { |
||||||
|
playerList.AddString(allowedPlayers[i]); |
||||||
|
} |
||||||
|
data.SetItem(P("allowedPlayers"), playerList); |
||||||
|
playerList.FreeSelf(); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
protected function FromData(HashTable source) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local ArrayList playerList; |
||||||
|
|
||||||
|
if (source == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
useChatInput = source.GetBool(P("useChatInput")); |
||||||
|
useMutateInput = source.GetBool(P("useMutateInput")); |
||||||
|
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!"); |
||||||
|
playerList = source.GetArrayList(P("allowedPlayers")); |
||||||
|
allowedPlayers.length = 0; |
||||||
|
if (playerList == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < playerList.GetLength(); i += 1) { |
||||||
|
allowedPlayers[allowedPlayers.length] = playerList.GetString(i); |
||||||
|
} |
||||||
|
playerList.FreeSelf(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function DefaultIt() |
||||||
|
{ |
||||||
|
useChatInput = true; |
||||||
|
useMutateInput = true; |
||||||
|
chatCommandPrefix = "!"; |
||||||
|
allowedPlayers.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
configName = "AcediaSystem" |
||||||
|
useChatInput = true |
||||||
|
useMutateInput = true |
||||||
|
chatCommandPrefix = "!" |
||||||
|
} |
@ -0,0 +1,626 @@ |
|||||||
|
/** |
||||||
|
* This feature provides a mechanism to define commands that automatically |
||||||
|
* parse their arguments into standard Acedia collection. It also allows to |
||||||
|
* manage them (and specify limitation on how they can be called) in a |
||||||
|
* centralized manner. |
||||||
|
* Copyright 2021-2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Commands_Feature extends Feature; |
||||||
|
|
||||||
|
/** |
||||||
|
* # `Commands_Feature` |
||||||
|
* |
||||||
|
* This feature provides a mechanism to define commands that automatically |
||||||
|
* parse their arguments into standard Acedia collection. It also allows to |
||||||
|
* manage them (and specify limitation on how they can be called) in a |
||||||
|
* centralized manner. |
||||||
|
* Support command input from chat and "mutate" command. |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* Should be enabled like any other feature. Additionally support |
||||||
|
* `EmergencyEnable()` enabling method that bypasses regular settings to allow |
||||||
|
* admins to start this feature while forcefully enabling "mutate" command |
||||||
|
* input method. |
||||||
|
* Available configuration: |
||||||
|
* |
||||||
|
* 1. Whether to use command input from chat and what prefix is used to |
||||||
|
* denote a command (by default "!"); |
||||||
|
* 2. Whether to use command input from "mutate" command. |
||||||
|
* |
||||||
|
* To add new commands into the system - get enabled instance of this |
||||||
|
* feature and call its `RegisterCommand()` method to add your custom |
||||||
|
* `Command` class. `RemoveCommand()` can also be used to de-register |
||||||
|
* a command, if you need this for some reason. |
||||||
|
* |
||||||
|
* ## Implementation |
||||||
|
* |
||||||
|
* Implementation is simple: calling a method `RegisterCommand()` adds |
||||||
|
* command into two caches `registeredCommands` for obtaining registered |
||||||
|
* commands by name and `groupedCommands` for obtaining arrays of commands by |
||||||
|
* their group name. These arrays are used for providing methods for fetching |
||||||
|
* arrays of commands and obtaining pre-allocated `Command` instances by their |
||||||
|
* name. |
||||||
|
* Depending on settings, this feature also connects to corresponding |
||||||
|
* signals for catching "mutate"/chat input, then it checks user-specified name |
||||||
|
* for being an alias and picks correct command from `registeredCommands`. |
||||||
|
* Emergency enabling this feature sets `emergencyEnabledMutate` flag that |
||||||
|
* enforces connecting to the "mutate" input. |
||||||
|
*/ |
||||||
|
|
||||||
|
// Delimiters that always separate command name from it's parameters |
||||||
|
var private array<Text> commandDelimiters; |
||||||
|
// Registered commands, recorded as (<command_name>, <command_instance>) pairs. |
||||||
|
// Keys should be deallocated when their entry is removed. |
||||||
|
var private HashTable registeredCommands; |
||||||
|
// `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs |
||||||
|
// to allow quick fetch of commands belonging to a single group |
||||||
|
var private HashTable groupedCommands; |
||||||
|
|
||||||
|
// When this flag is set to true, mutate input becomes available |
||||||
|
// despite `useMutateInput` flag to allow to unlock server in case of an error |
||||||
|
var private bool emergencyEnabledMutate; |
||||||
|
|
||||||
|
// Setting this to `true` enables players to input commands right in the chat |
||||||
|
// by prepending them with `chatCommandPrefix`. |
||||||
|
// Default is `true`. |
||||||
|
var private /*config*/ bool useChatInput; |
||||||
|
// Setting this to `true` enables players to input commands with "mutate" |
||||||
|
// console command. |
||||||
|
// Default is `true`. |
||||||
|
var private /*config*/ bool useMutateInput; |
||||||
|
// Chat messages, prepended by this prefix will be treated as commands. |
||||||
|
// Default is "!". Empty values are also treated as "!". |
||||||
|
var private /*config*/ Text chatCommandPrefix; |
||||||
|
// List of steam IDs of players allowed to use commands. |
||||||
|
// Temporary measure until a better solution is finished. |
||||||
|
var private /*config*/ array<string> allowedPlayers; |
||||||
|
|
||||||
|
// Contains name of the command to call plus, optionally, |
||||||
|
// additional sub-command name. |
||||||
|
// Normally sub-command name is parsed by the command itself, however |
||||||
|
// command aliases can try to enforce one. |
||||||
|
struct CommandCallPair |
||||||
|
{ |
||||||
|
var MutableText commandName; |
||||||
|
// In case it is enforced by an alias |
||||||
|
var MutableText subCommandName; |
||||||
|
}; |
||||||
|
|
||||||
|
var LoggerAPI.Definition errCommandDuplicate; |
||||||
|
|
||||||
|
protected function OnEnabled() |
||||||
|
{ |
||||||
|
registeredCommands = _.collections.EmptyHashTable(); |
||||||
|
groupedCommands = _.collections.EmptyHashTable(); |
||||||
|
RegisterCommand(class'ACommandHelp'); |
||||||
|
// Macro selector |
||||||
|
commandDelimiters[0] = _.text.FromString("@"); |
||||||
|
// Key selector |
||||||
|
commandDelimiters[1] = _.text.FromString("#"); |
||||||
|
// Player array (possibly JSON array) |
||||||
|
commandDelimiters[2] = _.text.FromString("["); |
||||||
|
// Negation of the selector |
||||||
|
commandDelimiters[3] = _.text.FromString("!"); |
||||||
|
} |
||||||
|
|
||||||
|
protected function OnDisabled() |
||||||
|
{ |
||||||
|
if (useChatInput) { |
||||||
|
_.chat.OnMessage(self).Disconnect(); |
||||||
|
} |
||||||
|
if (useMutateInput) { |
||||||
|
_server.unreal.mutator.OnMutate(self).Disconnect(); |
||||||
|
} |
||||||
|
useChatInput = false; |
||||||
|
useMutateInput = false; |
||||||
|
_.memory.Free(registeredCommands); |
||||||
|
_.memory.Free(groupedCommands); |
||||||
|
_.memory.Free(chatCommandPrefix); |
||||||
|
_.memory.FreeMany(commandDelimiters); |
||||||
|
registeredCommands = none; |
||||||
|
groupedCommands = none; |
||||||
|
chatCommandPrefix = none; |
||||||
|
commandDelimiters.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
protected function SwapConfig(FeatureConfig config) |
||||||
|
{ |
||||||
|
local Commands newConfig; |
||||||
|
|
||||||
|
newConfig = Commands(config); |
||||||
|
if (newConfig == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_.memory.Free(chatCommandPrefix); |
||||||
|
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix); |
||||||
|
allowedPlayers = newConfig.allowedPlayers; |
||||||
|
if (useChatInput != newConfig.useChatInput) |
||||||
|
{ |
||||||
|
useChatInput = newConfig.useChatInput; |
||||||
|
if (newConfig.useChatInput) { |
||||||
|
_.chat.OnMessage(self).connect = HandleCommands; |
||||||
|
} |
||||||
|
else { |
||||||
|
_.chat.OnMessage(self).Disconnect(); |
||||||
|
} |
||||||
|
} |
||||||
|
// Do not make any modifications here in case "mutate" was |
||||||
|
// emergency-enabled |
||||||
|
if (useMutateInput != newConfig.useMutateInput && !emergencyEnabledMutate) |
||||||
|
{ |
||||||
|
useMutateInput = newConfig.useMutateInput; |
||||||
|
if (newConfig.useMutateInput) { |
||||||
|
_server.unreal.mutator.OnMutate(self).connect = HandleMutate; |
||||||
|
} |
||||||
|
else { |
||||||
|
_server.unreal.mutator.OnMutate(self).Disconnect(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* `Command_Feature` is a critical command to have running on your server and, |
||||||
|
* if disabled by accident, there will be no way of starting it again without |
||||||
|
* restarting the level or even editing configs. |
||||||
|
* |
||||||
|
* This method allows to enable it along with "mutate" input in case something |
||||||
|
* goes wrong. |
||||||
|
*/ |
||||||
|
public final static function EmergencyEnable() |
||||||
|
{ |
||||||
|
local Text autoConfig; |
||||||
|
local Commands_Feature feature; |
||||||
|
|
||||||
|
if (!IsEnabled()) |
||||||
|
{ |
||||||
|
autoConfig = GetAutoEnabledConfig(); |
||||||
|
EnableMe(autoConfig); |
||||||
|
__().memory.Free(autoConfig); |
||||||
|
} |
||||||
|
feature = Commands_Feature(GetEnabledInstance()); |
||||||
|
if ( !feature.emergencyEnabledMutate |
||||||
|
&& !feature.IsUsingMutateInput() && !feature.IsUsingChatInput()) |
||||||
|
{ |
||||||
|
default.emergencyEnabledMutate = true; |
||||||
|
feature.emergencyEnabledMutate = true; |
||||||
|
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if `Commands_Feature` currently uses chat as input. |
||||||
|
* If `Commands_Feature` is not enabled, then it does not use anything |
||||||
|
* as input. |
||||||
|
* |
||||||
|
* @return `true` if `Commands_Feature` is currently enabled and is using chat |
||||||
|
* as input and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final static function bool IsUsingChatInput() |
||||||
|
{ |
||||||
|
local Commands_Feature instance; |
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance()); |
||||||
|
if (instance != none) { |
||||||
|
return instance.useChatInput; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if `Commands_Feature` currently uses mutate command as input. |
||||||
|
* If `Commands_Feature` is not enabled, then it does not use anything |
||||||
|
* as input. |
||||||
|
* |
||||||
|
* @return `true` if `Commands_Feature` is currently enabled and is using |
||||||
|
* mutate command as input and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final static function bool IsUsingMutateInput() |
||||||
|
{ |
||||||
|
local Commands_Feature instance; |
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance()); |
||||||
|
if (instance != none) { |
||||||
|
return instance.useMutateInput; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns prefix that will indicate that chat message is intended to be |
||||||
|
* a command. By default "!". |
||||||
|
* |
||||||
|
* @return Prefix that indicates that chat message is intended to be a command. |
||||||
|
* If `Commands_Feature` is disabled, always returns `false`. |
||||||
|
*/ |
||||||
|
public final static function Text GetChatPrefix() |
||||||
|
{ |
||||||
|
local Commands_Feature instance; |
||||||
|
|
||||||
|
instance = Commands_Feature(GetEnabledInstance()); |
||||||
|
if (instance != none && instance.chatCommandPrefix != none) { |
||||||
|
return instance.chatCommandPrefix.Copy(); |
||||||
|
} |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers given command class, making it available for usage. |
||||||
|
* |
||||||
|
* If `commandClass` provides command with a name that is already taken |
||||||
|
* (comparison is case-insensitive) by a different command - a warning will be |
||||||
|
* logged and newly passed `commandClass` discarded. |
||||||
|
* |
||||||
|
* @param commandClass New command class to register. |
||||||
|
*/ |
||||||
|
public final function RegisterCommand(class<Command> commandClass) |
||||||
|
{ |
||||||
|
local Text commandName, groupName; |
||||||
|
local ArrayList groupArray; |
||||||
|
local Command newCommandInstance, existingCommandInstance; |
||||||
|
|
||||||
|
if (commandClass == none) return; |
||||||
|
if (registeredCommands == none) return; |
||||||
|
|
||||||
|
newCommandInstance = Command(_.memory.Allocate(commandClass, true)); |
||||||
|
commandName = newCommandInstance.GetName(); |
||||||
|
groupName = newCommandInstance.GetGroupName(); |
||||||
|
// Check for duplicates and report them |
||||||
|
existingCommandInstance = Command(registeredCommands.GetItem(commandName)); |
||||||
|
if (existingCommandInstance != none) |
||||||
|
{ |
||||||
|
_.logger.Auto(errCommandDuplicate) |
||||||
|
.ArgClass(existingCommandInstance.class) |
||||||
|
.Arg(commandName) |
||||||
|
.ArgClass(commandClass); |
||||||
|
_.memory.Free(groupName); |
||||||
|
_.memory.Free(newCommandInstance); |
||||||
|
_.memory.Free(existingCommandInstance); |
||||||
|
return; |
||||||
|
} |
||||||
|
// Otherwise record new command |
||||||
|
// `commandName` used as a key, do not deallocate it |
||||||
|
registeredCommands.SetItem(commandName, newCommandInstance); |
||||||
|
// Add to grouped collection |
||||||
|
groupArray = groupedCommands.GetArrayList(groupName); |
||||||
|
if (groupArray == none) { |
||||||
|
groupArray = _.collections.EmptyArrayList(); |
||||||
|
} |
||||||
|
groupArray.AddItem(newCommandInstance); |
||||||
|
groupedCommands.SetItem(groupName, groupArray); |
||||||
|
_.memory.Free(groupArray); |
||||||
|
_.memory.Free(groupName); |
||||||
|
_.memory.Free(commandName); |
||||||
|
_.memory.Free(newCommandInstance); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes command of class `commandClass` from the list of |
||||||
|
* registered commands. |
||||||
|
* |
||||||
|
* WARNING: removing once registered commands is not an action that is expected |
||||||
|
* to be performed under normal circumstances and it is not efficient. |
||||||
|
* It is linear on the current amount of commands. |
||||||
|
* |
||||||
|
* @param commandClass Class of command to remove from being registered. |
||||||
|
*/ |
||||||
|
public final function RemoveCommand(class<Command> commandClass) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local CollectionIterator iter; |
||||||
|
local Command nextCommand; |
||||||
|
local Text nextCommandName; |
||||||
|
local array<Text> commandGroup; |
||||||
|
local array<Text> keysToRemove; |
||||||
|
|
||||||
|
if (commandClass == none) return; |
||||||
|
if (registeredCommands == none) return; |
||||||
|
|
||||||
|
for (iter = registeredCommands.Iterate(); !iter.HasFinished(); iter.Next()) |
||||||
|
{ |
||||||
|
nextCommand = Command(iter.Get()); |
||||||
|
nextCommandName = Text(iter.GetKey()); |
||||||
|
if ( nextCommand == none || nextCommandName == none |
||||||
|
|| nextCommand.class != commandClass) |
||||||
|
{ |
||||||
|
_.memory.Free(nextCommand); |
||||||
|
_.memory.Free(nextCommandName); |
||||||
|
continue; |
||||||
|
} |
||||||
|
keysToRemove[keysToRemove.length] = nextCommandName; |
||||||
|
commandGroup[commandGroup.length] = nextCommand.GetGroupName(); |
||||||
|
_.memory.Free(nextCommand); |
||||||
|
} |
||||||
|
iter.FreeSelf(); |
||||||
|
for (i = 0; i < keysToRemove.length; i += 1) |
||||||
|
{ |
||||||
|
registeredCommands.RemoveItem(keysToRemove[i]); |
||||||
|
_.memory.Free(keysToRemove[i]); |
||||||
|
} |
||||||
|
|
||||||
|
for (i = 0; i < commandGroup.length; i += 1) { |
||||||
|
RemoveClassFromGroup(commandClass, commandGroup[i]); |
||||||
|
} |
||||||
|
_.memory.FreeMany(commandGroup); |
||||||
|
} |
||||||
|
|
||||||
|
private final function RemoveClassFromGroup( |
||||||
|
class<Command> commandClass, |
||||||
|
BaseText commandGroup) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local ArrayList groupArray; |
||||||
|
local Command nextCommand; |
||||||
|
|
||||||
|
groupArray = groupedCommands.GetArrayList(commandGroup); |
||||||
|
if (groupArray == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
while (i < groupArray.GetLength()) |
||||||
|
{ |
||||||
|
nextCommand = Command(groupArray.GetItem(i)); |
||||||
|
if (nextCommand != none && nextCommand.class == commandClass) { |
||||||
|
groupArray.RemoveIndex(i); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
_.memory.Free(nextCommand); |
||||||
|
} |
||||||
|
if (groupArray.GetLength() == 0) { |
||||||
|
groupedCommands.RemoveItem(commandGroup); |
||||||
|
} |
||||||
|
_.memory.Free(groupArray); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns command based on a given name. |
||||||
|
* |
||||||
|
* @param commandName Name of the registered `Command` to return. |
||||||
|
* Case-insensitive. |
||||||
|
* @return Command, registered with a given name `commandName`. |
||||||
|
* If no command with such name was registered - returns `none`. |
||||||
|
*/ |
||||||
|
public final function Command GetCommand(BaseText commandName) |
||||||
|
{ |
||||||
|
local Text commandNameLowerCase; |
||||||
|
local Command commandInstance; |
||||||
|
|
||||||
|
if (commandName == none) return none; |
||||||
|
if (registeredCommands == none) return none; |
||||||
|
|
||||||
|
commandNameLowerCase = commandName.LowerCopy(); |
||||||
|
commandInstance = Command(registeredCommands.GetItem(commandNameLowerCase)); |
||||||
|
commandNameLowerCase.FreeSelf(); |
||||||
|
return commandInstance; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns array of names of all available commands. |
||||||
|
* |
||||||
|
* @return Array of names of all available (registered) commands. |
||||||
|
*/ |
||||||
|
public final function array<Text> GetCommandNames() |
||||||
|
{ |
||||||
|
local array<Text> emptyResult; |
||||||
|
|
||||||
|
if (registeredCommands != none) { |
||||||
|
return registeredCommands.GetTextKeys(); |
||||||
|
} |
||||||
|
return emptyResult; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns array of names of all available commands belonging to the group |
||||||
|
* `groupName`. |
||||||
|
* |
||||||
|
* @return Array of names of all available (registered) commands, belonging to |
||||||
|
* the group `groupName`. |
||||||
|
*/ |
||||||
|
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local ArrayList groupArray; |
||||||
|
local Command nextCommand; |
||||||
|
local array<Text> result; |
||||||
|
|
||||||
|
if (groupedCommands == none) return result; |
||||||
|
groupArray = groupedCommands.GetArrayList(groupName); |
||||||
|
if (groupArray == none) return result; |
||||||
|
|
||||||
|
for (i = 0; i < groupArray.GetLength(); i += 1) |
||||||
|
{ |
||||||
|
nextCommand = Command(groupArray.GetItem(i)); |
||||||
|
if (nextCommand != none) { |
||||||
|
result[result.length] = nextCommand.GetName(); |
||||||
|
} |
||||||
|
_.memory.Free(nextCommand); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns all available command groups' names. |
||||||
|
* |
||||||
|
* @return Array of all available command groups' names. |
||||||
|
*/ |
||||||
|
public final function array<Text> GetGroupsNames() |
||||||
|
{ |
||||||
|
local array<Text> emptyResult; |
||||||
|
|
||||||
|
if (groupedCommands != none) { |
||||||
|
return groupedCommands.GetTextKeys(); |
||||||
|
} |
||||||
|
return emptyResult; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Handles user input: finds appropriate command and passes the rest of |
||||||
|
* the arguments to it for further processing. |
||||||
|
* |
||||||
|
* @param input Test that contains user's command input. |
||||||
|
* @param callerPlayer Player that caused this command call. |
||||||
|
*/ |
||||||
|
public final function HandleInput(BaseText input, EPlayer callerPlayer) |
||||||
|
{ |
||||||
|
local Parser wrapper; |
||||||
|
|
||||||
|
if (input == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
wrapper = input.Parse(); |
||||||
|
HandleInputWith(wrapper, callerPlayer); |
||||||
|
wrapper.FreeSelf(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Handles user input: finds appropriate command and passes the rest of |
||||||
|
* the arguments to it for further processing. |
||||||
|
* |
||||||
|
* @param parser Parser filled with user input that is expected to |
||||||
|
* contain command's name and it's parameters. |
||||||
|
* @param callerPlayer Player that caused this command call. |
||||||
|
*/ |
||||||
|
public final function HandleInputWith(Parser parser, EPlayer callerPlayer) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local bool foundID; |
||||||
|
local string steamID; |
||||||
|
local PlayerController controller; |
||||||
|
local Command commandInstance; |
||||||
|
local Command.CallData callData; |
||||||
|
local CommandCallPair callPair; |
||||||
|
|
||||||
|
if (parser == none) return; |
||||||
|
if (callerPlayer == none) return; |
||||||
|
if (!parser.Ok()) return; |
||||||
|
controller = callerPlayer.GetController(); |
||||||
|
if (controller == none) return; |
||||||
|
|
||||||
|
steamID = controller.GetPlayerIDHash(); |
||||||
|
for (i = 0; i < allowedPlayers.length; i += 1) |
||||||
|
{ |
||||||
|
if (allowedPlayers[i] == steamID) |
||||||
|
{ |
||||||
|
foundID = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!foundID) { |
||||||
|
return; |
||||||
|
} |
||||||
|
callPair = ParseCommandCallPairWith(parser); |
||||||
|
commandInstance = GetCommand(callPair.commandName); |
||||||
|
if ( commandInstance == none |
||||||
|
&& callerPlayer != none && callerPlayer.IsExistent()) |
||||||
|
{ |
||||||
|
callerPlayer |
||||||
|
.BorrowConsole() |
||||||
|
.Flush() |
||||||
|
.Say(F("{$TextFailure Command not found!}")); |
||||||
|
} |
||||||
|
if (parser.Ok() && commandInstance != none) |
||||||
|
{ |
||||||
|
callData = commandInstance |
||||||
|
.ParseInputWith(parser, callerPlayer, callPair.subCommandName); |
||||||
|
commandInstance.Execute(callData, callerPlayer); |
||||||
|
commandInstance.DeallocateCallData(callData); |
||||||
|
} |
||||||
|
_.memory.Free(callPair.commandName); |
||||||
|
_.memory.Free(callPair.subCommandName); |
||||||
|
} |
||||||
|
|
||||||
|
// Parses command's name into `CommandCallPair` - sub-command is filled in case |
||||||
|
// specified name is an alias with specified sub-command name. |
||||||
|
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) |
||||||
|
{ |
||||||
|
local Text resolvedValue; |
||||||
|
local MutableText userSpecifiedName; |
||||||
|
local CommandCallPair result; |
||||||
|
local Text.Character dotCharacter; |
||||||
|
|
||||||
|
if (parser == none) return result; |
||||||
|
if (!parser.Ok()) return result; |
||||||
|
|
||||||
|
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true); |
||||||
|
resolvedValue = _.alias.ResolveCommand(userSpecifiedName); |
||||||
|
// This isn't an alias |
||||||
|
if (resolvedValue == none) |
||||||
|
{ |
||||||
|
result.commandName = userSpecifiedName; |
||||||
|
return result; |
||||||
|
} |
||||||
|
// It is an alias - parse it |
||||||
|
dotCharacter = _.text.GetCharacter("."); |
||||||
|
resolvedValue.Parse() |
||||||
|
.MUntil(result.commandName, dotCharacter) |
||||||
|
.MatchS(".") |
||||||
|
.MUntil(result.subCommandName, dotCharacter) |
||||||
|
.FreeSelf(); |
||||||
|
if (result.subCommandName.IsEmpty()) |
||||||
|
{ |
||||||
|
result.subCommandName.FreeSelf(); |
||||||
|
result.subCommandName = none; |
||||||
|
} |
||||||
|
resolvedValue.FreeSelf(); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private function bool HandleCommands( |
||||||
|
EPlayer sender, |
||||||
|
MutableText message, |
||||||
|
bool teamMessage) |
||||||
|
{ |
||||||
|
local Parser parser; |
||||||
|
|
||||||
|
// We are only interested in messages that start with `chatCommandPrefix` |
||||||
|
parser = _.text.Parse(message); |
||||||
|
if (!parser.Match(chatCommandPrefix).Ok()) |
||||||
|
{ |
||||||
|
parser.FreeSelf(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
// Pass input to command feature |
||||||
|
HandleInputWith(parser, sender); |
||||||
|
parser.FreeSelf(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private function HandleMutate(string command, PlayerController sendingPlayer) |
||||||
|
{ |
||||||
|
local Parser parser; |
||||||
|
local EPlayer sender; |
||||||
|
|
||||||
|
// A lot of other mutators use these commands |
||||||
|
if (command ~= "help") return; |
||||||
|
if (command ~= "version") return; |
||||||
|
if (command ~= "status") return; |
||||||
|
if (command ~= "credits") return; |
||||||
|
|
||||||
|
parser = _.text.ParseString(command); |
||||||
|
sender = _.players.FromController(sendingPlayer); |
||||||
|
HandleInputWith(parser, sender); |
||||||
|
sender.FreeSelf(); |
||||||
|
parser.FreeSelf(); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
configClass = class'Commands' |
||||||
|
errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.") |
||||||
|
} |
@ -0,0 +1,559 @@ |
|||||||
|
/** |
||||||
|
* Object for parsing what converting textual description of a group of |
||||||
|
* players into array of `EPlayer`s. Depends on the game context. |
||||||
|
* Copyright 2021-2022 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class PlayersParser extends AcediaObject |
||||||
|
dependson(Parser); |
||||||
|
|
||||||
|
/** |
||||||
|
* # `PlayersParser` |
||||||
|
* |
||||||
|
* This parser is supposed to parse player set definitions as they |
||||||
|
* are used in commands. |
||||||
|
* Basic use is to specify one of the selectors: |
||||||
|
* 1. Key selector: "#<integer>" (examples: "#1", "#5"). |
||||||
|
* This one is used to specify players by their key, assigned to |
||||||
|
* them when they enter the game. This type of selectors can be used |
||||||
|
* when players have hard to type names. |
||||||
|
* 2. Macro selector: "@self", "@me", "@all", "@admin" or just "@". |
||||||
|
* "@", "@me", and "@self" are identical and can be used to |
||||||
|
* specify player that called the command. |
||||||
|
* "@admin" can be used to specify all admins in the game at once. |
||||||
|
* "@all" specifies all current players. |
||||||
|
* In future it is planned to make macros extendable by allowing to |
||||||
|
* bind more names to specific groups of players. |
||||||
|
* 3. Name selectors: quoted strings and any other types of string that |
||||||
|
* do not start with either "#" or "@". |
||||||
|
* These specify name prefixes: any player with specified prefix |
||||||
|
* will be considered to match such selector. |
||||||
|
* |
||||||
|
* Negated selectors: "!<selector>". Specifying "!" in front of selector |
||||||
|
* will select all players that do not match it instead. |
||||||
|
* |
||||||
|
* Grouped selectors: "['<selector1>', '<selector2>', ... '<selectorN>']". |
||||||
|
* Specified selectors are process in order: from left to right. |
||||||
|
* First selector works as usual and selects a set of players. |
||||||
|
* All the following selectors either |
||||||
|
* expand that list (additive ones, without "!" prefix) |
||||||
|
* or remove specific players from the list (the ones with "!" prefix). |
||||||
|
* Examples of that: |
||||||
|
* *. "[@admin, !@self]" - selects all admins, except the one who called |
||||||
|
* the command (whether he is admin or not). |
||||||
|
* *. "[dkanus, 'mate']" - will select players "dkanus" and "mate". |
||||||
|
* Order also matters, since: |
||||||
|
* *. "[@admin, !@admin]" - won't select anyone, since it will first |
||||||
|
* add all the admins and then remove them. |
||||||
|
* *. "[!@admin, @admin]" - will select everyone, since it will first |
||||||
|
* select everyone who is not an admin and then adds everyone else. |
||||||
|
* |
||||||
|
* ## Usage |
||||||
|
* |
||||||
|
* 1. Allocate `PlayerParser`; |
||||||
|
* 2. Set caller player through `SetSelf()` method to make "@" and "@me" |
||||||
|
* selectors usable; |
||||||
|
* 3. Call `Parse()` or `ParseWith()` method with `BaseText`/`Parser` that |
||||||
|
* starts with proper players selector; |
||||||
|
* 4. Call `GetPlayers()` to obtain selected players array. |
||||||
|
* |
||||||
|
* ## Implementation |
||||||
|
* |
||||||
|
* When created, `PlayersParser` takes a snapshot (array) of current |
||||||
|
* players on the server. Then `currentSelection` is decided based on whether |
||||||
|
* first selector is positive (initial selection is taken as empty array) or |
||||||
|
* negative (initial selection is taken as full snapshot). |
||||||
|
* After that `PlayersParser` simply goes through specified selectors |
||||||
|
* (in case more than one is specified) and adds or removes appropriate players |
||||||
|
* in `currentSelection`, assuming that `playersSnapshot` is a current full |
||||||
|
* array of players. |
||||||
|
*/ |
||||||
|
|
||||||
|
// Player for which "@", "@me", and "@self" macros will refer |
||||||
|
var private EPlayer selfPlayer; |
||||||
|
// Copy of the list of current players at the moment of allocation of |
||||||
|
// this `PlayersParser`. |
||||||
|
var private array<EPlayer> playersSnapshot; |
||||||
|
// Players, selected according to selectors we have parsed so far |
||||||
|
var private array<EPlayer> currentSelection; |
||||||
|
// Have we parsed our first selector? |
||||||
|
// We need this to know whether to start with the list of |
||||||
|
// all players (if first selector removes them) or |
||||||
|
// with empty list (if first selector adds them). |
||||||
|
var private bool parsedFirstSelector; |
||||||
|
// Will be equal to a single-element array [","], used for parsing |
||||||
|
var private array<Text> selectorDelimiters; |
||||||
|
|
||||||
|
var const int TSELF, TME, TADMIN, TALL, TNOT, TKEY, TMACRO, TCOMMA; |
||||||
|
var const int TOPEN_BRACKET, TCLOSE_BRACKET; |
||||||
|
|
||||||
|
protected function Finalizer() |
||||||
|
{ |
||||||
|
// No need to deallocate `currentSelection`, |
||||||
|
// since it has `EPlayer`s from `playersSnapshot` or `selfPlayer` |
||||||
|
_.memory.Free(selfPlayer); |
||||||
|
_.memory.FreeMany(playersSnapshot); |
||||||
|
selfPlayer = none; |
||||||
|
parsedFirstSelector = false; |
||||||
|
playersSnapshot.length = 0; |
||||||
|
currentSelection.length = 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Set a player who will be referred to by "@", "@me" and "@self" macros. |
||||||
|
* |
||||||
|
* @param newSelfPlayer Player who will be referred to by "@", "@me" and |
||||||
|
* "@self" macros. Passing `none` will make it so no one is |
||||||
|
* referred by them. |
||||||
|
*/ |
||||||
|
public final function SetSelf(EPlayer newSelfPlayer) |
||||||
|
{ |
||||||
|
_.memory.Free(selfPlayer); |
||||||
|
selfPlayer = none; |
||||||
|
if (newSelfPlayer != none) { |
||||||
|
selfPlayer = EPlayer(newSelfPlayer.Copy()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Insert a new player into currently selected list of players |
||||||
|
// (`currentSelection`) such that there will be no duplicates. |
||||||
|
// `none` values are auto-discarded. |
||||||
|
private final function InsertPlayer(EPlayer toInsert) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (toInsert == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < currentSelection.length; i += 1) |
||||||
|
{ |
||||||
|
if (currentSelection[i] == toInsert) { |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
currentSelection[currentSelection.length] = toInsert; |
||||||
|
} |
||||||
|
|
||||||
|
// Adds all the players with specified key (`key`) to the current selection. |
||||||
|
private final function AddByKey(int key) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
for (i = 0; i < playersSnapshot.length; i += 1) |
||||||
|
{ |
||||||
|
if (playersSnapshot[i].GetIdentity().GetKey() == key) { |
||||||
|
InsertPlayer(playersSnapshot[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes all the players with specified key (`key`) from |
||||||
|
// the current selection. |
||||||
|
private final function RemoveByKey(int key) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
while (i < currentSelection.length) |
||||||
|
{ |
||||||
|
if (currentSelection[i].GetIdentity().GetKey() == key) { |
||||||
|
currentSelection.Remove(i, 1); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Adds all the players with specified name (`name`) to the current selection. |
||||||
|
private final function AddByName(BaseText name) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local Text nextPlayerName; |
||||||
|
|
||||||
|
if (name == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < playersSnapshot.length; i += 1) |
||||||
|
{ |
||||||
|
nextPlayerName = playersSnapshot[i].GetName(); |
||||||
|
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||||
|
InsertPlayer(playersSnapshot[i]); |
||||||
|
} |
||||||
|
nextPlayerName.FreeSelf(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes all the players with specified name (`name`) from |
||||||
|
// the current selection. |
||||||
|
private final function RemoveByName(BaseText name) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local Text nextPlayerName; |
||||||
|
|
||||||
|
while (i < currentSelection.length) |
||||||
|
{ |
||||||
|
nextPlayerName = currentSelection[i].GetName(); |
||||||
|
if (nextPlayerName.StartsWith(name, SCASE_INSENSITIVE)) { |
||||||
|
currentSelection.Remove(i, 1); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
nextPlayerName.FreeSelf(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Adds all the admins to the current selection. |
||||||
|
private final function AddAdmins() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
for (i = 0; i < playersSnapshot.length; i += 1) |
||||||
|
{ |
||||||
|
if (playersSnapshot[i].IsAdmin()) { |
||||||
|
InsertPlayer(playersSnapshot[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes all the admins from the current selection. |
||||||
|
private final function RemoveAdmins() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
while (i < currentSelection.length) |
||||||
|
{ |
||||||
|
if (currentSelection[i].IsAdmin()) { |
||||||
|
currentSelection.Remove(i, 1); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Add all the players specified by `macroText` (from macro "@<macroText>"). |
||||||
|
// Does nothing if there is no such macro. |
||||||
|
private final function AddByMacro(BaseText macroText) |
||||||
|
{ |
||||||
|
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
AddAdmins(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
currentSelection = playersSnapshot; |
||||||
|
return; |
||||||
|
} |
||||||
|
if ( macroText.IsEmpty() |
||||||
|
|| macroText.Compare(T(TSELF), SCASE_INSENSITIVE) |
||||||
|
|| macroText.Compare(T(TME), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
InsertPlayer(selfPlayer); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Removes all the players specified by `macroText` |
||||||
|
// (from macro "@<macroText>"). |
||||||
|
// Does nothing if there is no such macro. |
||||||
|
private final function RemoveByMacro(BaseText macroText) |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
|
||||||
|
if (macroText.Compare(T(TADMIN), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
RemoveAdmins(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (macroText.Compare(T(TALL), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
currentSelection.length = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
if (macroText.IsEmpty() || macroText.Compare(T(TSELF), SCASE_INSENSITIVE)) |
||||||
|
{ |
||||||
|
while (i < currentSelection.length) |
||||||
|
{ |
||||||
|
if (currentSelection[i] == selfPlayer) { |
||||||
|
currentSelection.Remove(i, 1); |
||||||
|
} |
||||||
|
else { |
||||||
|
i += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Parses one selector from `parser`, while accordingly modifying current |
||||||
|
// player selection list. |
||||||
|
private final function ParseSelector(Parser parser) |
||||||
|
{ |
||||||
|
local bool additiveSelector; |
||||||
|
local Parser.ParserState confirmedState; |
||||||
|
|
||||||
|
if (parser == none) return; |
||||||
|
if (!parser.Ok()) return; |
||||||
|
|
||||||
|
confirmedState = parser.GetCurrentState(); |
||||||
|
if (!parser.Match(T(TNOT)).Ok()) |
||||||
|
{ |
||||||
|
additiveSelector = true; |
||||||
|
parser.RestoreState(confirmedState); |
||||||
|
} |
||||||
|
// Determine whether we stars with empty or full player list |
||||||
|
if (!parsedFirstSelector) |
||||||
|
{ |
||||||
|
parsedFirstSelector = true; |
||||||
|
if (additiveSelector) { |
||||||
|
currentSelection.length = 0; |
||||||
|
} |
||||||
|
else { |
||||||
|
currentSelection = playersSnapshot; |
||||||
|
} |
||||||
|
} |
||||||
|
// Try all selector types |
||||||
|
confirmedState = parser.GetCurrentState(); |
||||||
|
if (parser.Match(T(TKEY)).Ok()) |
||||||
|
{ |
||||||
|
ParseKeySelector(parser, additiveSelector); |
||||||
|
return; |
||||||
|
} |
||||||
|
parser.RestoreState(confirmedState); |
||||||
|
if (parser.Match(T(TMACRO)).Ok()) |
||||||
|
{ |
||||||
|
ParseMacroSelector(parser, additiveSelector); |
||||||
|
return; |
||||||
|
} |
||||||
|
parser.RestoreState(confirmedState); |
||||||
|
ParseNameSelector(parser, additiveSelector); |
||||||
|
} |
||||||
|
|
||||||
|
// Parse key selector (assuming "#" is already consumed), while accordingly |
||||||
|
// modifying current player selection list. |
||||||
|
private final function ParseKeySelector(Parser parser, bool additiveSelector) |
||||||
|
{ |
||||||
|
local int key; |
||||||
|
|
||||||
|
if (parser == none) return; |
||||||
|
if (!parser.Ok()) return; |
||||||
|
if (!parser.MInteger(key).Ok()) return; |
||||||
|
|
||||||
|
if (additiveSelector) { |
||||||
|
AddByKey(key); |
||||||
|
} |
||||||
|
else { |
||||||
|
RemoveByKey(key); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Parse macro selector (assuming "@" is already consumed), while accordingly |
||||||
|
// modifying current player selection list. |
||||||
|
private final function ParseMacroSelector(Parser parser, bool additiveSelector) |
||||||
|
{ |
||||||
|
local MutableText macroName; |
||||||
|
local Parser.ParserState confirmedState; |
||||||
|
|
||||||
|
if (parser == none) return; |
||||||
|
if (!parser.Ok()) return; |
||||||
|
|
||||||
|
confirmedState = parser.GetCurrentState(); |
||||||
|
macroName = ParseLiteral(parser); |
||||||
|
if (!parser.Ok()) |
||||||
|
{ |
||||||
|
_.memory.Free(macroName); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (additiveSelector) { |
||||||
|
AddByMacro(macroName); |
||||||
|
} |
||||||
|
else { |
||||||
|
RemoveByMacro(macroName); |
||||||
|
} |
||||||
|
_.memory.Free(macroName); |
||||||
|
} |
||||||
|
|
||||||
|
// Parse name selector, while accordingly modifying current player |
||||||
|
// selection list. |
||||||
|
private final function ParseNameSelector(Parser parser, bool additiveSelector) |
||||||
|
{ |
||||||
|
local MutableText playerName; |
||||||
|
local Parser.ParserState confirmedState; |
||||||
|
|
||||||
|
if (parser == none) return; |
||||||
|
if (!parser.Ok()) return; |
||||||
|
|
||||||
|
confirmedState = parser.GetCurrentState(); |
||||||
|
playerName = ParseLiteral(parser); |
||||||
|
if (!parser.Ok() || playerName.IsEmpty()) |
||||||
|
{ |
||||||
|
_.memory.Free(playerName); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (additiveSelector) { |
||||||
|
AddByName(playerName); |
||||||
|
} |
||||||
|
else { |
||||||
|
RemoveByName(playerName); |
||||||
|
} |
||||||
|
_.memory.Free(playerName); |
||||||
|
} |
||||||
|
|
||||||
|
// Reads a string that can either be a body of name selector |
||||||
|
// (some player's name prefix) or of a macro selector (what comes after "@"). |
||||||
|
// This is different from `parser.MString()` because it also uses |
||||||
|
// "," as a separator. |
||||||
|
private final function MutableText ParseLiteral(Parser parser) |
||||||
|
{ |
||||||
|
local MutableText literal; |
||||||
|
local Parser.ParserState confirmedState; |
||||||
|
|
||||||
|
if (parser == none) return none; |
||||||
|
if (!parser.Ok()) return none; |
||||||
|
|
||||||
|
confirmedState = parser.GetCurrentState(); |
||||||
|
if (!parser.MStringLiteral(literal).Ok()) |
||||||
|
{ |
||||||
|
parser.RestoreState(confirmedState); |
||||||
|
parser.MUntilMany(literal, selectorDelimiters, true); |
||||||
|
} |
||||||
|
return literal; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns players parsed by the last `ParseWith()` or `Parse()` call. |
||||||
|
* If neither were yet called - returns an empty array. |
||||||
|
* |
||||||
|
* @return players parsed by the last `ParseWith()` or `Parse()` call. |
||||||
|
*/ |
||||||
|
public final function array<EPlayer> GetPlayers() |
||||||
|
{ |
||||||
|
local int i; |
||||||
|
local array<EPlayer> result; |
||||||
|
|
||||||
|
for (i = 0; i < currentSelection.length; i += 1) |
||||||
|
{ |
||||||
|
if (currentSelection[i].IsExistent()) { |
||||||
|
result[result.length] = EPlayer(currentSelection[i].Copy()); |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parses players from `parser` according to the currently present players. |
||||||
|
* |
||||||
|
* Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||||
|
* |
||||||
|
* @param parser `Parser` from which to parse player list. |
||||||
|
* It's state will be set to failed in case the parsing fails. |
||||||
|
* @return `true` if parsing was successful and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool ParseWith(Parser parser) |
||||||
|
{ |
||||||
|
local Parser.ParserState confirmedState; |
||||||
|
|
||||||
|
if (parser == none) return false; |
||||||
|
if (!parser.Ok()) return false; |
||||||
|
if (parser.HasFinished()) return false; |
||||||
|
|
||||||
|
Reset(); |
||||||
|
confirmedState = parser.Skip().GetCurrentState(); |
||||||
|
if (!parser.Match(T(TOPEN_BRACKET)).Ok()) |
||||||
|
{ |
||||||
|
ParseSelector(parser.RestoreState(confirmedState)); |
||||||
|
if (parser.Ok()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
while (parser.Ok() && !parser.HasFinished()) |
||||||
|
{ |
||||||
|
confirmedState = parser.Skip().GetCurrentState(); |
||||||
|
if (parser.Match(T(TCLOSE_BRACKET)).Ok()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
parser.RestoreState(confirmedState); |
||||||
|
if (parsedFirstSelector) { |
||||||
|
parser.Match(T(TCOMMA)).Skip(); |
||||||
|
} |
||||||
|
ParseSelector(parser); |
||||||
|
parser.Skip(); |
||||||
|
} |
||||||
|
parser.Fail(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Resets this object to initial state before parsing and update |
||||||
|
// `playersSnapshot` to contain current players. |
||||||
|
private final function Reset() |
||||||
|
{ |
||||||
|
parsedFirstSelector = false; |
||||||
|
currentSelection.length = 0; |
||||||
|
_.memory.FreeMany(playersSnapshot); |
||||||
|
playersSnapshot.length = 0; |
||||||
|
playersSnapshot = _.players.GetAll(); |
||||||
|
selectorDelimiters.length = 0; |
||||||
|
selectorDelimiters[0] = T(TCOMMA); |
||||||
|
selectorDelimiters[1] = T(TCLOSE_BRACKET); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parses players from `toParse` according to the currently present players. |
||||||
|
* |
||||||
|
* Array of parsed players can be retrieved by `self.GetPlayers()` method. |
||||||
|
* |
||||||
|
* @param toParse `Text` from which to parse player list. |
||||||
|
* @return `true` if parsing was successful and `false` otherwise. |
||||||
|
*/ |
||||||
|
public final function bool Parse(BaseText toParse) |
||||||
|
{ |
||||||
|
local bool wasSuccessful; |
||||||
|
local Parser parser; |
||||||
|
|
||||||
|
if (toParse == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
parser = _.text.Parse(toParse); |
||||||
|
wasSuccessful = ParseWith(parser); |
||||||
|
parser.FreeSelf(); |
||||||
|
return wasSuccessful; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties |
||||||
|
{ |
||||||
|
TSELF = 0 |
||||||
|
stringConstants(0) = "self" |
||||||
|
TADMIN = 1 |
||||||
|
stringConstants(1) = "admin" |
||||||
|
TALL = 2 |
||||||
|
stringConstants(2) = "all" |
||||||
|
TNOT = 3 |
||||||
|
stringConstants(3) = "!" |
||||||
|
TKEY = 4 |
||||||
|
stringConstants(4) = "#" |
||||||
|
TMACRO = 5 |
||||||
|
stringConstants(5) = "@" |
||||||
|
TCOMMA = 6 |
||||||
|
stringConstants(6) = "," |
||||||
|
TOPEN_BRACKET = 7 |
||||||
|
stringConstants(7) = "[" |
||||||
|
TCLOSE_BRACKET = 8 |
||||||
|
stringConstants(8) = "]" |
||||||
|
TME = 9 |
||||||
|
stringConstants(9) = "me" |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue