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