Compare commits
107 Commits
187e69eedc
...
f15e704ce2
268 changed files with 25574 additions and 8806 deletions
@ -0,0 +1,8 @@
|
||||
; 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" |
@ -0,0 +1,117 @@
|
||||
[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 |
@ -0,0 +1,5 @@
|
||||
; Define all databases you want Acedia to use here. |
||||
; For simply making default Acedia configs work, set `createIfMissing` below |
||||
; to `true`. |
||||
[Database LocalDatabase] |
||||
createIfMissing=false |
@ -0,0 +1,35 @@
|
||||
; Acedia requires adding its own `GameRules` to listen to many different |
||||
; game events. |
||||
|
||||
; In this config you can setup Acedia's user groups and persistent data |
||||
; storage. Enabling this feature automatically enables user group support, |
||||
; while persistent data is optional. |
||||
; Databases can be configured in `AcediaDB.ini`. |
||||
[default Users] |
||||
; Configures whether to use database (and which) for storing user groups. |
||||
; Set `useDatabaseForGroupsData` to `false` if you want to define which users |
||||
; belong to what groups inside this config. |
||||
useDatabaseForGroupsData=true |
||||
groupsDatabaseLink=[local]Database:/group_data |
||||
; Configures whether persistent data should be additionally used. |
||||
; It can only be stored inside a database. |
||||
usePersistentData=true |
||||
persistentDataDatabaseLink=[local]Database:/user_data |
||||
; Available groups. Only used if `useDatabaseForGroupsData` is set to `false`. |
||||
localUserGroup=admin |
||||
localUserGroup=moderator |
||||
localUserGroup=trusted |
||||
|
||||
; These groups definitions only work in case you're using a config with |
||||
; `useDatabaseForGroupsData` set to `false`. Simply add new `user=` record, |
||||
; specifying SteamIDs of the players, e.g. `user=76561197960287930`. |
||||
; You can also optionally specify a human-readable lable for the SteamID after |
||||
; slash "/", e.g. `user=76561197960287930/gabe`. |
||||
[admin UserGroup] |
||||
;user= |
||||
|
||||
[moderator UserGroup] |
||||
;user= |
||||
|
||||
[trusted UserGroup] |
||||
;user= |
@ -0,0 +1,22 @@
|
||||
[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" |
@ -0,0 +1,163 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,69 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,197 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,220 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,805 @@
|
||||
/** |
||||
* 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
@ -0,0 +1,939 @@
|
||||
/** |
||||
* 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.") |
||||
} |
@ -0,0 +1,249 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,983 @@
|
||||
/** |
||||
* 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.") |
||||
} |
@ -0,0 +1,66 @@
|
||||
/** |
||||
* 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 |
||||
} |
@ -0,0 +1,81 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,230 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,708 @@
|
||||
/** |
||||
* 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\".") |
||||
} |
@ -0,0 +1,39 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,39 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,39 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,39 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,39 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,494 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,61 @@
|
||||
/** |
||||
* 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 |
||||
{ |
||||
} |
@ -0,0 +1,351 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,306 @@
|
||||
/** |
||||
* 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.") |
||||
} |
@ -0,0 +1,142 @@
|
||||
/** |
||||
* 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'; |
||||
} |
@ -0,0 +1,177 @@
|
||||
/** |
||||
* 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.") |
||||
} |
@ -0,0 +1,119 @@
|
||||
/** |
||||
* 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' |
||||
} |
@ -0,0 +1,863 @@
|
||||
/** |
||||
* 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 |
||||
} |
@ -0,0 +1,440 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,132 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,654 @@
|
||||
/** |
||||
* 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 BigInt extends AcediaObject |
||||
dependson(MathApi); |
||||
|
||||
/// A simple big integer implementation. |
||||
/// |
||||
/// [`BigInt`]'s main purpose is to allow Acedia's databases to store integers of arbitrary size. |
||||
/// It can be used for long arithmetic computations, but it was mainly meant as a players' |
||||
/// statistics counter and, therefore, not optimized for performing large amount of operations. |
||||
|
||||
/// [`BigInt`] data as a struct - meant to be used to store [`BigInt`]'s values inside |
||||
/// the local databases. |
||||
struct BigIntData { |
||||
var bool negative; |
||||
var array<byte> digits; |
||||
}; |
||||
|
||||
/// Result of comparison for [`BigInt`]s with each other. |
||||
enum BigIntCompareResult |
||||
{ |
||||
BICR_Less, |
||||
BICR_Equal, |
||||
BICR_Greater |
||||
}; |
||||
|
||||
/// Does stored [`BigInt`] have a negative sign? |
||||
var private bool negative; |
||||
/// Digits array, from least to most significant. For example, for 13524: |
||||
/// |
||||
/// ``` |
||||
/// `digits[0] = 4` |
||||
/// `digits[1] = 2` |
||||
/// `digits[2] = 5` |
||||
/// `digits[3] = 3` |
||||
/// `digits[4] = 1` |
||||
/// ``` |
||||
/// |
||||
/// Valid [`BigInt`] should not have this array empty: zero should be represented by an array with |
||||
/// a single `0`-element. |
||||
/// This isn't a most efficient representation for [`BigInt`], but it's easy to convert to and from |
||||
/// decimal representation. |
||||
/// |
||||
/// # Invariants |
||||
/// |
||||
/// This array must not have leading (in the sense of significance) zeroes. |
||||
/// That is, last element of the array should not be a `0`. |
||||
/// The only exception if if stored value is `0`, then `digits` must consist of |
||||
/// a single `0` element. |
||||
var private array<byte> digits; |
||||
|
||||
/// Constants useful for converting [`BigInt`] back to [`int`], while avoiding overflow. |
||||
/// We can add less digits than that without any fear of overflow. |
||||
const DIGITS_IN_MAX_INT = 10; |
||||
/// Maximum [`int`] value is `2147483647`, so in case most significant digit is 10th and is `2` |
||||
/// (so number has a form of `2xxxxxxxxx`), to check for overflow we only need to compare |
||||
/// combination of the rest of the digits with this constant. |
||||
const ALMOST_MAX_INT = 147483647; |
||||
/// To add last digit we add/subtract that digit multiplied by this value. |
||||
const LAST_DIGIT_ORDER = 1000000000; |
||||
|
||||
protected function Constructor() { |
||||
SetZero(); |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
negative = false; |
||||
digits.length = 0; |
||||
} |
||||
|
||||
// Auxiliary method to set current value to zero |
||||
private function SetZero() { |
||||
negative = false; |
||||
digits.length = 1; |
||||
digits[0] = 0; |
||||
} |
||||
|
||||
// Minimal [`int`] value `-2,147,483,648` is somewhat of a pain to handle, so just use this |
||||
// auxiliary pre-made constructor for it |
||||
private function SetMinimalNegative() { |
||||
negative = true; |
||||
digits.length = 10; |
||||
digits[0] = 8; |
||||
digits[1] = 4; |
||||
digits[2] = 6; |
||||
digits[3] = 3; |
||||
digits[4] = 8; |
||||
digits[5] = 4; |
||||
digits[6] = 7; |
||||
digits[7] = 4; |
||||
digits[8] = 1; |
||||
digits[9] = 2; |
||||
} |
||||
|
||||
// Removes unnecessary zeroes from leading digit positions `digits`. |
||||
// Does not change contained value. |
||||
private final function TrimLeadingZeroes() { |
||||
local int i, zeroesToRemove; |
||||
|
||||
// Finds how many leading zeroes there is. |
||||
// Since `digits` stores digits from least to most significant, we need to check from the end of |
||||
// `digits` array. |
||||
for (i = digits.length - 1; i >= 0; i -= 1) { |
||||
if (digits[i] != 0) { |
||||
break; |
||||
} |
||||
zeroesToRemove += 1; |
||||
} |
||||
// `digits` must not be empty, enforce `0` value in that case |
||||
if (zeroesToRemove >= digits.length) { |
||||
SetZero(); |
||||
} |
||||
else { |
||||
digits.length = digits.length - zeroesToRemove; |
||||
} |
||||
} |
||||
|
||||
/// Changes current value of [`BigInt`] to given value. |
||||
public final function Set(BigInt value) |
||||
{ |
||||
if (value != none) { |
||||
value.TrimLeadingZeroes(); |
||||
digits = value.digits; |
||||
negative = value.negative; |
||||
} |
||||
} |
||||
|
||||
/// Changes current value of [`BigInt`] to given value. |
||||
public final function SetInt(int value) { |
||||
local MathApi.IntegerDivisionResult divisionResult; |
||||
|
||||
negative = false; |
||||
digits.length = 0; |
||||
if (value < 0) { |
||||
// Treat special case of minimal [`int`] value `-2,147,483,648` that |
||||
// won't fit into positive [`int`] as special and use pre-made |
||||
// specialized constructor `CreateMinimalNegative()` |
||||
if (value < -maxInt) { |
||||
SetMinimalNegative(); |
||||
return; |
||||
} else { |
||||
negative = true; |
||||
value *= -1; |
||||
} |
||||
} |
||||
if (value == 0) { |
||||
digits[0] = 0; |
||||
} else { |
||||
while (value > 0) { |
||||
divisionResult = __().math.IntegerDivision(value, 10); |
||||
value = divisionResult.quotient; |
||||
digits[digits.length] = divisionResult.remainder; |
||||
} |
||||
} |
||||
TrimLeadingZeroes(); |
||||
} |
||||
|
||||
/// Changes current value of [`BigInt`] to the value, given by decimal representation. |
||||
/// |
||||
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given |
||||
/// as an argument, caller's value won't change. |
||||
/// Returns `true` in case of success and `false` otherwise. |
||||
public final function bool SetDecimal(BaseText value) { |
||||
local int i; |
||||
local byte nextDigit; |
||||
local bool newNegative; |
||||
local array<byte> newDigits; |
||||
local Parser parser; |
||||
local Basetext.Character nextCharacter; |
||||
|
||||
if (value == none) { |
||||
return false; |
||||
} |
||||
parser = value.Parse(); |
||||
newNegative = ParseSign(parser); |
||||
// Reset to valid state whether sign was consumed or not |
||||
parser.Confirm(); |
||||
parser.R(); |
||||
newDigits.length = parser.GetRemainingLength(); |
||||
// Parse new one |
||||
i = newDigits.length - 1; |
||||
while (!parser.HasFinished()){ |
||||
// This should not happen, but just in case |
||||
if (i < 0) { |
||||
break; |
||||
} |
||||
parser.MCharacter(nextCharacter); |
||||
nextDigit = __().text.CharacterToInt(nextCharacter, 10); |
||||
if (nextDigit < 0) { |
||||
return false; |
||||
} |
||||
newDigits[i] = nextDigit; |
||||
i -= 1; |
||||
} |
||||
parser.FreeSelf(); |
||||
digits = newDigits; |
||||
negative = newNegative; |
||||
TrimLeadingZeroes(); |
||||
return true; |
||||
} |
||||
|
||||
// Tries to parse either `+` or `-` and returns `true` iff it parsed `-`. |
||||
// If neither got parsed, `parser` will enter failed state. |
||||
// Assumes `parser` isn't `none`. |
||||
private final function bool ParseSign(Parser parser) { |
||||
parser.Match(P("-")); |
||||
negative = parser.Ok(); |
||||
if (parser.Ok()) { |
||||
negative = true; |
||||
} |
||||
else { |
||||
parser.R(); |
||||
parser.Match(P("+")); |
||||
} |
||||
return negative; |
||||
} |
||||
|
||||
/// Changes current value of [`BigInt`] to the value, given by decimal representation. |
||||
/// |
||||
/// If invalid decimal representation (digits only, possibly with leading sign) is given as |
||||
/// an argument, caller's value won't change. |
||||
/// Returns `true` in case of success and `false` otherwise. |
||||
public final function bool SetDecimal_S(string value) { |
||||
local bool result; |
||||
local MutableText wrapper; |
||||
|
||||
wrapper = __().text.FromStringM(value); |
||||
result = SetDecimal(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
// Auxiliary method for comparing two [`BigInt`]s by their absolute value. |
||||
private function BigIntCompareResult _compareAbsolute(BigInt other) { |
||||
local int i; |
||||
local array<byte> otherDigits; |
||||
|
||||
otherDigits = other.digits; |
||||
if (digits.length == otherDigits.length) |
||||
{ |
||||
for (i = digits.length - 1; i >= 0; i -= 1) |
||||
{ |
||||
if (digits[i] < otherDigits[i]) { |
||||
return BICR_Less; |
||||
} |
||||
if (digits[i] > otherDigits[i]) { |
||||
return BICR_Greater; |
||||
} |
||||
} |
||||
return BICR_Equal; |
||||
} |
||||
if (digits.length < otherDigits.length) { |
||||
return BICR_Less; |
||||
} |
||||
return BICR_Greater; |
||||
} |
||||
|
||||
/// Compares caller [`BigInt`] to [`other`]. |
||||
/// |
||||
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result. |
||||
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then |
||||
/// it means that caller [`BigInt`] is smaller that `other`. |
||||
/// If argument is `none`, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`]. |
||||
public function BigIntCompareResult Compare(BigInt other) { |
||||
local BigIntCompareResult resultForModulus; |
||||
|
||||
if (other == none) return BICR_Less; |
||||
if (negative && !other.negative) return BICR_Less; |
||||
if (!negative && other.negative) return BICR_Greater; |
||||
resultForModulus = _compareAbsolute(other); |
||||
if (resultForModulus == BICR_Equal) return BICR_Equal; |
||||
if (negative && (resultForModulus == BICR_Greater)) return BICR_Less; |
||||
if (!negative && (resultForModulus == BICR_Less)) return BICR_Less; |
||||
|
||||
return BICR_Greater; |
||||
} |
||||
|
||||
/// Compares caller [`BigInt`] to [`other`]. |
||||
/// |
||||
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result. |
||||
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then |
||||
/// it means that caller [`BigInt`] is smaller that `other`. |
||||
public function BigIntCompareResult CompareInt(int other) { |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.ToBigInt(other); |
||||
result = Compare(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
/// Compares caller [`BigInt`] to a decimal representation of a number. |
||||
/// |
||||
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result. |
||||
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then |
||||
/// it means that caller [`BigInt`] is smaller that `other`. |
||||
/// If argument is `none` or is an invalid decimal representation (digits only, possibly with |
||||
/// leading sign, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`]. |
||||
public function BigIntCompareResult CompareDecimal(BaseText other) { |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.MakeBigInt(other); |
||||
result = Compare(wrapper); |
||||
_.memory.Free(wrapper); |
||||
return result; |
||||
} |
||||
|
||||
/// Compares caller [`BigInt`] to a decimal representation of a number. |
||||
/// |
||||
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result. |
||||
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then |
||||
/// it means that caller [`BigInt`] is smaller that `other`. |
||||
/// If argument is an invalid decimal representation (digits only, possibly with leading sign, then |
||||
/// it is considered to be less ([`BICR_Less`]) than caller [`BigInt`]. |
||||
public function BigIntCompareResult CompareDecimal_S(string other) { |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.MakeBigInt_S(other); |
||||
result = Compare(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
// Adds absolute values of caller [`BigInt`] and [`other`] with no changes to the sign. |
||||
private function _add(BigInt other) { |
||||
local int i; |
||||
local byte carry, digitSum; |
||||
local array<byte> otherDigits; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
otherDigits = other.digits; |
||||
if (digits.length < otherDigits.length) { |
||||
digits.length = otherDigits.length; |
||||
} else { |
||||
otherDigits.length = digits.length; |
||||
} |
||||
carry = 0; |
||||
for (i = 0; i < digits.length; i += 1) { |
||||
digitSum = digits[i] + otherDigits[i] + carry; |
||||
digits[i] = _.math.Remainder(digitSum, 10); |
||||
carry = (digitSum - digits[i]) / 10; |
||||
} |
||||
if (carry > 0) { |
||||
digits[digits.length] = carry; |
||||
} |
||||
// No leading zeroes can be created here, so no need to trim |
||||
} |
||||
|
||||
// Subtracts absolute value of [`other`] from the caller [`BigInt`], flipping caller's sign in case |
||||
// `other`'s absolute value is bigger. |
||||
private function _sub(BigInt other) { |
||||
local int i; |
||||
local int carry, nextDigit; |
||||
local array<byte> minuendDigits, subtrahendDigits; |
||||
local BigIntCompareResult resultForModulus; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
resultForModulus = _compareAbsolute(other); |
||||
if (resultForModulus == BICR_Equal) { |
||||
SetZero(); |
||||
return; |
||||
} |
||||
if (resultForModulus == BICR_Less) { |
||||
negative = !negative; |
||||
minuendDigits = other.digits; |
||||
subtrahendDigits = digits; |
||||
} else { |
||||
minuendDigits = digits; |
||||
subtrahendDigits = other.digits; |
||||
} |
||||
digits.length = minuendDigits.length; |
||||
subtrahendDigits.length = minuendDigits.length; |
||||
carry = 0; |
||||
for (i = 0; i < digits.length; i += 1) { |
||||
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry; |
||||
if (nextDigit < 0) { |
||||
nextDigit += 10; |
||||
carry = -1; |
||||
} else { |
||||
carry = 0; |
||||
} |
||||
digits[i] = nextDigit; |
||||
} |
||||
TrimLeadingZeroes(); |
||||
} |
||||
|
||||
/// Adds another value to the caller [`BigInt`]. |
||||
/// |
||||
/// If argument is `none`, then given method does nothing. |
||||
public function Add(BigInt other) { |
||||
if (other == none) { |
||||
return; |
||||
} |
||||
if (negative == other.negative) { |
||||
_add(other); |
||||
} else { |
||||
_sub(other); |
||||
} |
||||
} |
||||
|
||||
/// Adds another value to the caller [`BigInt`]. |
||||
public function AddInt(int other) { |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.ToBigInt(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Adds decimal representation of the number to the caller [`BigInt`]. |
||||
/// |
||||
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given - |
||||
/// does nothing. |
||||
public function AddDecimal(BaseText other) { |
||||
local BigInt otherObject; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
otherObject = _.math.MakeBigInt(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Adds decimal representation of the number to the caller [`BigInt`]. |
||||
/// |
||||
/// If invalid decimal representation (digits only, possibly with leading sign) is given - |
||||
/// does nothing. |
||||
public function AddDecimal_S(string other) { |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.MakeBigInt_S(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Subtracts another value to the caller [`BigInt`]. |
||||
/// |
||||
/// If argument is `none`, then given method does nothing. |
||||
public function Subtract(BigInt other) { |
||||
if (negative != other.negative) { |
||||
_add(other); |
||||
} else { |
||||
_sub(other); |
||||
} |
||||
} |
||||
|
||||
/// Adds another value to the caller [`BigInt`]. |
||||
public function SubtractInt(int other) { |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.ToBigInt(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Subtracts decimal representation of the number to the caller [`BigInt`]. |
||||
/// |
||||
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given - |
||||
/// does nothing. |
||||
public function SubtractDecimal(BaseText other) { |
||||
local BigInt otherObject; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
otherObject = _.math.MakeBigInt(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Adds decimal representation of the number to the caller [`BigInt`]. |
||||
/// |
||||
/// If invalid decimal representation (digits only, possibly with leading sign) is given - |
||||
/// does nothing. |
||||
public function SubtractDecimal_S(string other) { |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.MakeBigInt_S(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
} |
||||
|
||||
/// Checks if caller [`BigInt`] is negative. |
||||
/// |
||||
/// Returns if stored value is negative and `false` otherwise. |
||||
/// Zero is not considered negative number. |
||||
public function bool IsNegative() { |
||||
// Handle special case of zero first (it ignores `negative` flag) |
||||
if (digits.length == 1 && digits[0] == 0) { |
||||
return false; |
||||
} |
||||
return negative; |
||||
} |
||||
|
||||
/// Converts caller [`BigInt`] into [`int`] representation. |
||||
/// |
||||
/// In case stored value is outside `int`'s value range |
||||
/// (`[-maxInt-1, maxInt] == [-2147483648; 2147483647]`), method returns either maximal or minimal |
||||
// possible value, depending on the [`BigInt`]'s sign. |
||||
public function int ToInt() { |
||||
local int i; |
||||
local int accumulator; |
||||
local int safeDigitsAmount; |
||||
|
||||
if (digits.length <= 0) { |
||||
return 0; |
||||
} |
||||
if (digits.length > DIGITS_IN_MAX_INT) { |
||||
if (negative) { |
||||
return (-maxInt - 1); |
||||
} else { |
||||
return maxInt; |
||||
} |
||||
} |
||||
// At most `DIGITS_IN_MAX_INT - 1` iterations |
||||
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length); |
||||
for (i = safeDigitsAmount - 1; i >= 0; i -= 1) { |
||||
accumulator *= 10; |
||||
accumulator += digits[i]; |
||||
} |
||||
if (negative) { |
||||
accumulator *= -1; |
||||
} |
||||
accumulator = AddUnsafeDigitToInt(accumulator); |
||||
return accumulator; |
||||
} |
||||
|
||||
/// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but adding the next digit can, |
||||
/// so we need to handle it differently and more carefully. |
||||
/// Assumes `digits.length <= DIGITS_IN_MAX_INT`. |
||||
private function int AddUnsafeDigitToInt(int accumulator) { |
||||
local int unsafeDigit; |
||||
local bool noOverflow; |
||||
|
||||
if (digits.length < DIGITS_IN_MAX_INT) { |
||||
return accumulator; |
||||
} |
||||
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1]; |
||||
// `maxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`, there is no overflow, |
||||
// otherwise - check rest of the digits |
||||
noOverflow = (unsafeDigit < 2); |
||||
if (unsafeDigit == 2) { |
||||
// Include `maxInt` and `-maxInt-1` (minimal possible value) into an overflow too - this way |
||||
// we still give a correct result, but do not have to worry about `int`-arithmetic error |
||||
noOverflow = noOverflow |
||||
|| (negative && (accumulator > -ALMOST_MAX_INT - 1)) |
||||
|| (!negative && (accumulator < ALMOST_MAX_INT)); |
||||
} |
||||
if (noOverflow) { |
||||
if (negative) { |
||||
accumulator -= unsafeDigit * LAST_DIGIT_ORDER; |
||||
} |
||||
else { |
||||
accumulator += unsafeDigit * LAST_DIGIT_ORDER; |
||||
} |
||||
return accumulator; |
||||
} |
||||
// Handle overflow |
||||
if (negative) { |
||||
return (-maxInt - 1); |
||||
} |
||||
return maxInt; |
||||
} |
||||
|
||||
/// Converts caller [`BigInt`] into [`Text`] representation. |
||||
public function Text ToText() { |
||||
return ToText_M().IntoText(); |
||||
} |
||||
|
||||
/// Converts caller [`BigInt`] into [`MutableText`] representation. |
||||
public function MutableText ToText_M() { |
||||
local int i; |
||||
local MutableText result; |
||||
|
||||
result = _.text.Empty(); |
||||
if (negative) { |
||||
result.AppendCharacter(_.text.GetCharacter("-")); |
||||
} |
||||
for (i = digits.length - 1; i >= 0; i -= 1) { |
||||
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Converts caller [`BigInt`] into [`string`] representation. |
||||
public function string ToString() { |
||||
local int i; |
||||
local string result; |
||||
|
||||
if (negative) { |
||||
result = "-"; |
||||
} |
||||
for (i = digits.length - 1; i >= 0; i -= 1) { |
||||
result = result $ digits[i]; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Restores [`BigInt`] from the [`BigIntData`] value. |
||||
/// |
||||
/// This method is created to make an efficient way to store [`BigInt`]. |
||||
public function FromData(BigIntData data) { |
||||
local int i; |
||||
|
||||
negative = data.negative; |
||||
digits = data.digits; |
||||
// Deal with possibly erroneous data |
||||
for (i = 0; i < digits.length; i += 1) { |
||||
if (digits[i] > 9) { |
||||
digits[i] = 9; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Converts caller [`BigInt`]'s value into [`BigIntData`]. |
||||
/// |
||||
/// This method is created to make an efficient way to store [`BigInt`]. |
||||
public function BigIntData ToData() { |
||||
local BigIntData result; |
||||
|
||||
result.negative = negative; |
||||
result.digits = digits; |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,104 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2020-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 |
||||
*/ |
||||
class MathApi extends AcediaObject; |
||||
|
||||
//! API for basic math methods and [`BigInt`] creation. |
||||
|
||||
/// For storing result of integer division. |
||||
/// |
||||
/// If we divide `number` by `divisor`, then `number = divisor/// quotient + remainder`. |
||||
struct IntegerDivisionResult |
||||
{ |
||||
var int quotient; |
||||
var int remainder; |
||||
}; |
||||
|
||||
/// Converts given [`int`] value into [`BigInt`] value.. |
||||
public function BigInt ToBigInt(int value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
result.SetInt(value); |
||||
return result; |
||||
} |
||||
|
||||
|
||||
/// Creates new `BigInt` value, based on the decimal number representation. |
||||
/// |
||||
/// If (and only if) `none` or invalid decimal representation (digits only, possibly with |
||||
/// leading sign) is given as an argument, method will return `none`. |
||||
public function BigInt MakeBigInt(BaseText value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
if (result.SetDecimal(value)) { |
||||
return result; |
||||
} |
||||
result.FreeSelf(); |
||||
return none; |
||||
} |
||||
|
||||
/// Creates new `BigInt` value, based on the decimal number representation. |
||||
/// |
||||
/// If (and only if) invalid decimal representation (digits only, possibly with leading sign) is |
||||
/// given as an argument, method will return `none`. |
||||
public function BigInt MakeBigInt_S(string value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
if (result.SetDecimal_S(value)) { |
||||
return result; |
||||
} |
||||
result.FreeSelf(); |
||||
return none; |
||||
} |
||||
|
||||
/// Computes remainder of the integer division of [`number`] by [`divisor`]. |
||||
/// |
||||
/// This method is necessary as a replacement for `%` module operator, since it is an operation on |
||||
/// `float`s in UnrealScript and does not have appropriate value range to work with big integer |
||||
// values. |
||||
public function int Remainder(int number, int divisor) |
||||
{ |
||||
local int quotient; |
||||
|
||||
quotient = number / divisor; |
||||
return (number - quotient * divisor); |
||||
} |
||||
|
||||
/// Computes quotient and remainder of the integer division of [`number`] by [`divisor`]. |
||||
/// |
||||
/// See `MathApi::Remainder()` method if you only need remainder. |
||||
public function IntegerDivisionResult IntegerDivision(int number, int divisor) |
||||
{ |
||||
local IntegerDivisionResult result; |
||||
|
||||
result.quotient = number / divisor; |
||||
result.remainder = (number - result.quotient * divisor); |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,158 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2020-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 AcediaObjectPool extends Object |
||||
config(AcediaSystem); |
||||
|
||||
//! Acedia's implementation for object pool. |
||||
//! |
||||
//! Unlike generic built in [`Engine::ObjectPool`], that can only store objects of one specific |
||||
//! class, it specializes in a single class to allow for both faster allocation and |
||||
//! faster deallocation (we don't need to look for an object of particular class to return |
||||
//! an unused instance). |
||||
//! |
||||
//! Allows to set a maximum capacity in a config. |
||||
|
||||
/// Represents config entry about pool capacity. |
||||
/// |
||||
/// This struct and it's associated array [`poolSizeOverwrite`] allows server admins to rewrite |
||||
/// the pool capacity for each class. |
||||
struct PoolSizeSetting { |
||||
var class<AcediaObject> objectClass; |
||||
var int maxPoolSize; |
||||
}; |
||||
|
||||
var private config const array<PoolSizeSetting> poolSizeOverwrite; |
||||
// Class of objects that this `AcediaObjectPool` stores. |
||||
// if `== none`, - object pool is considered uninitialized. |
||||
var private class<AcediaObject> storedClass; |
||||
|
||||
/// Capacity for object pool that we are using. |
||||
/// Obtained from [`poolSizeOverwrite`] during initialization and cannot be changed later. |
||||
var private int usedMaxPoolSize; |
||||
|
||||
// Actual storage, functions on LIFO principle. |
||||
var private array<AcediaObject> objectPool; |
||||
|
||||
// Determines default object pool size for the initialization. |
||||
private final function int GetMaxPoolSizeForClass(class<AcediaObject> classToCheck) { |
||||
local int i; |
||||
local int result; |
||||
|
||||
if (classToCheck != none) { |
||||
result = classToCheck.default.defaultMaxPoolSize; |
||||
} |
||||
else { |
||||
result = -1; |
||||
} |
||||
// Try to replace it with server's settings |
||||
for (i = 0; i < poolSizeOverwrite.length; i += 1) { |
||||
if (poolSizeOverwrite[i].objectClass == classToCheck) { |
||||
result = poolSizeOverwrite[i].maxPoolSize; |
||||
break; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/// Initializes caller object pool to store objects of the given class. |
||||
/// |
||||
/// Returns `true` if initialization completed, `false` otherwise (including if it was already |
||||
/// completed with passed [`classToStore`]). |
||||
/// |
||||
/// If successful, this action is irreversible: same pool cannot be re-initialized. |
||||
/// |
||||
/// [`forcedPoolSize`] defines max pool size for the caller [`AcediaObjectPool`]. |
||||
/// Leaving it at default `0` value will cause method to auto-determine the size: gives priority to |
||||
/// the [`poolSizeOverwrite`] config array; if not specified, uses [`AcediaObject`]'s |
||||
/// [`AcediaObject::defaultMaxPoolSize`] (ignoring [`AcediaObject::usesObjectPool`] setting). |
||||
public final function bool Initialize( |
||||
class<AcediaObject> classToStore, |
||||
optional int forcedPoolSize |
||||
) { |
||||
if (storedClass != none) return false; |
||||
if (classToStore == none) return false; |
||||
|
||||
// If does not matter that we've set those variables until |
||||
// we set `storedClass`. |
||||
if (forcedPoolSize == 0) { |
||||
usedMaxPoolSize = GetMaxPoolSizeForClass(classToStore); |
||||
} |
||||
else { |
||||
usedMaxPoolSize = forcedPoolSize; |
||||
} |
||||
if (usedMaxPoolSize == 0) { |
||||
return false; |
||||
} |
||||
storedClass = classToStore; |
||||
return true; |
||||
} |
||||
|
||||
/// Returns class of objects stored inside the caller [`AcediaObjectPool`]. |
||||
/// |
||||
/// `none` means object pool was not initialized. |
||||
public final function class<AcediaObject> GetClassOfStoredObjects() { |
||||
return storedClass; |
||||
} |
||||
|
||||
/// Clear the storage of all its contents. |
||||
public final function Clear() { |
||||
objectPool.length = 0; |
||||
} |
||||
|
||||
/// Adds object to the caller storage (that needs to be initialized to store [`newObject.class`] |
||||
/// classes). |
||||
/// |
||||
/// Returns `true` on success and `false` on failure (can happen if passed [`newObject`] reference |
||||
/// was invalid, caller storage is not initialized yet or reached it's capacity). |
||||
/// |
||||
/// For performance purposes does not do duplicates checks, this should be verified from outside |
||||
/// [`AcediaObjectPool`]. |
||||
/// |
||||
/// Performs type checks and only allows objects of the class that caller [`AcediaObjectPool`] was |
||||
/// initialized for. |
||||
public final function bool Store(AcediaObject newObject) { |
||||
if (newObject == none) return false; |
||||
if (newObject.class != storedClass) return false; |
||||
|
||||
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) { |
||||
return false; |
||||
} |
||||
objectPool[objectPool.length] = newObject; |
||||
return true; |
||||
} |
||||
|
||||
/// Returns last stored object from the pool, removing it from that pool in the process. |
||||
/// |
||||
/// Only returns `none` if caller `AcediaObjectPool` is either empty or not initialized. |
||||
public final function AcediaObject Fetch() { |
||||
local AcediaObject result; |
||||
|
||||
if (storedClass == none) return none; |
||||
if (objectPool.length <= 0) return none; |
||||
|
||||
result = objectPool[objectPool.length - 1]; |
||||
objectPool.length = objectPool.length - 1; |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,366 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2020-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 MemoryApi extends AcediaObject; |
||||
|
||||
//! API that provides functions for managing object of classes, derived from `AcediaObject`. |
||||
//! |
||||
//! This is most-basic API that must be created before anything else in Acedia, since it is |
||||
//! responsible for the proper creation of `AcediaObject`s. |
||||
//! It takes care of managing their object pools, as well as ensuring that constructors and |
||||
//! finalizers are called properly. |
||||
//! |
||||
//! Almost all `AcediaObject`s should use this API's methods for their own creation and destruction. |
||||
//! |
||||
//! ## Usage |
||||
//! |
||||
//! First of all, this API is only meant for non-actor `Object` creation. |
||||
//! `Actor` creation is generally avoided in Acedia and, when unavoidable, different APIs |
||||
//! are dealing with that. |
||||
//! `MemoryApi` is designed to work in the absence of any level (and, therefore, `Actor`s) at all. |
||||
//! |
||||
//! Simply use `MemoryApi.Allocate()` to create a new object and `MemoryApi.Free()` to get rid of |
||||
//! unneeded reference. |
||||
//! Do note that `AcediaObject`s use reference counting and object will be deallocated and pooled |
||||
//! only after every trackable reference was released by `MemoryApi.Free()`. |
||||
//! |
||||
//! Best practice is to only care about what object reference you're keeping, properly release them |
||||
//! with `MemoryApi.Free()` and to NEVER EVER USE THEM after you've release them. |
||||
//! Regardless of whether they were actually deallocated. |
||||
//! |
||||
//! There's also a set of auxiliary methods for either loading `class`es from their |
||||
//! `BaseText`/`string`-given names or even directly creating objects of said classes. |
||||
//! |
||||
//! ## Customizing object pools for your classes |
||||
//! |
||||
//! Object pool usage can be disabled completely for your class by setting `usesObjectPool = false` |
||||
//! in `defaultproperties` block. |
||||
//! Without object pools `MemoryApi.Allocate()` will create a new instance of your class every |
||||
//! single time. |
||||
//! |
||||
//! You can also set a limit to how many objects will be stored in an object pool with |
||||
//! `defaultMaxPoolSize` variable. |
||||
//! Negative number (default for `AcediaObject`) means that object pool can grow without a limit. |
||||
//! `0` effectively disables object pool, similar to setting `usesObjectPool = false`. |
||||
//! However, this can be overwritten by server's settings |
||||
//! (see `AcediaSystem.ini`: `AcediaObjectPool`). |
||||
|
||||
// Store all created pools, so that we can quickly forget stored objects upon garbage collection |
||||
var private array<AcediaObjectPool> registeredPools; |
||||
|
||||
/// Forgets about all stored (deallocated) object references in registered object pools. |
||||
protected function DropPools() { |
||||
local int i; |
||||
|
||||
registeredPools = default.registeredPools; |
||||
for (i = 0; i < registeredPools.length; i += 1) { |
||||
if (registeredPools[i] == none) { |
||||
continue; |
||||
} |
||||
registeredPools[i].Clear(); |
||||
} |
||||
} |
||||
|
||||
/// Creates a class instance from its `BaseText` representation. |
||||
/// |
||||
/// Does not generate log messages upon failure. |
||||
public function class<Object> LoadClass(BaseText classReference) { |
||||
if (classReference == none) { |
||||
return none; |
||||
} |
||||
return class<Object>(DynamicLoadObject(classReference.ToString(), class'Class', true)); |
||||
} |
||||
|
||||
/// Creates a class instance from its `string` representation. |
||||
/// |
||||
/// Does not generate log messages upon failure. |
||||
public function class<Object> LoadClass_S(string classReference) { |
||||
return class<Object>(DynamicLoadObject(classReference, class'Class', true)); |
||||
} |
||||
|
||||
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and |
||||
/// calling its constructor. |
||||
/// |
||||
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled |
||||
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`. |
||||
/// |
||||
/// Return value will only be `none` if `classToAllocate` is `none` or abstract. |
||||
public function AcediaObject Allocate( |
||||
class<AcediaObject> classToAllocate, |
||||
optional bool forceNewInstance |
||||
) { |
||||
local AcediaObject allocatedObject; |
||||
local AcediaObjectPool relevantPool; |
||||
|
||||
if (classToAllocate == none) { |
||||
return none; |
||||
} |
||||
// Try using pool first (but only if new instance is not required) |
||||
if (!forceNewInstance) { |
||||
relevantPool = classToAllocate.static._getPool(); |
||||
// `relevantPool == none` is expected if object / actor of is setup to |
||||
// not use object pools. |
||||
if (relevantPool != none) { |
||||
allocatedObject = relevantPool.Fetch(); |
||||
} |
||||
} |
||||
// If pools did not work - simply create object manually |
||||
if (allocatedObject == none) { |
||||
allocatedObject = (new classToAllocate); |
||||
} |
||||
// Allocation through `new` cannot fail, so its safe to call constructor |
||||
allocatedObject._constructor(); |
||||
return allocatedObject; |
||||
} |
||||
|
||||
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and |
||||
/// calling its constructor. |
||||
/// |
||||
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled |
||||
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`. |
||||
/// |
||||
/// Return value will only be `none` if `refToClassToAllocate` is `none`, doesn't refer to |
||||
/// an existing class or refers to an abstract class. |
||||
public function AcediaObject AllocateByReference( |
||||
BaseText refToClassToAllocate, |
||||
optional bool forceNewInstance |
||||
) { |
||||
local class<Object> classToAllocate; |
||||
|
||||
classToAllocate = LoadClass(refToClassToAllocate); |
||||
return Allocate(class<AcediaObject>(classToAllocate), forceNewInstance); |
||||
} |
||||
|
||||
/// Creates a new `AcediaObject` of a given subclass using pool (if permitted by settings) and |
||||
/// calling its constructor. |
||||
/// |
||||
/// If Acedia's object does make use of object pools, this method guarantees to return last pooled |
||||
/// object (in a LIFO queue), unless `forceNewInstance` is set to `true`. |
||||
/// |
||||
/// Return value will only be `none` if `refToClassToAllocate` is `none`, doesn't refer to |
||||
/// an existing class or refers to an abstract class. |
||||
public function AcediaObject AllocateByReference_S( |
||||
string refToClassToAllocate, |
||||
optional bool forceNewInstance |
||||
) { |
||||
local class<Object> classToAllocate; |
||||
|
||||
classToAllocate = LoadClass_S(refToClassToAllocate); |
||||
return Allocate(class<AcediaObject>(classToAllocate), forceNewInstance); |
||||
} |
||||
|
||||
/// Releases one reference to a given [`AcediaObject`], calling its finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store [`objectToRelease`] in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free(AcediaObject objectToRelease) { |
||||
local AcediaObjectPool relevantPool; |
||||
|
||||
if (objectToRelease == none) return; |
||||
if (!objectToRelease.IsAllocated()) return; |
||||
|
||||
objectToRelease._deref(); |
||||
// Finalize object if all of its references are gone |
||||
if (objectToRelease._getRefCount() <= 0) { |
||||
relevantPool = objectToRelease._getPool(); |
||||
objectToRelease._finalizer(); |
||||
if (relevantPool != none) { |
||||
relevantPool.Store(objectToRelease); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free2(AcediaObject objectToRelease1, AcediaObject objectToRelease2) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free3( |
||||
AcediaObject objectToRelease1, |
||||
AcediaObject objectToRelease2, |
||||
AcediaObject objectToRelease3 |
||||
) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
Free(objectToRelease3); |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free4( |
||||
AcediaObject objectToRelease1, |
||||
AcediaObject objectToRelease2, |
||||
AcediaObject objectToRelease3, |
||||
AcediaObject objectToRelease4 |
||||
) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
Free(objectToRelease3); |
||||
Free(objectToRelease4); |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free5( |
||||
AcediaObject objectToRelease1, |
||||
AcediaObject objectToRelease2, |
||||
AcediaObject objectToRelease3, |
||||
AcediaObject objectToRelease4, |
||||
AcediaObject objectToRelease5 |
||||
) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
Free(objectToRelease3); |
||||
Free(objectToRelease4); |
||||
Free(objectToRelease5); |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free6( |
||||
AcediaObject objectToRelease1, |
||||
AcediaObject objectToRelease2, |
||||
AcediaObject objectToRelease3, |
||||
AcediaObject objectToRelease4, |
||||
AcediaObject objectToRelease5, |
||||
AcediaObject objectToRelease6 |
||||
) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
Free(objectToRelease3); |
||||
Free(objectToRelease4); |
||||
Free(objectToRelease5); |
||||
Free(objectToRelease6); |
||||
} |
||||
|
||||
/// Releases references to given [`AcediaObject`]s, calling their finalizers in case |
||||
/// all references were released. |
||||
/// |
||||
/// Method will attempt to store released objects in its object pool once deallocated, |
||||
/// unless it is forbidden by its class' settings. |
||||
public function Free7( |
||||
AcediaObject objectToRelease1, |
||||
AcediaObject objectToRelease2, |
||||
AcediaObject objectToRelease3, |
||||
AcediaObject objectToRelease4, |
||||
AcediaObject objectToRelease5, |
||||
AcediaObject objectToRelease6, |
||||
AcediaObject objectToRelease7 |
||||
) { |
||||
Free(objectToRelease1); |
||||
Free(objectToRelease2); |
||||
Free(objectToRelease3); |
||||
Free(objectToRelease4); |
||||
Free(objectToRelease5); |
||||
Free(objectToRelease6); |
||||
Free(objectToRelease7); |
||||
} |
||||
|
||||
/// Releases one reference for each `AcediaObject` inside the given array `objectsToRelease`, |
||||
/// calling finalizers for the ones that got all of their references released. |
||||
/// |
||||
/// Method will attempt to store objects inside `objectsToRelease` in their object pools, unless it |
||||
/// is forbidden by their class' settings. |
||||
public function FreeMany(array<AcediaObject> objectsToRelease) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < objectsToRelease.length; i += 1) { |
||||
Free(objectsToRelease[i]); |
||||
} |
||||
} |
||||
|
||||
/// Forces engine to perform garbage collection. |
||||
/// |
||||
/// Process of manual garbage collection causes significant lag spike during the game and should be |
||||
/// used sparingly and at right moments. |
||||
/// |
||||
/// If no `LevelCore` was setup, Acedia doesn't have access to the level and cannot perform garbage |
||||
/// collection, meaning that this method can fail. |
||||
/// |
||||
/// By default also cleans up all of the Acedia's objects pools. |
||||
/// Set [`keepAcediaPools`] to `true` to NOT garbage collect objects inside pools. |
||||
/// Pools won't be dropped regardless of this parameter if no `LevelCore` is found. |
||||
/// |
||||
/// Returns `true` if garbage collection successfully happened and `false` if it failed. |
||||
/// Garbage collection can only fail if no `LevelCore` was yet setup. |
||||
public function bool /*unreal*/ CollectGarbage(optional bool keepAcediaPools) { |
||||
local LevelCore core; |
||||
|
||||
// Try to find level core |
||||
core = class'ServerLevelCore'.static.GetInstance(); |
||||
if (core == none) { |
||||
core = class'ClientLevelCore'.static.GetInstance(); |
||||
} |
||||
if (core == none) { |
||||
return false; |
||||
} |
||||
// Drop content of all `AcediaObjectPools` first |
||||
if (!keepAcediaPools) { |
||||
DropPools(); |
||||
} |
||||
// This makes Unreal Engine do garbage collection |
||||
core.ConsoleCommand("obj garbage"); |
||||
return true; |
||||
} |
||||
|
||||
/// Registers new object pool to auto-clean before Acedia's garbage collection. |
||||
/// |
||||
/// Returns `true` if `newPool` was registered and `false` if `newPool == none` or was already |
||||
/// registered. |
||||
public function bool RegisterNewPool(AcediaObjectPool newPool) { |
||||
local int i; |
||||
|
||||
if (newPool == none) { |
||||
return false; |
||||
} |
||||
registeredPools = default.registeredPools; |
||||
for (i = 0; i < registeredPools.length; i += 1) { |
||||
if (registeredPools[i] == newPool) { |
||||
return false; |
||||
} |
||||
} |
||||
registeredPools[registeredPools.length] = newPool; |
||||
default.registeredPools = registeredPools; |
||||
return true; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,333 @@
|
||||
/** |
||||
* 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 SchedulerApi extends AcediaObject |
||||
config(AcediaSystem); |
||||
|
||||
//! This API is meant for scheduling various actions over time to help emulating |
||||
//! multi-threading by spreading some code executions over several different |
||||
//! game/server ticks. |
||||
//! |
||||
//! UnrealScript is inherently single-threaded and whatever method you call, |
||||
//! it will be completely executed within a single game's tick. |
||||
|
||||
// How often can files be saved on disk. |
||||
// |
||||
// This is a relatively expensive operation and we don't want to write a lot of different files |
||||
// at once. |
||||
// But since we lack a way to exactly measure how much time that saving will take, AcediaCore falls |
||||
// back to simply performing every saving with same uniform time intervals in-between. |
||||
// This variable decides how much time there should be between two file writing accesses. |
||||
// Negative and zero values mean that all writing disk access will be granted as soon as possible, |
||||
// without any cooldowns. |
||||
var private config float diskSaveCooldown; |
||||
|
||||
// Maximum total work units for jobs allowed per tick. |
||||
// |
||||
// Jobs are expected to be constructed such that they don't lead to a crash if they have to perform |
||||
// this much work. |
||||
// Changing default value of `10000` is not advised. |
||||
var private config int maxWorkUnits; |
||||
|
||||
// How many different jobs can be performed per tick. |
||||
// |
||||
// This limit is added so that `maxWorkUnits` won't be spread too thin if a lot of jobs |
||||
// get registered at once. |
||||
var private config int maxJobsPerTick; |
||||
|
||||
// We can (and will) automatically tick |
||||
var private bool tickAvailable; |
||||
// `true` == it is safe to use server API for a tick |
||||
// `false` == it is safe to use client API for a tick |
||||
var private bool tickFromServer; |
||||
// Our `Tick()` method is currently connected to the `OnTick()` signal. |
||||
// |
||||
// Keeping track of this allows us to disconnect from `OnTick()` signal when it is not necessary. |
||||
var private bool connectedToTick; |
||||
|
||||
// How much time if left until we can write to the disk again? |
||||
var private float currentDiskCooldown; |
||||
|
||||
// There is a limit (`maxJobsPerTick`) to how many different jobs we can perform per tick and if we |
||||
// register an amount jobs over that limit, we need to uniformly spread execution time between them. |
||||
// |
||||
// To achieve that we simply cyclically (in order) go over `currentJobs` array, each time executing |
||||
// exactly `maxJobsPerTick` jobs. |
||||
// |
||||
// `nextJobToPerform` remembers what job is to be executed next tick. |
||||
var private int nextJobToPerform; |
||||
var private array<SchedulerJob> currentJobs; |
||||
// Storing receiver objects, following example of signals/slots, is done without increasing their |
||||
// reference count, allowing them to get deallocated while we are still keeping their reference. |
||||
// |
||||
// To avoid using such deallocated receivers, we keep track of the life versions they've had when |
||||
// their disk requests were registered. |
||||
var private array<SchedulerDiskRequest> diskQueue; |
||||
var private array<AcediaObject> receivers; |
||||
var private array<int> receiversLifeVersions; |
||||
|
||||
/// Registers new scheduler job to be executed in the API. |
||||
/// |
||||
/// Does nothing if given `newJob` is already added. |
||||
public function AddJob(SchedulerJob newJob) { |
||||
local int i; |
||||
|
||||
if (newJob == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < currentJobs.length; i += 1) { |
||||
if (currentJobs[i] == newJob) { |
||||
return; |
||||
} |
||||
} |
||||
newJob.NewRef(); |
||||
currentJobs[currentJobs.length] = newJob; |
||||
UpdateTickConnection(); |
||||
} |
||||
|
||||
/// Requests another disk access. |
||||
/// |
||||
/// Use it like signal: `RequestDiskAccess(<receiver>).connect = <handler>`. |
||||
/// Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned wrapper object |
||||
/// [`SchedulerDiskRequest`]. |
||||
/// |
||||
/// Same as for signal/slots, [`receiver`] is an object, responsible for the disk request. |
||||
/// If this object gets deallocated - request will be thrown away. |
||||
/// Typically this should be an object in which connected method will be executed. |
||||
/// Returns wrapper object that provides `connect` delegate. |
||||
/// |
||||
/// # Examples |
||||
/// |
||||
/// ``` |
||||
/// _.scheduler.RequestDiskAccess(self).connect = MethodThatSaves(); |
||||
/// ``` |
||||
public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) { |
||||
local SchedulerDiskRequest newRequest; |
||||
|
||||
if (receiver == none) return none; |
||||
if (!receiver.IsAllocated()) return none; |
||||
|
||||
newRequest = SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); |
||||
diskQueue[diskQueue.length] = newRequest; |
||||
receivers[receivers.length] = receiver; |
||||
receiversLifeVersions[receiversLifeVersions.length] = receiver.GetLifeVersion(); |
||||
UpdateTickConnection(); |
||||
return newRequest; |
||||
} |
||||
|
||||
/// Returns amount of incomplete jobs are currently registered in the scheduler. |
||||
public function int GetJobsAmount() { |
||||
CleanCompletedJobs(); |
||||
return currentJobs.length; |
||||
} |
||||
|
||||
/// Returns amount of disk access requests are currently registered in the scheduler. |
||||
public function int GetDiskQueueSize() { |
||||
CleanDiskQueue(); |
||||
return diskQueue.length; |
||||
} |
||||
|
||||
/// Performs another batch of scheduled tasks. |
||||
/// |
||||
/// In case neither server, nor client core is registered, scheduler must be ticked manually. |
||||
/// For that call this method each separate tick (or whatever is your closest approximation |
||||
/// available for that). |
||||
/// Before manually invoking this method, you should check if scheduler actually started to tick |
||||
/// *automatically*. |
||||
/// Use `_.scheduler.IsAutomated()` for that. |
||||
/// |
||||
/// Argument is a time (real, not in-game one) that is supposedly passes from the moment |
||||
/// [`SchedulerApi::ManualTick()`] was called last time. |
||||
/// Used for tracking disk access cooldowns. |
||||
/// How [`SchedulerJob`]s are executed is independent from this value. |
||||
/// |
||||
/// Returns time (real, not in-game one) that is supposedly passes from the moment |
||||
/// [`SchedulerApi::ManualTick()`] was called last time. |
||||
/// |
||||
/// # Examples |
||||
/// |
||||
/// ``` |
||||
/// if (!_.scheduler.IsAutomated()) { |
||||
/// _.scheduler.ManualTick(0.05); |
||||
/// } |
||||
/// ``` |
||||
/// |
||||
/// # Note |
||||
/// |
||||
/// If neither server-/client- core is created, nor [`SchedulerApi::ManualTick()`] is invoked |
||||
/// manually, [`SchedulerApi`] won't actually do anything. |
||||
public final function ManualTick(optional float delta) { |
||||
Tick(delta, 1.0); |
||||
} |
||||
|
||||
/// Returns whether scheduler ticking automated. |
||||
/// |
||||
/// It can only be automated if either server or client level cores are created. |
||||
/// Scheduler can automatically enable automation and it cannot be prevented, but can be helped by |
||||
/// using [`SchedulerApi::UpdateTickConnection()`] method. |
||||
public function bool IsAutomated() { |
||||
return tickAvailable; |
||||
} |
||||
|
||||
/// Causes `SchedulerApi` to try automating itself by searching for level cores (checking if |
||||
/// server/client APIs are enabled). |
||||
public function UpdateTickConnection() { |
||||
local bool needsConnection; |
||||
local UnrealAPI api; |
||||
|
||||
if (!tickAvailable) { |
||||
if (_server.IsAvailable()) { |
||||
tickAvailable = true; |
||||
tickFromServer = true; |
||||
} |
||||
else if (_client.IsAvailable()) { |
||||
tickAvailable = true; |
||||
tickFromServer = false; |
||||
} |
||||
if (!tickAvailable) { |
||||
return; |
||||
} |
||||
} |
||||
needsConnection = (currentJobs.length > 0 || diskQueue.length > 0); |
||||
if (connectedToTick == needsConnection) { |
||||
return; |
||||
} |
||||
if (tickFromServer) { |
||||
api = _server.unreal; |
||||
} else { |
||||
api = _client.unreal; |
||||
} |
||||
if (connectedToTick && !needsConnection) { |
||||
api.OnTick(self).Disconnect(); |
||||
} else if (!connectedToTick && needsConnection) { |
||||
api.OnTick(self).connect = Tick; |
||||
} |
||||
connectedToTick = needsConnection; |
||||
} |
||||
|
||||
private function Tick(float delta, float dilationCoefficient) { |
||||
delta = delta / dilationCoefficient; |
||||
if (currentDiskCooldown > 0) { |
||||
currentDiskCooldown -= delta; |
||||
} |
||||
if (currentDiskCooldown <= 0 && diskQueue.length > 0) { |
||||
currentDiskCooldown = diskSaveCooldown; |
||||
ProcessDiskQueue(); |
||||
} |
||||
// Manage jobs |
||||
if (currentJobs.length > 0) { |
||||
ProcessJobs(); |
||||
} |
||||
UpdateTickConnection(); |
||||
} |
||||
|
||||
private function ProcessJobs() |
||||
{ |
||||
local int unitsPerJob; |
||||
local int jobsToPerform; |
||||
|
||||
CleanCompletedJobs(); |
||||
jobsToPerform = Min(currentJobs.length, maxJobsPerTick); |
||||
if (jobsToPerform <= 0) { |
||||
return; |
||||
} |
||||
unitsPerJob = maxWorkUnits / jobsToPerform; |
||||
while (jobsToPerform > 0) { |
||||
if (nextJobToPerform >= currentJobs.length) { |
||||
nextJobToPerform = 0; |
||||
} |
||||
currentJobs[nextJobToPerform].DoWork(unitsPerJob); |
||||
nextJobToPerform += 1; |
||||
jobsToPerform -= 1; |
||||
} |
||||
} |
||||
|
||||
private function ProcessDiskQueue() |
||||
{ |
||||
local int i; |
||||
|
||||
// Even if we clean disk queue here, we still need to double check |
||||
// lifetimes in the code below, since we have no idea what `.connect()` |
||||
// calls might do |
||||
CleanDiskQueue(); |
||||
if (diskQueue.length <= 0) { |
||||
return; |
||||
} |
||||
if (diskSaveCooldown > 0) { |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||
diskQueue[i].connect(); |
||||
} |
||||
_.memory.Free(diskQueue[0]); |
||||
diskQueue.Remove(0, 1); |
||||
receivers.Remove(0, 1); |
||||
receiversLifeVersions.Remove(0, 1); |
||||
return; |
||||
} |
||||
for (i = 0; i < diskQueue.length; i += 1) { |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||
diskQueue[i].connect(); |
||||
} |
||||
_.memory.Free(diskQueue[i]); |
||||
} |
||||
diskQueue.length = 0; |
||||
receivers.length = 0; |
||||
receiversLifeVersions.length = 0; |
||||
} |
||||
|
||||
// Removes completed jobs |
||||
private function CleanCompletedJobs() |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < currentJobs.length) { |
||||
if (currentJobs[i].IsCompleted()) { |
||||
if (i < nextJobToPerform) { |
||||
nextJobToPerform -= 1; |
||||
} |
||||
currentJobs[i].FreeSelf(); |
||||
currentJobs.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Remove disk requests with deallocated receivers |
||||
private function CleanDiskQueue() { |
||||
local int i; |
||||
|
||||
while (i < diskQueue.length) { |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||
i += 1; |
||||
continue; |
||||
} |
||||
_.memory.Free(diskQueue[i]); |
||||
diskQueue.Remove(i, 1); |
||||
receivers.Remove(i, 1); |
||||
receiversLifeVersions.Remove(i, 1); |
||||
} |
||||
} |
||||
|
||||
defaultproperties { |
||||
diskSaveCooldown = 0.25 |
||||
maxWorkUnits = 10000 |
||||
maxJobsPerTick = 5 |
||||
} |
@ -0,0 +1,31 @@
|
||||
/** |
||||
* 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 SchedulerDiskRequest extends AcediaObject; |
||||
|
||||
//! Slot-like object that represents a request for a writing disk access, capable of being scheduled |
||||
//! on the [`SchedulerApi`]. |
||||
|
||||
delegate connect() { |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,44 @@
|
||||
/** |
||||
* 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 SchedulerJob extends AcediaObject |
||||
abstract; |
||||
|
||||
//! Template object that represents a job, capable of being scheduled on the [`SchedulerAPI`]. |
||||
//! Use [`IsCompleted()`] to mark job as completed. |
||||
|
||||
/// Checks if caller [`SchedulerJob`] was completed. |
||||
/// |
||||
/// Returns `true` if [`SchedulerJob`] is already completed and doesn't need to be further executed |
||||
/// and `false` otherwise. |
||||
/// Once this method returns `true`, it shouldn't start returning `false` again. |
||||
public function bool IsCompleted(); |
||||
|
||||
/// Called when scheduler decides that [`SchedulerJob`] should be executed, taking amount of abstract |
||||
/// "work units" that it is allowed to spend for work. |
||||
/// |
||||
/// By default there is `10000` work units per second, so you can expect about 10000 / 1000 = 10 |
||||
/// work units per millisecond or, on servers with `30` tick rate, about `10000 * (30 / 1000) = 300` |
||||
/// work units per tick to be allotted to all the scheduled jobs. |
||||
public function DoWork(int allottedWorkUnits); |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,277 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,212 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,76 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,33 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,87 @@
|
||||
/** |
||||
* 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" |
||||
} |
@ -0,0 +1,29 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,49 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,28 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,35 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,41 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,48 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,30 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,28 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,32 @@
|
||||
/** |
||||
* 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 { |
||||
} |
@ -0,0 +1,432 @@
|
||||
/** |
||||
* 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.") |
||||
} |
@ -0,0 +1,411 @@
|
||||
/** |
||||
* 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 AcediaEnvironment extends AcediaObject |
||||
config(AcediaSystem); |
||||
|
||||
//! API for management of running `Feature`s and loaded packages. |
||||
//! |
||||
//! Instance of this class will be used by Acedia to manage resources available |
||||
//! from different packages like `Feature`s and such other etc.. |
||||
//! This is mostly necessary to implement Acedia loader (and, possibly, |
||||
//! its alternatives) that would load available packages and enable `Feature`s |
||||
//! admin wants to be enabled. |
||||
|
||||
var private bool acediaShutDown; |
||||
|
||||
var private array< class<_manifest> > availablePackages; |
||||
|
||||
var private array< class<Feature> > availableFeatures; |
||||
var private array<Feature> enabledFeatures; |
||||
var private array<int> enabledFeaturesLifeVersions; |
||||
|
||||
var private string manifestSuffix; |
||||
|
||||
var private const config bool debugMode; |
||||
|
||||
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered; |
||||
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled; |
||||
var private LoggerAPI.Definition warnFeatureAlreadyEnabled; |
||||
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled; |
||||
|
||||
var private SimpleSignal onShutdownSignal; |
||||
var private SimpleSignal onShutdownSystemSignal; |
||||
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal; |
||||
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal; |
||||
|
||||
protected function Constructor() { |
||||
// Always register our core package |
||||
RegisterPackage_S("AcediaCore"); |
||||
onShutdownSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); |
||||
onShutdownSystemSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal')); |
||||
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal( |
||||
_.memory.Allocate(class'Environment_FeatureEnabled_Signal')); |
||||
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal( |
||||
_.memory.Allocate(class'Environment_FeatureDisabled_Signal')); |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
_.memory.Free(onShutdownSignal); |
||||
_.memory.Free(onShutdownSystemSignal); |
||||
_.memory.Free(onFeatureEnabledSignal); |
||||
_.memory.Free(onFeatureDisabledSignal); |
||||
} |
||||
|
||||
/// Signal that will be emitted right before Acedia shuts down. |
||||
/// |
||||
/// At the point of emission all APIs should still exist and function. |
||||
/// |
||||
/// # Signature |
||||
/// |
||||
/// void <slot>() |
||||
public final /*signal*/ function SimpleSlot OnShutDown(AcediaObject receiver) { |
||||
return SimpleSlot(onShutdownSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/// Signal that will be emitted during Acedia shut down. |
||||
/// |
||||
/// System API will use it to clean up after themselves, so one shouldn't rely on using them. |
||||
/// |
||||
/// There is no reason to use this signal unless you're reimplementing one of the APIs. |
||||
/// Otherwise you probably want to use `OnShutDown()` signal instead. |
||||
/// |
||||
/// # Signature |
||||
/// |
||||
/// void <slot>() |
||||
public final /*signal*/ function SimpleSlot OnShutDownSystem(AcediaObject receiver) { |
||||
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/// Signal that will be emitted right after a new `Feature` is enabled and its `OnEnabled()` method |
||||
// was called. |
||||
/// |
||||
/// # Signature |
||||
/// |
||||
/// void <slot>(Feature enabledFeature) |
||||
/// @param enabledFeature `Feature` instance that was just enabled. |
||||
public final /*signal*/ function Environment_FeatureEnabled_Slot OnFeatureEnabled( |
||||
AcediaObject receiver |
||||
) { |
||||
return Environment_FeatureEnabled_Slot(onFeatureEnabledSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/// Signal that will be emitted right after when a `Feature` is disabled and its `OnDisabled()` |
||||
/// method was called. |
||||
/// |
||||
/// # Signature |
||||
/// |
||||
/// void <slot>(class<Feature> disabledFeatureClass) |
||||
/// @param disabledFeatureClass Class of the `Feature` instance that was just disabled. |
||||
public final /*signal*/ function Environment_FeatureDisabled_Slot OnFeatureDisabled( |
||||
AcediaObject receiver |
||||
) { |
||||
return Environment_FeatureDisabled_Slot(onFeatureEnabledSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/// Shuts AcediaCore down, performing all the necessary clean up. |
||||
public final function Shutdown() { |
||||
local LevelCore core; |
||||
|
||||
if (acediaShutDown) { |
||||
return; |
||||
} |
||||
DisableAllFeatures(); |
||||
onShutdownSignal.Emit(); |
||||
onShutdownSystemSignal.Emit(); |
||||
core = class'ServerLevelCore'.static.GetInstance(); |
||||
if (core != none) { |
||||
core.Destroy(); |
||||
} |
||||
core = class'ClientLevelCore'.static.GetInstance(); |
||||
if (core != none) { |
||||
core.Destroy(); |
||||
} |
||||
_.DropCoreAPI(); |
||||
acediaShutDown = true; |
||||
} |
||||
|
||||
/// Registers an Acedia package wit ha given name. |
||||
/// |
||||
/// Returns `true` if package was successfully registered, `false` if it either does not exist, |
||||
/// was already registered or [`packageName`] is `none`. |
||||
/// |
||||
/// # Errors |
||||
/// |
||||
/// Will log an error if the package has failed to get registered (it is either missing or not |
||||
/// an Acedia package). |
||||
public final function bool RegisterPackage(BaseText packageName) { |
||||
local class<_manifest> manifestClass; |
||||
|
||||
if (packageName == none) { |
||||
return false; |
||||
} |
||||
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy()); |
||||
manifestClass = class<_manifest>(DynamicLoadObject( |
||||
packageName.ToString() $ manifestSuffix, class'Class', true)); |
||||
if (manifestClass == none) { |
||||
_.logger.Auto(errNotRegistered).Arg(packageName.Copy()); |
||||
return false; |
||||
} |
||||
if (IsManifestRegistered(manifestClass)) { |
||||
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy()); |
||||
return false; |
||||
} |
||||
availablePackages[availablePackages.length] = manifestClass; |
||||
ReadManifest(manifestClass); |
||||
return true; |
||||
} |
||||
|
||||
/// Registers an Acedia package wit ha given name. |
||||
/// |
||||
/// Returns `true` if package was successfully registered, `false` if it either does not exist or |
||||
/// was already registered. |
||||
/// |
||||
/// # Errors |
||||
/// |
||||
/// Will log an error if the package has failed to get registered (it is either missing or not |
||||
/// an Acedia package). |
||||
public final function RegisterPackage_S(string packageName) { |
||||
local Text wrapper; |
||||
|
||||
wrapper = _.text.FromString(packageName); |
||||
RegisterPackage(wrapper); |
||||
_.memory.Free(wrapper); |
||||
} |
||||
|
||||
private final function bool IsManifestRegistered(class<_manifest> manifestClass) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < availablePackages.length; i += 1) { |
||||
if (manifestClass == availablePackages[i]) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function ReadManifest(class<_manifest> manifestClass) { |
||||
local int i; |
||||
|
||||
for (i = 0; i < manifestClass.default.features.length; i += 1) { |
||||
if (manifestClass.default.features[i] == none) { |
||||
continue; |
||||
} |
||||
manifestClass.default.features[i].static.LoadConfigs(); |
||||
availableFeatures[availableFeatures.length] = manifestClass.default.features[i]; |
||||
} |
||||
for (i = 0; i < manifestClass.default.testCases.length; i += 1) { |
||||
class'TestingService'.static.RegisterTestCase(manifestClass.default.testCases[i]); |
||||
} |
||||
} |
||||
|
||||
/// Returns `true` iff AcediaCore is running in the debug mode. |
||||
/// |
||||
/// AcediaCore's debug mode allows features to enable functionality that is only useful during |
||||
/// development. |
||||
/// Whether AcediaCore is running in a debug mode is decided at launch and cannot be changed. |
||||
public final function bool IsDebugging() { |
||||
return debugMode; |
||||
} |
||||
|
||||
/// Returns all packages registered in the caller [`AcediaEnvironment`]. |
||||
public final function array< class<_manifest> > GetAvailablePackages() { |
||||
return availablePackages; |
||||
} |
||||
|
||||
/// Returns all [`Feature`]s available in the caller `AcediaEnvironment`. |
||||
public final function array< class<Feature> > GetAvailableFeatures() { |
||||
return availableFeatures; |
||||
} |
||||
|
||||
/// Returns all currently enabled [`Feature`]s. |
||||
public final function array<Feature> GetEnabledFeatures() { |
||||
local int i; |
||||
|
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
enabledFeatures[i].NewRef(); |
||||
} |
||||
return enabledFeatures; |
||||
} |
||||
|
||||
// CleanRemove `Feature`s that got deallocated. |
||||
// This shouldn't happen unless someone messes up. |
||||
private final function CleanEnabledFeatures() |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < enabledFeatures.length) { |
||||
if (enabledFeatures[i].GetLifeVersion() != enabledFeaturesLifeVersions[i]) { |
||||
enabledFeatures.Remove(i, 1); |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Checks if `Feature` of given class is enabled. |
||||
/// |
||||
/// Even if If feature of class `featureClass` is enabled, it's not necessarily that the instance |
||||
/// you have reference to is enabled. |
||||
/// |
||||
/// Although unlikely, it is possible that someone spawned another instance of the same class that |
||||
/// isn't considered enabled. If you want to check whether some particular instance of given class |
||||
/// [`featureClass`] is enabled, use [`IsFeatureEnabled()`] method instead. |
||||
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) { |
||||
local int i; |
||||
|
||||
if (featureClass == none) { |
||||
return false; |
||||
} |
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
if (featureClass == enabledFeatures[i].class) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// Checks if given `Feature` instance is enabled. |
||||
/// |
||||
/// If you want to check if any instance instance of given class `classToCheck` is enabled |
||||
/// (and not [`feature`] specifically), use [`IsFeatureClassEnabled()`] method instead. |
||||
public final function bool IsFeatureEnabled(Feature feature) { |
||||
local int i; |
||||
|
||||
if (feature == none) return false; |
||||
if (!feature.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
if (feature == enabledFeatures[i]) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// Returns enabled `Feature` instance of the given class. |
||||
/// |
||||
/// Returns `none` only if `featureClass` is not enabled (or also `none`). |
||||
public final function Feature GetEnabledFeature(class<Feature> featureClass) { |
||||
local int i; |
||||
if (featureClass == none) { |
||||
return none; |
||||
} |
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
if (featureClass == enabledFeatures[i].class) { |
||||
enabledFeatures[i].NewRef(); |
||||
return enabledFeatures[i]; |
||||
} |
||||
} |
||||
return none; |
||||
} |
||||
|
||||
/// Enables given `Feature` instance `newEnabledFeature` with a given config. |
||||
/// Does not change a config for already enabled feature, failing instead. |
||||
/// |
||||
/// Returns `true` if given `newEnabledFeature` was enabled and `false` otherwise |
||||
/// (including if feature of the same class has already been enabled). |
||||
public final function bool EnableFeature(Feature newEnabledFeature, optional BaseText configName) { |
||||
local int i; |
||||
|
||||
if (newEnabledFeature == none) return false; |
||||
if (!newEnabledFeature.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
if (newEnabledFeature.class == enabledFeatures[i].class) { |
||||
if (newEnabledFeature == enabledFeatures[i]) { |
||||
_.logger |
||||
.Auto(warnFeatureAlreadyEnabled) |
||||
.Arg(_.text.FromClass(newEnabledFeature.class)); |
||||
} |
||||
else { |
||||
_.logger |
||||
.Auto(errFeatureClassAlreadyEnabled) |
||||
.Arg(_.text.FromClass(newEnabledFeature.class)); |
||||
} |
||||
return false; |
||||
} |
||||
} |
||||
newEnabledFeature.NewRef(); |
||||
enabledFeatures[enabledFeatures.length] = newEnabledFeature; |
||||
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] = |
||||
newEnabledFeature.GetLifeVersion(); |
||||
newEnabledFeature.EnableInternal(configName); |
||||
onFeatureEnabledSignal.Emit(newEnabledFeature); |
||||
return true; |
||||
} |
||||
|
||||
/// Disables given `Feature` instance `featureToDisable`. |
||||
/// |
||||
/// Returns `true` if given `newEnabledFeature` was disabled and `false` otherwise |
||||
/// (including if it already was disabled). |
||||
public final function bool DisableFeature(Feature featureToDisable) { |
||||
local int i; |
||||
|
||||
if (featureToDisable == none) return false; |
||||
if (!featureToDisable.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
if (featureToDisable == enabledFeatures[i]) { |
||||
enabledFeatures.Remove(i, 1); |
||||
enabledFeaturesLifeVersions.Remove(i, 1); |
||||
featureToDisable.DisableInternal(); |
||||
onFeatureDisabledSignal.Emit(featureToDisable.class); |
||||
_.memory.Free(featureToDisable); |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/// Disables all currently enabled `Feature`s. |
||||
/// |
||||
/// Mainly intended for the clean up when Acedia shuts down. |
||||
public final function DisableAllFeatures() { |
||||
local int i; |
||||
local array<Feature> featuresCopy; |
||||
|
||||
CleanEnabledFeatures(); |
||||
featuresCopy = enabledFeatures; |
||||
enabledFeatures.length = 0; |
||||
enabledFeaturesLifeVersions.length = 0; |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
featuresCopy[i].DisableInternal(); |
||||
onFeatureDisabledSignal.Emit(featuresCopy[i].class); |
||||
} |
||||
_.memory.FreeMany(featuresCopy); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
manifestSuffix = ".Manifest" |
||||
debugMode = false |
||||
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") |
||||
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") |
||||
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.") |
||||
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.") |
||||
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.") |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* 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 Environment_FeatureDisabled_Slot extends Slot; |
||||
|
||||
delegate connect(class<Feature> disabledFeatureClass) { |
||||
DummyCall(); |
||||
} |
||||
|
||||
protected function Constructor() { |
||||
connect = none; |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
super.Finalizer(); |
||||
connect = none; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,38 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* 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 Environment_FeatureEnabled_Slot extends Slot; |
||||
|
||||
delegate connect(Feature enabledFeature) { |
||||
DummyCall(); |
||||
} |
||||
|
||||
protected function Constructor() { |
||||
connect = none; |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
super.Finalizer(); |
||||
connect = none; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,132 @@
|
||||
/** |
||||
* Author: dkanus |
||||
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||
* License: GPL |
||||
* Copyright 2020-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 Global extends Object; |
||||
|
||||
//! Class singleton instance of an object that would hold references to any API that do not depend |
||||
//! on [`Actor`]s. |
||||
//! |
||||
//! To overcome cumbersome syntax of UnrealScript we gather all functions we want to be global into |
||||
//! special "API objects" and store their single references inside this one, [`Global`]'s instance. |
||||
//! Providing reference to properly initialized [`Global`] object to all [`AcediaObject`]s and |
||||
//! [`AcediaActor`]s will give them convenient accessors to all Acedia API. |
||||
//! |
||||
//! [`Global`] is expected to behave like a singleton and will store its main instance in this |
||||
//! variable's default value. |
||||
|
||||
// For getting instance of [`Global`] from any object. |
||||
var protected Global myself; |
||||
|
||||
var public UnflectApi unflect; |
||||
var public SideEffectAPI sideEffects; |
||||
var public RefAPI ref; |
||||
var public BoxAPI box; |
||||
var public MathAPI math; |
||||
var public LoggerAPI logger; |
||||
var public CollectionsAPI collections; |
||||
var public AliasesAPI alias; |
||||
var public TextAPI text; |
||||
var public MemoryAPI memory; |
||||
var public ConsoleAPI console; |
||||
var public ChatAPI chat; |
||||
var public ColorAPI color; |
||||
var public UserAPI users; |
||||
var public PlayersAPI players; |
||||
var public JsonAPI json; |
||||
var public SchedulerAPI scheduler; |
||||
var public CommandAPI commands; |
||||
var public AvariceAPI avarice; |
||||
|
||||
var public AcediaEnvironment environment; |
||||
|
||||
/// Returns instance of the [`Global`] object. |
||||
/// |
||||
/// [`Global`] is supposed to be used as a singleton, meaning that only one instance of it should be |
||||
/// created. |
||||
/// This method creates and returns such instance. |
||||
/// In case it was already created, that instance will be returned from now one. |
||||
public final static function Global GetInstance() { |
||||
if (default.myself == none) { |
||||
// `...Global`s are special and exist outside main Acedia's object infrastructure, |
||||
//so we allocate it without using [`MemoryAPI`] methods. |
||||
default.myself = new class'Global'; |
||||
default.myself.Initialize(); |
||||
} |
||||
return default.myself; |
||||
} |
||||
|
||||
/// Initializes [`Global`] by creating all API from base realm. |
||||
protected function Initialize() { |
||||
// Special case that we cannot spawn with memory API since it obviously |
||||
// does not exist yet! |
||||
memory = new class'MemoryAPI'; |
||||
memory._constructor(); |
||||
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI` |
||||
sideEffects = SideEffectAPI(memory.Allocate(class'SideEffectAPI')); |
||||
ref = RefAPI(memory.Allocate(class'RefAPI')); |
||||
box = BoxAPI(memory.Allocate(class'BoxAPI')); |
||||
text = TextAPI(memory.Allocate(class'TextAPI')); |
||||
math = MathAPI(memory.Allocate(class'MathAPI')); |
||||
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); |
||||
unflect = UnflectAPI(memory.Allocate(class'UnflectAPI')); |
||||
json = JsonAPI(memory.Allocate(class'JsonAPI')); |
||||
logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); |
||||
color = ColorAPI(memory.Allocate(class'ColorAPI')); |
||||
alias = AliasesAPI(memory.Allocate(class'AliasesAPI')); |
||||
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI')); |
||||
chat = ChatAPI(memory.Allocate(class'ChatAPI')); |
||||
users = UserAPI(memory.Allocate(class'UserAPI')); |
||||
players = PlayersAPI(memory.Allocate(class'PlayersAPI')); |
||||
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); |
||||
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); |
||||
commands = CommandAPI(memory.Allocate(class'CommandAPI')); |
||||
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); |
||||
} |
||||
|
||||
/// Drops references to all API from base realm, including self-reference, previously returned by |
||||
/// [`Global::GetInstance()`] method. |
||||
public function DropCoreAPI() { |
||||
unflect._drop(); |
||||
memory = none; |
||||
unflect = none; |
||||
sideEffects = none; |
||||
ref = none; |
||||
box = none; |
||||
text = none; |
||||
math = none; |
||||
collections = none; |
||||
logger = none; |
||||
alias = none; |
||||
console = none; |
||||
chat = none; |
||||
color = none; |
||||
users = none; |
||||
players = none; |
||||
json = none; |
||||
scheduler = none; |
||||
commands = none; |
||||
avarice = none; |
||||
environment = none; |
||||
default.myself = none; |
||||
} |
||||
|
||||
defaultproperties { |
||||
} |
@ -0,0 +1,68 @@
|
||||
/** |
||||
* 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 Iter extends AcediaObject |
||||
abstract; |
||||
|
||||
//! Base class for iterator, an auxiliary object for iterating through |
||||
//! a set of objects obtained from some context-dependent source. |
||||
|
||||
/// Status of the [`Iter`]'s filter regarding some specific property. |
||||
/// |
||||
/// [`Iter`]s can filter objects they're iterating on by the presence or lack of a certain property, |
||||
/// recording this choice requires 3 values (for requiring having/not having a certain property and |
||||
/// for ignoring it). |
||||
/// This enumeration is for inner purposes and is there to unify iterator implementations. |
||||
enum IterFilter { |
||||
/// We don't use relevant property for filtering |
||||
IF_Nothing, |
||||
/// Iterated objects must have that property |
||||
IF_Have, |
||||
/// Iterated objects must not have that property |
||||
IF_NotHave |
||||
}; |
||||
|
||||
/// Advances iterator to the next item. |
||||
/// |
||||
/// Makes iterator refer to the next item from the source and returns `true`, as long as the source |
||||
/// of items isn't yet exhausted. |
||||
/// In case there's no more items, method has to return `false` and do nothing else. |
||||
/// [`Iter::HasFinished()`] can also be used to check whether there are more items available. |
||||
public function bool Next(); |
||||
|
||||
/// Returns value currently pointed to by an iterator. |
||||
/// |
||||
/// Does not advance iteration: use [`Iter::Next()`] to pick next value. |
||||
/// |
||||
/// In case iterator has already reached the end (and [`Iter::Next()``] returned `false`), this |
||||
/// method will return `none`. |
||||
/// Note that depending on context `none` values can also be returned, use |
||||
/// [`Iter::LeaveOnlyNotNone()`] method to prevent that. |
||||
public function AcediaObject Get(); |
||||
|
||||
/// Checks if caller [`Iter`] has finished iterating. |
||||
public function bool HasFinished(); |
||||
|
||||
/// Makes caller iterator skip any `none` items during iteration. |
||||
public function LeaveOnlyNotNone(); |
||||
|
||||
defaultproperties { |
||||
} |
@ -1,839 +0,0 @@
|
||||
/** |
||||
* A simple big integer implementation, mostly to allow Acedia's databases to |
||||
* store integers of arbitrary size. |
||||
* 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 BigInt extends AcediaObject |
||||
dependson(MathAPI); |
||||
|
||||
/** |
||||
* # `BigInt` |
||||
* |
||||
* A simple big integer implementation, mostly to allow Acedia's databases to |
||||
* store integers of arbitrary size. It can be used for long arithmetic |
||||
* computations, but it was mainly meant as a players' statistics counter and, |
||||
* therefore, not optimized for performing large amount of operations. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* `BigInt` can be created from both `int` and decimal `BaseText`/`string` |
||||
* representation, preferably by `MathAPI` (`_.math.`) methods |
||||
* `ToBigInt()`/`MakeBigInt()`. |
||||
* Then it can be combined either directly with other `BigInt` or with |
||||
* `int`/`BaseText`/`string` through available arithmetic operations. |
||||
* To make use of stored value one can convert it back into either `int` or |
||||
* decimal `BaseText`/`string` representation. |
||||
* Newly allocated `BigInt` is guaranteed to hold `0` as value. |
||||
*/ |
||||
|
||||
/** |
||||
* `BigInt` data as a `struct` - meant to be used to store `BigInt`'s values |
||||
* inside the local databases. |
||||
*/ |
||||
struct BigIntData |
||||
{ |
||||
var bool negative; |
||||
var array<byte> digits; |
||||
}; |
||||
|
||||
/** |
||||
* Used to represent a result of comparison for `BigInt`s with each other. |
||||
*/ |
||||
enum BigIntCompareResult |
||||
{ |
||||
BICR_Less, |
||||
BICR_Equal, |
||||
BICR_Greater |
||||
}; |
||||
|
||||
// Does stored `BigInt` has negative sign? |
||||
var private bool negative; |
||||
// Digits array, from least to most significant. For example, for 13524: |
||||
// `digits[0] = 4` |
||||
// `digits[1] = 2` |
||||
// `digits[2] = 5` |
||||
// `digits[3] = 3` |
||||
// `digits[4] = 1` |
||||
// Valid `BigInt` should not have this array empty: zero should be |
||||
// represented by an array with a single `0`-element. |
||||
// This isn't a most efficient representation for `BigInt`, but it's easy |
||||
// to convert to and from decimal representation. |
||||
// INVARIANT: this array must not have leading (in the sense of significance) |
||||
// zeroes. That is, last element of the array should not be a `0`. The only |
||||
// exception if if stored value is `0`, then `digits` must consist of a single |
||||
// `0` element. |
||||
var private array<byte> digits; |
||||
|
||||
// Constants useful for converting `BigInt` back to `int`, while avoiding |
||||
// overflow. |
||||
// We can add less digits than that without any fear of overflow |
||||
const DIGITS_IN_MAX_INT = 10; |
||||
// Maximum `int` value is `2147483647`, so in case most significant digit |
||||
// is 10th and is `2` (so number has a form of "2xxxxxxxxx"), to check for |
||||
// overflow we only need to compare combination of the rest of the digits with |
||||
// this constant. |
||||
const ALMOST_MAX_INT = 147483647; |
||||
// To add last digit we add/subtract that digit multiplied by this value. |
||||
const LAST_DIGIT_ORDER = 1000000000; |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
SetZero(); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
negative = false; |
||||
digits.length = 0; |
||||
} |
||||
|
||||
// Auxiliary method to set current value to zero |
||||
private function BigInt SetZero() |
||||
{ |
||||
negative = false; |
||||
digits.length = 1; |
||||
digits[0] = 0; |
||||
return self; |
||||
} |
||||
|
||||
// Minimal `int` value `-2,147,483,648` is somewhat of a pain to handle, so |
||||
// just use this auxiliary pre-made constructor for it |
||||
private function BigInt SetMinimalNegative() |
||||
{ |
||||
negative = true; |
||||
digits.length = 10; |
||||
digits[0] = 8; |
||||
digits[1] = 4; |
||||
digits[2] = 6; |
||||
digits[3] = 3; |
||||
digits[4] = 8; |
||||
digits[5] = 4; |
||||
digits[6] = 7; |
||||
digits[7] = 4; |
||||
digits[8] = 1; |
||||
digits[9] = 2; |
||||
return self; |
||||
} |
||||
|
||||
// Removes unnecessary zeroes from leading digit positions `digits`. |
||||
// Does not change contained value. |
||||
private final function TrimLeadingZeroes() |
||||
{ |
||||
local int i, zeroesToRemove; |
||||
|
||||
// Find how many leading zeroes there is. |
||||
// Since `digits` stores digits from least to most significant, we need |
||||
// to check from the end of `digits` array. |
||||
for (i = digits.length - 1; i >= 0; i -= 1) |
||||
{ |
||||
if (digits[i] != 0) { |
||||
break; |
||||
} |
||||
zeroesToRemove += 1; |
||||
} |
||||
// `digits` must not be empty, enforce `0` value in that case |
||||
if (zeroesToRemove >= digits.length) { |
||||
SetZero(); |
||||
} |
||||
else { |
||||
digits.length = digits.length - zeroesToRemove; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Changes current value of `BigInt` to given `BigInt` value. |
||||
* |
||||
* @param value New value of the caller `BigInt`. If `none` is given, |
||||
* method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public final function BigInt Set(BigInt value) |
||||
{ |
||||
if (value == none) { |
||||
return self; |
||||
} |
||||
value.TrimLeadingZeroes(); |
||||
digits = value.digits; |
||||
negative = value.negative; |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Changes current value of `BigInt` to given `int` value `value`. |
||||
* |
||||
* Cannot fail. |
||||
* |
||||
* @param value New value of the caller `BigInt`. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public final function BigInt SetInt(int value) |
||||
{ |
||||
local MathAPI.IntegerDivisionResult divisionResult; |
||||
|
||||
negative = false; |
||||
digits.length = 0; |
||||
if (value < 0) |
||||
{ |
||||
// Treat special case of minimal `int` value `-2,147,483,648` that |
||||
// won't fit into positive `int` as special and use pre-made |
||||
// specialized constructor `CreateMinimalNegative()` |
||||
if (value < -MaxInt) { |
||||
return SetMinimalNegative(); |
||||
} |
||||
else |
||||
{ |
||||
negative = true; |
||||
value *= -1; |
||||
} |
||||
} |
||||
if (value == 0) { |
||||
digits[0] = 0; |
||||
} |
||||
else |
||||
{ |
||||
while (value > 0) |
||||
{ |
||||
divisionResult = __().math.IntegerDivision(value, 10); |
||||
value = divisionResult.quotient; |
||||
digits[digits.length] = divisionResult.remainder; |
||||
} |
||||
} |
||||
TrimLeadingZeroes(); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Changes current value of `BigInt` to the value, given by decimal |
||||
* representation inside `value` argument. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param value New value of the caller `BigInt`, given by decimal |
||||
* its representation. If `none` is given, method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public final function BigInt SetDecimal(BaseText value) |
||||
{ |
||||
local int i; |
||||
local byte nextDigit; |
||||
local Parser parser; |
||||
local Basetext.Character nextCharacter; |
||||
|
||||
if (value == none) { |
||||
return none; |
||||
} |
||||
parser = value.Parse(); |
||||
negative = parser.Match(P("-")).Ok(); |
||||
if (!parser.Ok()) { |
||||
parser.R().Match(P("+")).Ok(); |
||||
} |
||||
// Reset to valid state whether sign was consumed or not |
||||
parser.Confirm(); |
||||
parser.R(); |
||||
// Reset current value |
||||
digits.length = 0; |
||||
digits.length = parser.GetRemainingLength(); |
||||
// Parse new one |
||||
i = digits.length - 1; |
||||
while (!parser.HasFinished()) |
||||
{ |
||||
// This should not happen, but just in case |
||||
if (i < 0) { |
||||
break; |
||||
} |
||||
parser.MCharacter(nextCharacter); |
||||
nextDigit = Clamp(__().text.CharacterToInt(nextCharacter), 0, 9); |
||||
digits[i] = nextDigit; |
||||
i -= 1; |
||||
} |
||||
parser.FreeSelf(); |
||||
TrimLeadingZeroes(); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Changes current value of `BigInt` to the value, given by decimal |
||||
* representation inside `value` argument. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param value New value of the caller `BigInt`, given by decimal |
||||
* its representation. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public final function BigInt SetDecimal_S(string value) |
||||
{ |
||||
local MutableText wrapper; |
||||
|
||||
wrapper = __().text.FromStringM(value); |
||||
SetDecimal(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return self; |
||||
} |
||||
|
||||
// Auxiliary method for comparing two `BigInt`s by their absolute value. |
||||
private function BigIntCompareResult _compareAbsolute(BigInt other) |
||||
{ |
||||
local int i; |
||||
local array<byte> otherDigits; |
||||
|
||||
otherDigits = other.digits; |
||||
if (digits.length == otherDigits.length) |
||||
{ |
||||
for (i = digits.length - 1; i >= 0; i -= 1) |
||||
{ |
||||
if (digits[i] < otherDigits[i]) { |
||||
return BICR_Less; |
||||
} |
||||
if (digits[i] > otherDigits[i]) { |
||||
return BICR_Greater; |
||||
} |
||||
} |
||||
return BICR_Equal; |
||||
} |
||||
if (digits.length < otherDigits.length) { |
||||
return BICR_Less; |
||||
} |
||||
return BICR_Greater; |
||||
} |
||||
|
||||
/** |
||||
* Compares caller `BigInt` to `other`. |
||||
* |
||||
* @param other Value to compare the caller `BigInt`. |
||||
* If given reference is `none` - behavior is undefined. |
||||
* @return `BigIntCompareResult` representing the result of comparison. |
||||
* Returned value describes how caller `BigInt` relates to the `other`, |
||||
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is |
||||
* smaller that `other`. |
||||
*/ |
||||
public function BigIntCompareResult Compare(BigInt other) |
||||
{ |
||||
local BigIntCompareResult resultForModulus; |
||||
|
||||
if (other == none) { |
||||
return BICR_Less; |
||||
} |
||||
if (negative && !other.negative) { |
||||
return BICR_Less; |
||||
} |
||||
if (!negative && other.negative) { |
||||
return BICR_Greater; |
||||
} |
||||
resultForModulus = _compareAbsolute(other); |
||||
if (resultForModulus == BICR_Equal) { |
||||
return BICR_Equal; |
||||
} |
||||
if ( (negative && (resultForModulus == BICR_Greater)) |
||||
|| (!negative && (resultForModulus == BICR_Less)) ) |
||||
{ |
||||
return BICR_Less; |
||||
} |
||||
return BICR_Greater; |
||||
} |
||||
|
||||
/** |
||||
* Compares caller `BigInt` to `other`. |
||||
* |
||||
* @param other Value to compare the caller `BigInt`. |
||||
* @return `BigIntCompareResult` representing the result of comparison. |
||||
* Returned value describes how caller `BigInt` relates to the `other`, |
||||
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is |
||||
* smaller that `other`. |
||||
*/ |
||||
public function BigIntCompareResult CompareInt(int other) |
||||
{ |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.ToBigInt(other); |
||||
result = Compare(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Compares caller `BigInt` to `other`. |
||||
* |
||||
* @param other Value to compare the caller `BigInt`. |
||||
* If given reference is `none` - behavior is undefined. |
||||
* @return `BigIntCompareResult` representing the result of comparison. |
||||
* Returned value describes how caller `BigInt` relates to the `other`, |
||||
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is |
||||
* smaller that `other`. |
||||
*/ |
||||
public function BigIntCompareResult CompareDecimal(BaseText other) |
||||
{ |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.MakeBigInt(other); |
||||
result = Compare(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Compares caller `BigInt` to `other`. |
||||
* |
||||
* @param other Value to compare the caller `BigInt`. |
||||
* If given value contains invalid decimal value - behavior is undefined. |
||||
* @return `BigIntCompareResult` representing the result of comparison. |
||||
* Returned value describes how caller `BigInt` relates to the `other`, |
||||
* e.g. if `BICR_Less` was returned - it means that caller `BigInt` is |
||||
* smaller that `other`. |
||||
*/ |
||||
public function BigIntCompareResult CompareDecimal_S(string other) |
||||
{ |
||||
local BigInt wrapper; |
||||
local BigIntCompareResult result; |
||||
|
||||
wrapper = _.math.MakeBigInt_S(other); |
||||
result = Compare(wrapper); |
||||
wrapper.FreeSelf(); |
||||
return result; |
||||
} |
||||
|
||||
// Adds absolute values of caller `BigInt` and `other` with no changes to |
||||
// the sign |
||||
private function _add(BigInt other) |
||||
{ |
||||
local int i; |
||||
local byte carry, digitSum; |
||||
local array<byte> otherDigits; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
otherDigits = other.digits; |
||||
if (digits.length < otherDigits.length) { |
||||
digits.length = otherDigits.length; |
||||
} |
||||
else { |
||||
otherDigits.length = digits.length; |
||||
} |
||||
carry = 0; |
||||
for (i = 0; i < digits.length; i += 1) |
||||
{ |
||||
digitSum = digits[i] + otherDigits[i] + carry; |
||||
digits[i] = _.math.Remainder(digitSum, 10); |
||||
carry = (digitSum - digits[i]) / 10; |
||||
} |
||||
if (carry > 0) { |
||||
digits[digits.length] = carry; |
||||
} |
||||
// No leading zeroes can be created here, so no need to trim |
||||
} |
||||
|
||||
// Subtracts absolute value of `other` from the caller `BigInt`, flipping |
||||
// caller's sign in case `other`'s absolute value is bigger. |
||||
private function _sub(BigInt other) |
||||
{ |
||||
local int i; |
||||
local int carry, nextDigit; |
||||
local array<byte> minuendDigits, subtrahendDigits; |
||||
local BigIntCompareResult resultForModulus; |
||||
|
||||
if (other == none) { |
||||
return; |
||||
} |
||||
resultForModulus = _compareAbsolute(other); |
||||
if (resultForModulus == BICR_Equal) |
||||
{ |
||||
SetZero(); |
||||
return; |
||||
} |
||||
if (resultForModulus == BICR_Less) |
||||
{ |
||||
negative = !negative; |
||||
minuendDigits = other.digits; |
||||
subtrahendDigits = digits; |
||||
} |
||||
else |
||||
{ |
||||
minuendDigits = digits; |
||||
subtrahendDigits = other.digits; |
||||
} |
||||
digits.length = minuendDigits.length; |
||||
subtrahendDigits.length = minuendDigits.length; |
||||
carry = 0; |
||||
for (i = 0; i < digits.length; i += 1) |
||||
{ |
||||
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry; |
||||
if (nextDigit < 0) |
||||
{ |
||||
nextDigit += 10; |
||||
carry = -1; |
||||
} |
||||
else { |
||||
carry = 0; |
||||
} |
||||
digits[i] = nextDigit; |
||||
} |
||||
TrimLeadingZeroes(); |
||||
} |
||||
|
||||
/** |
||||
* Adds `other` value to the caller `BigInt`. |
||||
* |
||||
* @param other Value to add. If `none` is given method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt Add(BigInt other) |
||||
{ |
||||
if (other == none) { |
||||
return self; |
||||
} |
||||
if (negative == other.negative) { |
||||
_add(other); |
||||
} |
||||
else { |
||||
_sub(other); |
||||
} |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Adds `other` value to the caller `BigInt`. |
||||
* |
||||
* Cannot fail. |
||||
* |
||||
* @param other Value to add. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt AddInt(int other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.ToBigInt(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Adds `other` value to the caller `BigInt`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param other Value to add. If `none` is given, method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt AddDecimal(BaseText other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
if (other == none) { |
||||
return self; |
||||
} |
||||
otherObject = _.math.MakeBigInt(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Adds `other` value to the caller `BigInt`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param other Value to add. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt AddDecimal_S(string other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.MakeBigInt_S(other); |
||||
Add(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Subtracts `other` value to the caller `BigInt`. |
||||
* |
||||
* @param other Value to subtract. If `none` is given method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt Subtract(BigInt other) |
||||
{ |
||||
if (negative != other.negative) { |
||||
_add(other); |
||||
} |
||||
else { |
||||
_sub(other); |
||||
} |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Subtracts `other` value to the caller `BigInt`. |
||||
* |
||||
* Cannot fail. |
||||
* |
||||
* @param other Value to subtract. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt SubtractInt(int other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.ToBigInt(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Subtracts `other` value to the caller `BigInt`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param other Value to subtract. If `none`, method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt SubtractDecimal(BaseText other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
if (other == none) { |
||||
return self; |
||||
} |
||||
otherObject = _.math.MakeBigInt(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Subtracts `other` value to the caller `BigInt`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - behavior is undefined. Otherwise cannot fail. |
||||
* |
||||
* @param other Value to subtract. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt SubtractDecimal_S(string other) |
||||
{ |
||||
local BigInt otherObject; |
||||
|
||||
otherObject = _.math.MakeBigInt_S(other); |
||||
Subtract(otherObject); |
||||
_.memory.Free(otherObject); |
||||
return self; |
||||
} |
||||
|
||||
/** |
||||
* Checks if caller `BigInt` is negative. Zero is not considered negative |
||||
* number. |
||||
* |
||||
* @return `true` if stored value is negative (`< 0`) and `false` otherwise |
||||
* (`>= 0`). |
||||
*/ |
||||
public function bool IsNegative() |
||||
{ |
||||
// Handle special case of zero first (it ignores `negative` flag) |
||||
if (digits.length == 1 && digits[0] == 0) { |
||||
return false; |
||||
} |
||||
return negative; |
||||
} |
||||
|
||||
/** |
||||
* Converts caller `BigInt` into `int` representation. |
||||
* |
||||
* In case stored value is outside `int`'s value range |
||||
* (`[-MaxInt-1, MaxInt] == [-2147483648; 2147483647]`), |
||||
* method returns either maximal or minimal possible value, depending on |
||||
* the `BigInt`'s sign. |
||||
* |
||||
* @return `int` representation of the caller `BigInt`, clamped into available |
||||
* `int` value range. |
||||
*/ |
||||
public function int ToInt() |
||||
{ |
||||
local int i; |
||||
local int accumulator; |
||||
local int safeDigitsAmount; |
||||
|
||||
if (digits.length <= 0) { |
||||
return 0; |
||||
} |
||||
if (digits.length > DIGITS_IN_MAX_INT) |
||||
{ |
||||
if (negative) { |
||||
return (-MaxInt - 1); |
||||
} |
||||
else { |
||||
return MaxInt; |
||||
} |
||||
} |
||||
// At most `DIGITS_IN_MAX_INT - 1` iterations |
||||
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length); |
||||
for (i = safeDigitsAmount - 1; i >= 0; i -= 1) |
||||
{ |
||||
accumulator *= 10; |
||||
accumulator += digits[i]; |
||||
} |
||||
if (negative) { |
||||
accumulator *= -1; |
||||
} |
||||
accumulator = AddUnsafeDigitToInt(accumulator); |
||||
return accumulator; |
||||
} |
||||
|
||||
// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but |
||||
// adding the next digit can, so we need to handle it differently and more |
||||
// carefully. |
||||
// Assumes `digits.length <= DIGITS_IN_MAX_INT`. |
||||
private function int AddUnsafeDigitToInt(int accumulator) |
||||
{ |
||||
local int unsafeDigit; |
||||
local bool noOverflow; |
||||
|
||||
if (digits.length < DIGITS_IN_MAX_INT) { |
||||
return accumulator; |
||||
} |
||||
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1]; |
||||
// `MaxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`, |
||||
// there is no overflow, otherwise - check rest of the digits |
||||
noOverflow = (unsafeDigit < 2); |
||||
if (unsafeDigit == 2) |
||||
{ |
||||
// Include `MaxInt` and `-MaxInt-1` (minimal possible value) into |
||||
// an overflow too - this way we still give a correct result, but do |
||||
// not have to worry about `int`-arithmetic error |
||||
noOverflow = noOverflow |
||||
|| (negative && (accumulator > -ALMOST_MAX_INT - 1)) |
||||
|| (!negative && (accumulator < ALMOST_MAX_INT)); |
||||
} |
||||
if (noOverflow) |
||||
{ |
||||
if (negative) { |
||||
accumulator -= unsafeDigit * LAST_DIGIT_ORDER; |
||||
} |
||||
else { |
||||
accumulator += unsafeDigit * LAST_DIGIT_ORDER; |
||||
} |
||||
return accumulator; |
||||
} |
||||
// Handle overflow |
||||
if (negative) { |
||||
return (-MaxInt - 1); |
||||
} |
||||
return MaxInt; |
||||
} |
||||
|
||||
/** |
||||
* Converts caller `BigInt` into `Text` representation. |
||||
* |
||||
* @return `Text` representation of the caller `BigInt`. |
||||
*/ |
||||
public function Text ToText() |
||||
{ |
||||
return ToText_M().IntoText(); |
||||
} |
||||
|
||||
/** |
||||
* Converts caller `BigInt` into `MutableText` representation. |
||||
* |
||||
* @return `MutableText` representation of the caller `BigInt`. |
||||
*/ |
||||
public function MutableText ToText_M() |
||||
{ |
||||
local int i; |
||||
local MutableText result; |
||||
|
||||
result = _.text.Empty(); |
||||
if (negative) { |
||||
result.AppendCharacter(_.text.GetCharacter("-")); |
||||
} |
||||
for (i = digits.length - 1; i >= 0; i -= 1) { |
||||
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48)); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Converts caller `BigInt` into `string` representation. |
||||
* |
||||
* @return `string` representation of the caller `BigInt`. |
||||
*/ |
||||
public function string ToString() |
||||
{ |
||||
local int i; |
||||
local string result; |
||||
|
||||
if (negative) { |
||||
result = "-"; |
||||
} |
||||
for (i = digits.length - 1; i >= 0; i -= 1) { |
||||
result = result $ digits[i]; |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Restores `BigInt` from the `BigIntData` value. |
||||
* |
||||
* This method is created to make an efficient way to store `BigInt` inside |
||||
* local databases. |
||||
* |
||||
* @param data Data to read new caller `BigInt`'s value from. |
||||
*/ |
||||
public function FromData(BigIntData data) |
||||
{ |
||||
local int i; |
||||
|
||||
negative = data.negative; |
||||
digits = data.digits; |
||||
// Deal with possibly erroneous data |
||||
for (i = 0; i < digits.length; i += 1) { |
||||
if (digits[i] > 9) { |
||||
digits[i] = 9; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Converts caller `BigInt`'s value into `BigIntData`. |
||||
* |
||||
* This method is created to make an efficient way to store `BigInt` inside |
||||
* local databases. |
||||
* |
||||
* @return Value of the caller `BigInt` in the `struct` form. |
||||
*/ |
||||
public function BigIntData ToData() |
||||
{ |
||||
local BigIntData result; |
||||
|
||||
result.negative = negative; |
||||
result.digits = digits; |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,131 +0,0 @@
|
||||
/** |
||||
* API that provides a collection of non-built in math methods used in Acedia. |
||||
* 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 MathAPI extends AcediaObject; |
||||
|
||||
/** |
||||
* For storing result of integer division. |
||||
* |
||||
* If we divide `number` by `divisor`, then |
||||
* `number = divisor * quotient + remainder` |
||||
*/ |
||||
struct IntegerDivisionResult |
||||
{ |
||||
var int quotient; |
||||
var int remainder; |
||||
}; |
||||
|
||||
/** |
||||
* Changes current value of `BigInt` to given `BigInt` value. |
||||
* |
||||
* @param value New value of the caller `BigInt`. If `none` is given, |
||||
* method does nothing. |
||||
* @return Self-reference to allow for method chaining. |
||||
*/ |
||||
public function BigInt ToBigInt(int value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
return result.SetInt(value); |
||||
} |
||||
|
||||
/** |
||||
* Creates new `BigInt` value, base on the decimal representation given by |
||||
* `value`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - contents of returned value are undefined. Otherwise cannot fail. |
||||
* |
||||
* @param value New value of the caller `BigInt`, given by decimal |
||||
* its representation. If `none` is given, method returns `BigInt` |
||||
* containing `0` as value. |
||||
* @return Created `BigInt`, containing value, given by its the decimal |
||||
* representation `value`. |
||||
*/ |
||||
public function BigInt MakeBigInt(BaseText value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
return result.SetDecimal(value); |
||||
} |
||||
|
||||
/** |
||||
* Creates new `BigInt` value, base on the decimal representation given by |
||||
* `value`. |
||||
* |
||||
* If invalid decimal representation (digits only, possibly with leading sign) |
||||
* is given - contents of returned value are undefined. Otherwise cannot fail. |
||||
* |
||||
* @param value New value of the caller `BigInt`, given by decimal |
||||
* its representation. |
||||
* @return Created `BigInt`, containing value, given by its the decimal |
||||
* representation `value`. |
||||
*/ |
||||
public function BigInt MakeBigInt_S(string value) |
||||
{ |
||||
local BigInt result; |
||||
|
||||
result = BigInt(_.memory.Allocate(class'BigInt')); |
||||
return result.SetDecimal_S(value); |
||||
} |
||||
|
||||
/** |
||||
* Computes remainder of the integer division of `number` by `divisor`. |
||||
* |
||||
* This method is necessary as a replacement for `%` module operator, since it |
||||
* is an operation on `float`s in UnrealScript and does not have appropriate |
||||
* value range to work with big integer values. |
||||
* |
||||
* @see `IntegerDivision()` method if you need both quotient and remainder. |
||||
* |
||||
* @param number Number that we are dividing. |
||||
* @param divisor Number we are dividing by. |
||||
* @return Remainder of the integer division. |
||||
*/ |
||||
public function int Remainder(int number, int divisor) |
||||
{ |
||||
local int quotient; |
||||
|
||||
quotient = number / divisor; |
||||
return (number - quotient * divisor); |
||||
} |
||||
|
||||
/** |
||||
* Computes quotient and remainder of the integer division of `number` by |
||||
* `divisor`. |
||||
* |
||||
* @see `IntegerDivision()` method if you only need remainder. |
||||
* @param number Number that we are dividing. |
||||
* @param divisor Number we are dividing by. |
||||
* @return `struct` with quotient and remainder of the integer division. |
||||
*/ |
||||
public function IntegerDivisionResult IntegerDivision(int number, int divisor) |
||||
{ |
||||
local IntegerDivisionResult result; |
||||
|
||||
result.quotient = number / divisor; |
||||
result.remainder = (number - result.quotient * divisor); |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,175 +0,0 @@
|
||||
/** |
||||
* Acedia's implementation for object pool that can only store objects of |
||||
* one specific class to allow for both faster allocation and |
||||
* faster deallocation. |
||||
* Allows to set a maximum capacity. |
||||
* Copyright 2020-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 AcediaObjectPool extends Object |
||||
config(AcediaSystem); |
||||
|
||||
// Class of objects that this `AcediaObjectPool` stores. |
||||
// if `== none`, - object pool is considered uninitialized. |
||||
var private class<AcediaObject> storedClass; |
||||
// Actual storage, functions on LIFO principle. |
||||
var public array<AcediaObject> objectPool; |
||||
|
||||
// This struct and it's associated array `poolSizeOverwrite` allows |
||||
// server admins to rewrite the pool capacity for each class. |
||||
struct PoolSizeSetting |
||||
{ |
||||
var class<AcediaObject> objectClass; |
||||
var int maxPoolSize; |
||||
}; |
||||
var private config const array<PoolSizeSetting> poolSizeOverwrite; |
||||
// Capacity for object pool that we are using. |
||||
// Set during initialization and cannot be changed later. |
||||
var private int usedMaxPoolSize; |
||||
|
||||
/** |
||||
* Initialize caller object pool to store objects of `initStoredClass` class. |
||||
* |
||||
* If successful, this action is irreversible: same pool cannot be |
||||
* re-initialized. |
||||
* |
||||
* @param initStoredClass Class of objects that caller object pool will store. |
||||
* @param forcedPoolSize Max pool size for the caller `AcediaObjectPool`. |
||||
* Leaving it at default `0` value will cause method to auto-determine |
||||
* the size: gives priority to the `poolSizeOverwrite` config array; |
||||
* if not specified, uses `AcediaObject`'s `defaultMaxPoolSize` |
||||
* (ignoring `usesObjectPool` setting). |
||||
* @return `true` if initialization completed, `false` otherwise |
||||
* (including if it was already completed with passed `initStoredClass`). |
||||
*/ |
||||
public final function bool Initialize( |
||||
class<AcediaObject> initStoredClass, |
||||
optional int forcedPoolSize) |
||||
{ |
||||
if (storedClass != none) return false; |
||||
if (initStoredClass == none) return false; |
||||
|
||||
// If does not matter that we've set those variables until |
||||
// we set `storedClass`. |
||||
if (forcedPoolSize == 0) { |
||||
usedMaxPoolSize = GetMaxPoolSizeForClass(initStoredClass); |
||||
} |
||||
else { |
||||
usedMaxPoolSize = forcedPoolSize; |
||||
} |
||||
if (usedMaxPoolSize == 0) { |
||||
return false; |
||||
} |
||||
storedClass = initStoredClass; |
||||
return true; |
||||
} |
||||
|
||||
// Determines default object pool size for the initialization. |
||||
private final function int GetMaxPoolSizeForClass( |
||||
class<AcediaObject> classToCheck) |
||||
{ |
||||
local int i; |
||||
local int result; |
||||
if (classToCheck != none) { |
||||
result = classToCheck.default.defaultMaxPoolSize; |
||||
} |
||||
else { |
||||
result = -1; |
||||
} |
||||
// Try to replace it with server's settings |
||||
for (i = 0; i < poolSizeOverwrite.length; i += 1) |
||||
{ |
||||
if (poolSizeOverwrite[i].objectClass == classToCheck) |
||||
{ |
||||
result = poolSizeOverwrite[i].maxPoolSize; |
||||
break; |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Returns class of objects inside the caller `AcediaObjectPool`. |
||||
* |
||||
* @return class of objects inside caller the caller object pool; |
||||
* `none` means object pool was not initialized. |
||||
*/ |
||||
public final function class<AcediaObject> GetClassOfStoredObjects() |
||||
{ |
||||
return storedClass; |
||||
} |
||||
|
||||
/** |
||||
* Clear the storage of all it's contents. |
||||
* |
||||
* Can be used before UnrealEngine's garbage collection to free pooled objects. |
||||
*/ |
||||
public final function Clear() |
||||
{ |
||||
objectPool.length = 0; |
||||
} |
||||
|
||||
/** |
||||
* Adds object to the caller storage |
||||
* (that needs to be initialized to store `newObject.class` classes). |
||||
* |
||||
* For performance purposes does not do duplicates checks, |
||||
* this should be verified from outside `AcediaObjectPool`. |
||||
* |
||||
* Does type checks and only allows objects of the class that caller |
||||
* `AcediaObjectPool` was initialized for. |
||||
* |
||||
* @param newObject Object to put inside caller pool. Must be not `none` and |
||||
* have precisely the class this object pool was initialized to store. |
||||
* @return `true` on success and `false` on failure |
||||
* (can happen if passed `newObject` reference was invalid, caller storage |
||||
* is not initialized yet or reached it's capacity). |
||||
*/ |
||||
public final function bool Store(AcediaObject newObject) |
||||
{ |
||||
if (newObject == none) return false; |
||||
if (newObject.class != storedClass) return false; |
||||
|
||||
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) { |
||||
return false; |
||||
} |
||||
objectPool[objectPool.length] = newObject; |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Extracts last stored object from the pool. Returned object will no longer |
||||
* be stored in the pool. |
||||
* |
||||
* @return Reference to the last (not destroyed) stored object. |
||||
* Only returns `none` if caller `AcediaObjectPool` is either empty or |
||||
* not initialized. |
||||
*/ |
||||
public final function AcediaObject Fetch() |
||||
{ |
||||
local AcediaObject result; |
||||
if (storedClass == none) return none; |
||||
if (objectPool.length <= 0) return none; |
||||
|
||||
result = objectPool[objectPool.length - 1]; |
||||
objectPool.length = objectPool.length - 1; |
||||
return result; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,437 +0,0 @@
|
||||
/** |
||||
* API that provides functions for managing object of classes, derived from |
||||
* `AcediaObject`. It takes care of managing their object pools, as well as |
||||
* ensuring that constructors and finalizers are called properly. |
||||
* Almost all `AcediaObject`s should use this API's methods for their own |
||||
* creation and destruction. |
||||
* Copyright 2020-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 MemoryAPI extends AcediaObject; |
||||
|
||||
/** |
||||
* # Memory API |
||||
* |
||||
* This is most-basic API that must be created before anything else in Acedia, |
||||
* since it is responsible for the proper creation of `AcediaObject`s. |
||||
* It takes care of managing their object pools, as well as ensuring that |
||||
* constructors and finalizers are called properly. |
||||
* Almost all `AcediaObject`s should use this API's methods for their own |
||||
* creation and destruction. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* First of all, this API is only meant for non-actor `Object` creation. |
||||
* `Actor` creation is generally avoided in Acedia and, when unavoidable, |
||||
* different APIs are dealing with that. `MemoryAPI` is designed to work in |
||||
* the absence of any level (and, therefore, `Actor`s) at all. |
||||
* Simply use `MemoryAPI.Allocate()` to create a new object and |
||||
* `MemoryAPI.Free()` to get rid on unneeded reference. Do note that |
||||
* `AcediaObject`s use reference counting and object will be deallocated and |
||||
* pooled only after every trackable reference was released by |
||||
* `MemoryAPI.Free()`. |
||||
* Best practice is to only care about what object reference you're |
||||
* keeping, properly release them with `MemoryAPI.Free()` and to NEVER EVER USE |
||||
* THEM after you've release them. Regardless of whether they were actually |
||||
* deallocated. |
||||
* |
||||
* There's also a set of auxiliary methods for either loading `class`es from |
||||
* their `BaseText`/`string`-given names or even directly creating objects of |
||||
* said classes. |
||||
* |
||||
* ## Motivation |
||||
* |
||||
* UnrealScript lacks any practical way to destroy non-actor objects on |
||||
* demand: the best one can do is remove any references to the object and wait |
||||
* for garbage collection. But garbage collection itself is too slow and causes |
||||
* noticeable lag spikes for players, making it suitable only for cleaning |
||||
* objects when switching levels. To alleviate this problem, there exists |
||||
* a standard class `ObjectPool` that stores unused objects (mostly resources |
||||
* such as textures) inside dynamic array until they are needed. |
||||
* Unfortunately, using a single ObjectPool for a large volume of objects |
||||
* is impractical from performance perspective, since it stores objects of all |
||||
* classes together and each object allocation from the pool can potentially |
||||
* require going through the whole array (see `Engine/ObjectPool.uc`). |
||||
* Acedia uses a separate object pool (implemented by `AcediaObjectPool`) |
||||
* for every single class, making object allocation as trivial as grabbing |
||||
* the last stored object from `AcediaObjectPool`'s internal dynamic array. |
||||
* New pool is prepared for every class you create, as long as it is |
||||
* derived from `AcediaObject`. `AcediaActors` do not use object pools and are |
||||
* meant to be simply `Destroy()`ed. |
||||
* |
||||
* ## Customizing object pools for your classes |
||||
* |
||||
* Object pool usage can be disabled completely for your class by setting |
||||
* `usesObjectPool = false` in `defaultproperties` block. Without object pools |
||||
* `MemoryAPI.Allocate()` will create a new instance of your class every single |
||||
* time. |
||||
* You can also set a limit to how many objects will be stored in an object |
||||
* pool with defaultMaxPoolSize variable. Negative number (default for |
||||
* `AcediaObject`) means that object pool can grow without a limit. |
||||
* `0` effectively disables object pool, similar to setting |
||||
* `usesObjectPool = false`. However, this can be overwritten by server's |
||||
* settings (see `AcediaSystem.ini`: `AcediaObjectPool`). |
||||
*/ |
||||
|
||||
// Store all created pools, so that we can quickly forget stored objects upon |
||||
// garbage collection |
||||
var private array<AcediaObjectPool> registeredPools; |
||||
|
||||
/** |
||||
* Creates a class instance from its `Text` representation. |
||||
* |
||||
* Does not generate log messages upon failure. |
||||
* |
||||
* @param classReference Text representation of the class to return. |
||||
* @return Loaded class, corresponding to its name from `classReference`. |
||||
*/ |
||||
public function class<Object> LoadClass(BaseText classReference) |
||||
{ |
||||
if (classReference == none) { |
||||
return none; |
||||
} |
||||
return class<Object>( |
||||
DynamicLoadObject(classReference.ToString(), |
||||
class'Class', |
||||
true)); |
||||
} |
||||
|
||||
/** |
||||
* Creates a class instance from its `string` representation. |
||||
* |
||||
* Does not generate log messages upon failure. |
||||
* |
||||
* @param classReference `string` representation of the class to return. |
||||
* @return Loaded class, corresponding to its name from `classReference`. |
||||
*/ |
||||
public function class<Object> LoadClass_S(string classReference) |
||||
{ |
||||
return class<Object>(DynamicLoadObject(classReference, class'Class', true)); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new `Object` of a given class. |
||||
* |
||||
* For `AcediaObject`s calls constructors and tries (uses them only if they |
||||
* aren't forbidden for a given class) to make use of their classes' object |
||||
* pools. |
||||
* |
||||
* If Acedia's object does make use of object pools, - |
||||
* guarantees to return last pooled object (in a LIFO queue), |
||||
* unless `forceNewInstance` is set to `true`. |
||||
* |
||||
* @see `AllocateByReference()`, `AllocateByReference_S()` |
||||
* |
||||
* @param classToAllocate Class of the `Object` that this method will |
||||
* create. Must not be subclass of `Actor`. |
||||
* @param forceNewInstance Set this to `true` if you require this method to |
||||
* create a new instance, bypassing any object pools. |
||||
* @return Newly created object. Will only be `none` if: |
||||
* 1. `classToAllocate` is `none`; |
||||
* 2. `classToAllocate` is abstract; |
||||
* 3. `classToAllocate` is derived from `Actor`. |
||||
*/ |
||||
public function Object Allocate( |
||||
class<Object> classToAllocate, |
||||
optional bool forceNewInstance) |
||||
{ |
||||
// TODO: this is an old code require while we still didn't get rid of |
||||
// services - replace it later |
||||
local LevelCore core; |
||||
local Object allocatedObject; |
||||
local AcediaObjectPool relevantPool; |
||||
local class<AcediaObject> acediaObjectClassToAllocate; |
||||
local class<AcediaActor> acediaActorClassToAllocate; |
||||
local class<Actor> actorClassToAllocate; |
||||
|
||||
if (classToAllocate == none) { |
||||
return none; |
||||
} |
||||
// Try using pool first (only if new instance is not required) |
||||
acediaObjectClassToAllocate = class<AcediaObject>(classToAllocate); |
||||
acediaActorClassToAllocate = class<AcediaActor>(classToAllocate); |
||||
if (!forceNewInstance) |
||||
{ |
||||
if (acediaObjectClassToAllocate != none) { |
||||
relevantPool = acediaObjectClassToAllocate.static._getPool(); |
||||
} |
||||
// `relevantPool == none` is expected if object / actor of is setup to |
||||
// not use object pools. |
||||
if (relevantPool != none) { |
||||
allocatedObject = relevantPool.Fetch(); |
||||
} |
||||
} |
||||
// If pools did not work - spawn / create object through regular methods |
||||
if (allocatedObject == none) |
||||
{ |
||||
actorClassToAllocate = class<Actor>(classToAllocate); |
||||
if (actorClassToAllocate != none) |
||||
{ |
||||
core = class'ServerLevelCore'.static.GetInstance(); |
||||
if (core == none) { |
||||
core = class'ClientLevelCore'.static.GetInstance(); |
||||
} |
||||
allocatedObject = core.Spawn(actorClassToAllocate); |
||||
} |
||||
else { |
||||
allocatedObject = (new classToAllocate); |
||||
} |
||||
} |
||||
// Call constructors |
||||
if (acediaObjectClassToAllocate != none) { |
||||
AcediaObject(allocatedObject)._constructor(); |
||||
} |
||||
if (acediaActorClassToAllocate != none) |
||||
{ |
||||
// Call it here, just in case, to make sure constructor is called |
||||
// as soon as possible |
||||
AcediaActor(allocatedObject)._constructor(); |
||||
} |
||||
return allocatedObject; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new `Object` of a given class using its `BaseText` |
||||
* representation. |
||||
* |
||||
* For `AcediaObject`s calls constructors and tries (uses them only if they |
||||
* aren't forbidden for a given class) to make use of their classes' object |
||||
* pools. |
||||
* |
||||
* If Acedia's object does make use of object pools, - |
||||
* guarantees to return last pooled object (in a LIFO queue), |
||||
* unless `forceNewInstance` is set to `true`. |
||||
* @see `Allocate()`, `AllocateByReference_S()` |
||||
* |
||||
* @param refToClassToAllocate `BaseText` representation of the class' name |
||||
* of the `Object` that this method will create. Must not be subclass of |
||||
* `Actor`. |
||||
* @param forceNewInstance Set this to `true` if you require this method to |
||||
* create a new instance, bypassing any object pools. |
||||
* @return Newly created object. Will only be `none` if: |
||||
* 1. `classToAllocate` is `none`; |
||||
* 2. `classToAllocate` is abstract; |
||||
* 3. `classToAllocate` is derived from `Actor`. |
||||
*/ |
||||
public function Object AllocateByReference( |
||||
BaseText refToClassToAllocate, |
||||
optional bool forceNewInstance) |
||||
{ |
||||
return Allocate(LoadClass(refToClassToAllocate), forceNewInstance); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new `Object` of a given class using its `string` |
||||
* representation. |
||||
* |
||||
* For `AcediaObject`s calls constructors and tries (uses them only if they |
||||
* aren't forbidden for a given class) to make use of their classes' object |
||||
* pools. |
||||
* |
||||
* If Acedia's object does make use of object pools, - |
||||
* guarantees to return last pooled object (in a LIFO queue), |
||||
* unless `forceNewInstance` is set to `true`. |
||||
* |
||||
* @see `Allocate()`, `AllocateByReference()` |
||||
* |
||||
* @param refToClassToAllocate `string` representation of the class' name |
||||
* of the `Object` that this method will create. Must not be subclass of |
||||
* `Actor`. |
||||
* @param forceNewInstance Set this to `true` if you require this method to |
||||
* create a new instance, bypassing any object pools. |
||||
* @return Newly created object. Will only be `none` if: |
||||
* 1. `classToAllocate` is `none`; |
||||
* 2. `classToAllocate` is abstract; |
||||
* 3. `classToAllocate` is derived from `Actor`. |
||||
*/ |
||||
public function Object AllocateByReference_S( |
||||
string refToClassToAllocate, |
||||
optional bool forceNewInstance) |
||||
{ |
||||
return Allocate(LoadClass_S(refToClassToAllocate), forceNewInstance); |
||||
} |
||||
|
||||
/** |
||||
* Releases one reference to a given `AcediaObject`, calling its finalizers in |
||||
* case all references were released. |
||||
* |
||||
* Method will attempt to store `objectToRelease` in its object pool once |
||||
* deallocated, unless it is forbidden by its class' settings. |
||||
* |
||||
* @see `FreeMany()` |
||||
* |
||||
* @param objectToRelease Object, which reference method needs to release. |
||||
*/ |
||||
public function Free(Object objectToRelease) |
||||
{ |
||||
// TODO: this is an old code require while we still didn't get rid of |
||||
// services - replace it later, changing argument to `AcediaObject` |
||||
local AcediaObjectPool relevantPool; |
||||
local Actor objectAsActor; |
||||
local AcediaActor objectAsAcediaActor; |
||||
local AcediaObject objectAsAcediaObject; |
||||
|
||||
if (objectToRelease == none) { |
||||
return; |
||||
} |
||||
// Call finalizers for Acedia's objects and actors |
||||
objectAsAcediaObject = AcediaObject(objectToRelease); |
||||
objectAsAcediaActor = AcediaActor(objectToRelease); |
||||
if (objectAsAcediaObject != none) |
||||
{ |
||||
if (!objectAsAcediaObject.IsAllocated()) { |
||||
return; |
||||
} |
||||
objectAsAcediaObject._deref(); |
||||
if (objectAsAcediaObject._getRefCount() > 0) { |
||||
return; |
||||
} |
||||
relevantPool = objectAsAcediaObject._getPool(); |
||||
objectAsAcediaObject._finalizer(); |
||||
} |
||||
if (objectAsAcediaActor != none) |
||||
{ |
||||
if (!objectAsAcediaActor.IsAllocated()) { |
||||
return; |
||||
} |
||||
objectAsAcediaActor._deref(); |
||||
if (objectAsAcediaActor._getRefCount() > 0) { |
||||
return; |
||||
} |
||||
objectAsAcediaActor._finalizer(); |
||||
} |
||||
// Try to store freed object in a pool |
||||
if (relevantPool != none && relevantPool.Store(objectAsAcediaObject)) { |
||||
return; |
||||
} |
||||
// Otherwise destroy actors and forget about objects |
||||
objectAsActor = Actor(objectToRelease); |
||||
if (objectAsActor != none) { |
||||
objectAsActor.Destroy(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Releases one reference to each `AcediaObject` inside the given array |
||||
* `objectsToRelease`, calling finalizers for the ones that got all of their |
||||
* references released. |
||||
* |
||||
* Method will attempt to store objects inside `objectsToRelease` in their |
||||
* object pools, unless it is forbidden by their class' settings. |
||||
* |
||||
* @see `Free()` |
||||
* |
||||
* @param objectToRelease Array of objects, which reference method needs |
||||
* to release. |
||||
*/ |
||||
public function FreeMany(array<Object> objectsToRelease) |
||||
{ |
||||
// TODO: this is an old code require while we still didn't get rid of |
||||
// services - replace it later, changing argument to `AcediaObject` |
||||
local int i; |
||||
|
||||
for (i = 0; i < objectsToRelease.length; i += 1) { |
||||
Free(objectsToRelease[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Forces Unreal Engine to perform garbage collection. |
||||
* By default also cleans up all of the Acedia's objects pools. |
||||
* |
||||
* Process of garbage collection causes significant lag spike during the game |
||||
* and should be used sparingly and at right moments. |
||||
* |
||||
* If not `LevelCore` was setup, Acedia doesn't have access to the level and |
||||
* cannot perform garbage collection, meaning that this method can fail. |
||||
* |
||||
* @param keepAcediaPools Set this to `true` to NOT garbage collect |
||||
* objects inside pools. Otherwise keep it `false`. |
||||
* Pools won't be dropped regardless of this parameter if no `LevelCore` is |
||||
* found. |
||||
* @return `true` if garbage collection successfully happened and `false` if it |
||||
* failed. Garbage collection can only fail if no `LevelCore` was yet |
||||
* setup. |
||||
*/ |
||||
public function bool CollectGarbage(optional bool keepAcediaPools) |
||||
{ |
||||
local LevelCore core; |
||||
|
||||
// Try to find level core |
||||
core = class'ServerLevelCore'.static.GetInstance(); |
||||
if (core == none) { |
||||
core = class'ClientLevelCore'.static.GetInstance(); |
||||
} |
||||
if (core == none) { |
||||
return false; |
||||
} |
||||
// Drop content of all `AcediaObjectPools` first |
||||
if (!keepAcediaPools) { |
||||
DropPools(); |
||||
} |
||||
// This makes Unreal Engine do garbage collection |
||||
core.ConsoleCommand("obj garbage"); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Registers new object pool to auto-clean before Acedia's garbage collection. |
||||
* |
||||
* @param newPool New object pool that can get cleaned if `CollectGarbage()` |
||||
* is called with appropriate parameters. |
||||
* @return `true` if `newPool` was registered, |
||||
* `false` if `newPool == none` or was already registered. |
||||
*/ |
||||
public function bool RegisterNewPool(AcediaObjectPool newPool) |
||||
{ |
||||
local int i; |
||||
|
||||
if (newPool == none) { |
||||
return false; |
||||
} |
||||
registeredPools = default.registeredPools; |
||||
for (i = 0; i < registeredPools.length; i += 1) |
||||
{ |
||||
if (registeredPools[i] == newPool) { |
||||
return false; |
||||
} |
||||
} |
||||
registeredPools[registeredPools.length] = newPool; |
||||
default.registeredPools = registeredPools; |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Forgets about all stored (deallocated) object references in registered |
||||
* object pools. |
||||
*/ |
||||
protected function DropPools() |
||||
{ |
||||
local int i; |
||||
registeredPools = default.registeredPools; |
||||
for (i = 0; i < registeredPools.length; i += 1) |
||||
{ |
||||
if (registeredPools[i] == none) { |
||||
continue; |
||||
} |
||||
registeredPools[i].Clear(); |
||||
} |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,421 +0,0 @@
|
||||
/** |
||||
* API that provides functions for scheduling jobs and expensive tasks such |
||||
* as writing onto the disk. Also provides methods for users to inform API that |
||||
* they've recently did an expensive operation, so that `SchedulerAPI` is to |
||||
* try and use less resources when managing jobs. |
||||
* 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 SchedulerAPI extends AcediaObject |
||||
config(AcediaSystem); |
||||
|
||||
/** |
||||
* # `SchedulerAPI` |
||||
* |
||||
* UnrealScript is inherently single-threaded and whatever method you call, |
||||
* it will be completely executed within a single game's tick. |
||||
* This API is meant for scheduling various actions over time to help emulating |
||||
* multi-threading by spreading some code executions over several different |
||||
* game/server ticks. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* ### Job scheduling |
||||
* |
||||
* One of the reasons which is faulty infinite loop detection system that |
||||
* will crash the game/server if it thinks UnrealScript code has executed too |
||||
* many operations (it is not about execution time, logging a lot of messages |
||||
* with `Log()` can take a lot of time and not crash anything, while simple |
||||
* loop, that would've finished much sooner, can trigger a crash). |
||||
* This is a very atypical problem for mods to have, but Acedia's |
||||
* introduction of databases and avarice link can lead to users trying to read |
||||
* (from database or network) an object that is too big, leading to a crash. |
||||
* Jobs are not about performance, they're about crash prevention. |
||||
* |
||||
* In case you have such a job of your own, that can potentially take too |
||||
* many steps to finish without crashing, you can convert it into |
||||
* a `SchedulerJob` (you make a subclass for your type of the job and |
||||
* instantiate it for each execution of the job). This requires you to |
||||
* restructure your algorithm in such a way, that it is able to run for some |
||||
* finite (maybe small) amount of steps and postpone the rest of calculations |
||||
* to the next tick and put it into a method |
||||
* `SchedulerJob.DoWork(int allottedWorkUnits)`, where `allottedWorkUnits` is |
||||
* how much your method is allowed to do during this call, assuming `10000` |
||||
* units of work on their own won't lead to a crash. |
||||
* Another method `SchedulerJob.IsCompleted()` needs to be setup to return |
||||
* `true` iff your job is done. |
||||
* After you prepared an instance of your job subclass, simply pass it to |
||||
* `_.scheduler.AddJob()`. |
||||
* |
||||
* ### Disk usage requests |
||||
* |
||||
* Writing to the disk (saving data into config file, saving local database |
||||
* changes) can be an expensive operation and to avoid lags in gameplay you |
||||
* might want to spread such operations over time. |
||||
* `_.scheduler.RequestDiskAccess()` method allows you to do that. It is not |
||||
* exactly a signal, but it acts similar to one: to request a right to save to |
||||
* the disk, just do the following: |
||||
* `_.scheduler.RequestDiskAccess(<receiver>).connect = <disk_writing_method>` |
||||
* and `disk_writing_method()` will be called once your turn come up. |
||||
* |
||||
* ## Manual ticking |
||||
* |
||||
* If any kind of level core (either server or client one) was created, |
||||
* this API will automatically perform necessary actions every tick. |
||||
* Otherwise, if only base API is available, there's no way to do that, but |
||||
* you can manually decide when to tick this API by calling `ManualTick()` |
||||
* method. |
||||
*/ |
||||
|
||||
/** |
||||
* How often can files be saved on disk. This is a relatively expensive |
||||
* operation and we don't want to write a lot of different files at once. |
||||
* But since we lack a way to exactly measure how much time that saving will |
||||
* take, AcediaCore falls back to simply performing every saving with same |
||||
* uniform time intervals in-between. |
||||
* This variable decides how much time there should be between two file |
||||
* writing accesses. |
||||
* Negative and zero values mean that all writing disk access will be |
||||
* granted as soon as possible, without any cooldowns. |
||||
*/ |
||||
var private config float diskSaveCooldown; |
||||
/** |
||||
* Maximum total work units for jobs allowed per tick. Jobs are expected to be |
||||
* constructed such that they don't lead to a crash if they have to perform |
||||
* this much work. |
||||
* |
||||
* Changing default value of `10000` is not advised. |
||||
*/ |
||||
var private config int maxWorkUnits; |
||||
/** |
||||
* How many different jobs can be performed per tick. This limit is added so |
||||
* that `maxWorkUnits` won't be spread too thin if a lot of jobs get registered |
||||
* at once. |
||||
*/ |
||||
var private config int maxJobsPerTick; |
||||
|
||||
// We can (and will) automatically tick |
||||
var private bool tickAvailable; |
||||
// `true` == it is safe to use server API for a tick |
||||
// `false` == it is safe to use client API for a tick |
||||
var private bool tickFromServer; |
||||
// Our `Tick()` method is currently connected to the `OnTick()` signal. |
||||
// Keeping track of this allows us to disconnect from `OnTick()` signal |
||||
// when it is not necessary. |
||||
var private bool connectedToTick; |
||||
|
||||
// How much time if left until we can write to the disk again? |
||||
var private float currentDiskCooldown; |
||||
|
||||
// There is a limit (`maxJobsPerTick`) to how many different jobs we can |
||||
// perform per tick and if we register an amount jobs over that limit, we need |
||||
// to uniformly spread execution time between them. |
||||
// To achieve that we simply cyclically (in order) go over `currentJobs` |
||||
// array, each time executing exactly `maxJobsPerTick` jobs. |
||||
// `nextJobToPerform` remembers what job is to be executed next tick. |
||||
var private int nextJobToPerform; |
||||
var private array<SchedulerJob> currentJobs; |
||||
// Storing receiver objects, following example of signals/slots, is done |
||||
// without increasing their reference count, allowing them to get deallocated |
||||
// while we are still keeping their reference. |
||||
// To avoid using such deallocated receivers, we keep track of the life |
||||
// versions they've had when their disk requests were registered. |
||||
var private array<SchedulerDiskRequest> diskQueue; |
||||
var private array<AcediaObject> receivers; |
||||
var private array<int> receiversLifeVersions; |
||||
|
||||
/** |
||||
* Registers new scheduler job `newJob` to be executed in the API. |
||||
* |
||||
* @param newJob New job to be scheduled for execution. |
||||
* Does nothing if given `newJob` is already added. |
||||
*/ |
||||
public function AddJob(SchedulerJob newJob) |
||||
{ |
||||
local int i; |
||||
|
||||
if (newJob == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < currentJobs.length; i += 1) |
||||
{ |
||||
if (currentJobs[i] == newJob) { |
||||
return; |
||||
} |
||||
} |
||||
newJob.NewRef(); |
||||
currentJobs[currentJobs.length] = newJob; |
||||
UpdateTickConnection(); |
||||
} |
||||
|
||||
/** |
||||
* Requests another disk access. |
||||
* |
||||
* Use it like signal: `RequestDiskAccess(<receiver>).connect = <handler>`. |
||||
* Since it is meant to be used as a signal, so DO NOT STORE/RELEASE returned |
||||
* wrapper object `SchedulerDiskRequest`. |
||||
* |
||||
* @param receiver Same as for signal/slots, this is an object, responsible |
||||
* for the disk request. If this object gets deallocated - request will be |
||||
* thrown away. |
||||
* Typically this should be an object in which connected method will be |
||||
* executed. |
||||
* @return Wrapper object that provides `connect` delegate. |
||||
*/ |
||||
public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver) |
||||
{ |
||||
local SchedulerDiskRequest newRequest; |
||||
|
||||
if (receiver == none) return none; |
||||
if (!receiver.IsAllocated()) return none; |
||||
|
||||
newRequest = |
||||
SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); |
||||
diskQueue[diskQueue.length] = newRequest; |
||||
receivers[receivers.length] = receiver; |
||||
receiversLifeVersions[receiversLifeVersions.length] = |
||||
receiver.GetLifeVersion(); |
||||
UpdateTickConnection(); |
||||
return newRequest; |
||||
} |
||||
|
||||
/** |
||||
* Tells you how many incomplete jobs are currently registered in |
||||
* the scheduler. |
||||
* |
||||
* @return How many incomplete jobs are currently registered in the scheduler. |
||||
*/ |
||||
public function int GetJobsAmount() |
||||
{ |
||||
CleanCompletedJobs(); |
||||
return currentJobs.length; |
||||
} |
||||
|
||||
/** |
||||
* Tells you how many disk access requests are currently registered in |
||||
* the scheduler. |
||||
* |
||||
* @return How many incomplete disk access requests are currently registered |
||||
* in the scheduler. |
||||
*/ |
||||
public function int GetDiskQueueSize() |
||||
{ |
||||
CleanDiskQueue(); |
||||
return diskQueue.length; |
||||
} |
||||
|
||||
/** |
||||
* In case neither server, nor client core is registered, scheduler must be |
||||
* ticked manually. For that call this method each separate tick (or whatever |
||||
* is your closest approximation available for that). |
||||
* |
||||
* Before manually invoking this method, you should check if scheduler |
||||
* actually started to tick *automatically*. Use `_.scheduler.IsAutomated()` |
||||
* for that. |
||||
* |
||||
* NOTE: If neither server-/client- core is created, nor `ManualTick()` is |
||||
* invoked manually, `SchedulerAPI` won't actually do anything. |
||||
* |
||||
* @param delta Time (real one) that is supposedly passes from the moment |
||||
* `ManualTick()` was called last time. Used for tracking disk access |
||||
* cooldowns. How `SchedulerJob`s are executed is independent from this |
||||
* value. |
||||
*/ |
||||
public final function ManualTick(optional float delta) |
||||
{ |
||||
Tick(delta, 1.0); |
||||
} |
||||
|
||||
/** |
||||
* Is scheduler ticking automated? It can only be automated if either |
||||
* server or client level cores are created. Scheduler can automatically enable |
||||
* automation and it cannot be prevented, but can be helped by using |
||||
* `UpdateTickConnection()` method. |
||||
* |
||||
* @return `true` if scheduler's tick is automatically called and `false` |
||||
* otherwise (and calling `ManualTick()` is required). |
||||
*/ |
||||
public function bool IsAutomated() |
||||
{ |
||||
return tickAvailable; |
||||
} |
||||
|
||||
/** |
||||
* Causes `SchedulerAPI` to try automating itself by searching for level cores |
||||
* (checking if server/client APIs are enabled). |
||||
*/ |
||||
public function UpdateTickConnection() |
||||
{ |
||||
local bool needsConnection; |
||||
local UnrealAPI api; |
||||
|
||||
if (!tickAvailable) |
||||
{ |
||||
if (_server.IsAvailable()) |
||||
{ |
||||
tickAvailable = true; |
||||
tickFromServer = true; |
||||
} |
||||
else if (_client.IsAvailable()) |
||||
{ |
||||
tickAvailable = true; |
||||
tickFromServer = false; |
||||
} |
||||
if (!tickAvailable) { |
||||
return; |
||||
} |
||||
} |
||||
needsConnection = (currentJobs.length > 0 || diskQueue.length > 0); |
||||
if (connectedToTick == needsConnection) { |
||||
return; |
||||
} |
||||
if (tickFromServer) { |
||||
api = _server.unreal; |
||||
} |
||||
else { |
||||
api = _client.unreal; |
||||
} |
||||
if (connectedToTick && !needsConnection) { |
||||
api.OnTick(self).Disconnect(); |
||||
} |
||||
else if (!connectedToTick && needsConnection) { |
||||
api.OnTick(self).connect = Tick; |
||||
} |
||||
connectedToTick = needsConnection; |
||||
} |
||||
|
||||
private function Tick(float delta, float dilationCoefficient) |
||||
{ |
||||
delta = delta / dilationCoefficient; |
||||
// Manage disk cooldown |
||||
if (currentDiskCooldown > 0) { |
||||
currentDiskCooldown -= delta; |
||||
} |
||||
if (currentDiskCooldown <= 0 && diskQueue.length > 0) |
||||
{ |
||||
currentDiskCooldown = diskSaveCooldown; |
||||
ProcessDiskQueue(); |
||||
} |
||||
// Manage jobs |
||||
if (currentJobs.length > 0) { |
||||
ProcessJobs(); |
||||
} |
||||
UpdateTickConnection(); |
||||
} |
||||
|
||||
private function ProcessJobs() |
||||
{ |
||||
local int unitsPerJob; |
||||
local int jobsToPerform; |
||||
|
||||
CleanCompletedJobs(); |
||||
jobsToPerform = Min(currentJobs.length, maxJobsPerTick); |
||||
if (jobsToPerform <= 0) { |
||||
return; |
||||
} |
||||
unitsPerJob = maxWorkUnits / jobsToPerform; |
||||
while (jobsToPerform > 0) |
||||
{ |
||||
if (nextJobToPerform >= currentJobs.length) { |
||||
nextJobToPerform = 0; |
||||
} |
||||
currentJobs[nextJobToPerform].DoWork(unitsPerJob); |
||||
nextJobToPerform += 1; |
||||
jobsToPerform -= 1; |
||||
} |
||||
} |
||||
|
||||
private function ProcessDiskQueue() |
||||
{ |
||||
local int i; |
||||
|
||||
// Even if we clean disk queue here, we still need to double check |
||||
// lifetimes in the code below, since we have no idea what `.connect()` |
||||
// calls might do |
||||
CleanDiskQueue(); |
||||
if (diskQueue.length <= 0) { |
||||
return; |
||||
} |
||||
if (diskSaveCooldown > 0) |
||||
{ |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||
diskQueue[i].connect(); |
||||
} |
||||
_.memory.Free(diskQueue[0]); |
||||
diskQueue.Remove(0, 1); |
||||
receivers.Remove(0, 1); |
||||
receiversLifeVersions.Remove(0, 1); |
||||
return; |
||||
} |
||||
for (i = 0; i < diskQueue.length; i += 1) |
||||
{ |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) { |
||||
diskQueue[i].connect(); |
||||
} |
||||
_.memory.Free(diskQueue[i]); |
||||
} |
||||
diskQueue.length = 0; |
||||
receivers.length = 0; |
||||
receiversLifeVersions.length = 0; |
||||
} |
||||
|
||||
// Removes completed jobs |
||||
private function CleanCompletedJobs() |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < currentJobs.length) |
||||
{ |
||||
if (currentJobs[i].IsCompleted()) |
||||
{ |
||||
if (i < nextJobToPerform) { |
||||
nextJobToPerform -= 1; |
||||
} |
||||
currentJobs[i].FreeSelf(); |
||||
currentJobs.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Remove disk requests with deallocated receivers |
||||
private function CleanDiskQueue() |
||||
{ |
||||
local int i; |
||||
|
||||
while (i < diskQueue.length) |
||||
{ |
||||
if (receivers[i].GetLifeVersion() == receiversLifeVersions[i]) |
||||
{ |
||||
i += 1; |
||||
continue; |
||||
} |
||||
_.memory.Free(diskQueue[i]); |
||||
diskQueue.Remove(i, 1); |
||||
receivers.Remove(i, 1); |
||||
receiversLifeVersions.Remove(i, 1); |
||||
} |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
diskSaveCooldown = 0.25 |
||||
maxWorkUnits = 10000 |
||||
maxJobsPerTick = 5 |
||||
} |
@ -1,47 +0,0 @@
|
||||
/** |
||||
* Template object that represents a job, capable of being scheduled on the |
||||
* `SchedulerAPI`. Use `IsCompleted()` to mark job as completed. |
||||
* 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 SchedulerJob extends AcediaObject |
||||
abstract; |
||||
|
||||
/** |
||||
* Checks if caller `SchedulerJob` was completed. |
||||
* Once this method returns `true`, it shouldn't start returning `false` again. |
||||
* |
||||
* @return `true` if `SchedulerJob` is already completed and doesn't need to |
||||
* be further executed and `false` otherwise. |
||||
*/ |
||||
public function bool IsCompleted(); |
||||
|
||||
/** |
||||
* Called when scheduler decides that `SchedulerJob` should be executed, taking |
||||
* amount of abstract "work units" that it is allowed to spend for work. |
||||
* |
||||
* @param allottedWorkUnits Work units allotted to the caller |
||||
* `SchedulerJob`. By default there is `10000` work units per second, so |
||||
* you can expect about 10000 / 1000 = 10 work units per millisecond or, |
||||
* on servers with 30 tick rate, about 10000 * (30 / 1000) = 300 work units |
||||
* per tick to be allotted to all the scheduled jobs. |
||||
*/ |
||||
public function DoWork(int allottedWorkUnits); |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -1,526 +0,0 @@
|
||||
/** |
||||
* Container for the information about available resources from other packages. |
||||
* 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 AcediaEnvironment extends AcediaObject; |
||||
|
||||
/** |
||||
* # `AcediaEnvironment` |
||||
* |
||||
* Instance of this class will be used by Acedia to manage resources available |
||||
* from different packages like `Feature`s and such other etc.. |
||||
* This is mostly necessary to implement Acedia loader (and, possibly, |
||||
* its alternatives) that would load available packages and enable `Feature`s |
||||
* admin wants to be enabled. |
||||
* |
||||
* ## Packages |
||||
* |
||||
* Any package to be used in Acedia should first be *registered* with |
||||
* `RegisterPackage()` method. Then a manifest class from it will be read and |
||||
* Acedia will become aware of all the resources that package contains. |
||||
* Once any of those resources is used, package gets marked as *loaded* and its |
||||
* *entry object* (if specified) will be created. |
||||
* |
||||
* ## `Feature`s |
||||
* |
||||
* Whether `Feature` is enabled is governed by the `AcediaEnvironment` added |
||||
* into the `Global` class. It is possible to create several `Feature` |
||||
* instances of the same class instance of each class, but only one can be |
||||
* considered enabled at the same time. |
||||
*/ |
||||
|
||||
var private bool acediaShutDown; |
||||
|
||||
var private array< class<_manifest> > availablePackages; |
||||
var private array< class<_manifest> > loadedPackages; |
||||
|
||||
var private array< class<Feature> > availableFeatures; |
||||
var private array<Feature> enabledFeatures; |
||||
var private array<int> enabledFeaturesLifeVersions; |
||||
|
||||
var private string manifestSuffix; |
||||
|
||||
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered; |
||||
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled; |
||||
var private LoggerAPI.Definition warnFeatureAlreadyEnabled; |
||||
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled; |
||||
|
||||
var private SimpleSignal onShutdownSignal; |
||||
var private SimpleSignal onShutdownSystemSignal; |
||||
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal; |
||||
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal; |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
// Always register our core package |
||||
RegisterPackage_S("AcediaCore"); |
||||
onShutdownSignal = SimpleSignal( |
||||
_.memory.Allocate(class'SimpleSignal')); |
||||
onShutdownSystemSignal = SimpleSignal( |
||||
_.memory.Allocate(class'SimpleSignal')); |
||||
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal( |
||||
_.memory.Allocate(class'Environment_FeatureEnabled_Signal')); |
||||
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal( |
||||
_.memory.Allocate(class'Environment_FeatureDisabled_Signal')); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
_.memory.Free(onShutdownSignal); |
||||
_.memory.Free(onShutdownSystemSignal); |
||||
_.memory.Free(onFeatureEnabledSignal); |
||||
_.memory.Free(onFeatureDisabledSignal); |
||||
} |
||||
|
||||
/** |
||||
* Signal that will be emitted before Acedia shuts down. |
||||
* At this point all APIs should still exist and function. |
||||
* |
||||
* [Signature] |
||||
* void <slot>() |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function SimpleSlot OnShutDown(AcediaObject receiver) |
||||
{ |
||||
return SimpleSlot(onShutdownSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/** |
||||
* Signal that will be emitted during Acedia shut down. System API use it to |
||||
* clean up after themselves, so one shouldn't rely on them. |
||||
* |
||||
* There is no reason to use this signal unless you're reimplementing one of |
||||
* the APIs. Otherwise you probably want to use `OnShutDown()` signal instead. |
||||
* |
||||
* [Signature] |
||||
* void <slot>() |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function SimpleSlot OnShutDownSystem(AcediaObject receiver) |
||||
{ |
||||
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/** |
||||
* Signal that will be emitted when new `Feature` is enabled. |
||||
* Emitted after `Feature`'s `OnEnabled()` method was called. |
||||
* |
||||
* [Signature] |
||||
* void <slot>(Feature enabledFeature) |
||||
* |
||||
* @param enabledFeature `Feature` instance that was just enabled. |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function Environment_FeatureEnabled_Slot OnFeatureEnabled( |
||||
AcediaObject receiver) |
||||
{ |
||||
return Environment_FeatureEnabled_Slot( |
||||
onFeatureEnabledSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/** |
||||
* Signal that will be emitted when new `Feature` is disabled. |
||||
* Emitted after `Feature`'s `OnDisabled()` method was called. |
||||
* |
||||
* [Signature] |
||||
* void <slot>(class<Feature> disabledFeatureClass) |
||||
* |
||||
* @param disabledFeatureClass Class of the `Feature` instance that was |
||||
* just disabled. |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function Environment_FeatureDisabled_Slot OnFeatureDisabled( |
||||
AcediaObject receiver) |
||||
{ |
||||
return Environment_FeatureDisabled_Slot( |
||||
onFeatureEnabledSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
/** |
||||
* Shuts AcediaCore down, performing all the necessary cleaning up. |
||||
*/ |
||||
public final function Shutdown() |
||||
{ |
||||
local LevelCore core; |
||||
if (acediaShutDown) { |
||||
return; |
||||
} |
||||
DisableAllFeatures(); |
||||
onShutdownSignal.Emit(); |
||||
onShutdownSystemSignal.Emit(); |
||||
core = class'ServerLevelCore'.static.GetInstance(); |
||||
if (core != none) { |
||||
core.Destroy(); |
||||
} |
||||
core = class'ClientLevelCore'.static.GetInstance(); |
||||
if (core != none) { |
||||
core.Destroy(); |
||||
} |
||||
acediaShutDown = true; |
||||
} |
||||
|
||||
/** |
||||
* Registers an Acedia package with name given by `packageName`. |
||||
* |
||||
* @param packageName Name of the package to register. Must not be `none`. |
||||
* This package must exist and not have yet been registered in this |
||||
* environment. |
||||
* @return `true` if package was successfully registered, `false` if it |
||||
* either does not exist, was already registered or `packageName` is |
||||
* `none`. |
||||
*/ |
||||
public final function bool RegisterPackage(BaseText packageName) |
||||
{ |
||||
local class<_manifest> manifestClass; |
||||
|
||||
if (packageName == none) { |
||||
return false; |
||||
} |
||||
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy()); |
||||
manifestClass = class<_manifest>(DynamicLoadObject( |
||||
packageName.ToString() $ manifestSuffix, class'Class', true)); |
||||
if (manifestClass == none) |
||||
{ |
||||
_.logger.Auto(errNotRegistered).Arg(packageName.Copy()); |
||||
return false; |
||||
} |
||||
if (IsManifestRegistered(manifestClass)) |
||||
{ |
||||
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy()); |
||||
return false; |
||||
} |
||||
availablePackages[availablePackages.length] = manifestClass; |
||||
ReadManifest(manifestClass); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Registers an Acedia package with name given by `packageName`. |
||||
* |
||||
* @param packageName Name of the package to register. |
||||
* This package must exist and not have yet been registered in this |
||||
* environment. |
||||
* @return `true` if package was successfully registered, `false` if it |
||||
* either does not exist or was already registered. |
||||
*/ |
||||
public final function RegisterPackage_S(string packageName) |
||||
{ |
||||
local Text wrapper; |
||||
|
||||
wrapper = _.text.FromString(packageName); |
||||
RegisterPackage(wrapper); |
||||
_.memory.Free(wrapper); |
||||
} |
||||
|
||||
private final function bool IsManifestRegistered(class<_manifest> manifestClass) |
||||
{ |
||||
local int i; |
||||
|
||||
for (i = 0; i < availablePackages.length; i += 1) |
||||
{ |
||||
if (manifestClass == availablePackages[i]) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function ReadManifest(class<_manifest> manifestClass) |
||||
{ |
||||
local int i; |
||||
|
||||
for (i = 0; i < manifestClass.default.features.length; i += 1) |
||||
{ |
||||
if (manifestClass.default.features[i] == none) { |
||||
continue; |
||||
} |
||||
manifestClass.default.features[i].static.LoadConfigs(); |
||||
availableFeatures[availableFeatures.length] = |
||||
manifestClass.default.features[i]; |
||||
} |
||||
for (i = 0; i < manifestClass.default.testCases.length; i += 1) |
||||
{ |
||||
class'TestingService'.static |
||||
.RegisterTestCase(manifestClass.default.testCases[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns all packages registered in the caller `AcediaEnvironment`. |
||||
* |
||||
* NOTE: package being registered doesn't mean it's actually loaded. |
||||
* Package must either be explicitly loaded or automatically when one of its |
||||
* resources is being used. |
||||
* |
||||
* @return All packages registered in caller `AcediaEnvironment`. |
||||
*/ |
||||
public final function array< class<_manifest> > GetAvailablePackages() |
||||
{ |
||||
return availablePackages; |
||||
} |
||||
|
||||
/** |
||||
* Returns all packages loaded in the caller `AcediaEnvironment`. |
||||
* |
||||
* NOTE: package being registered doesn't mean it's actually loaded. |
||||
* Package must either be explicitly loaded or automatically when one of its |
||||
* resources is being used. |
||||
* |
||||
* @return All packages loaded in caller `AcediaEnvironment`. |
||||
*/ |
||||
public final function array< class<_manifest> > GetLoadedPackages() |
||||
{ |
||||
return loadedPackages; |
||||
} |
||||
|
||||
/** |
||||
* Returns all `Feature`s available in the caller `AcediaEnvironment`. |
||||
* |
||||
* @return All `Feature`s available in the caller `AcediaEnvironment`. |
||||
*/ |
||||
public final function array< class<Feature> > GetAvailableFeatures() |
||||
{ |
||||
return availableFeatures; |
||||
} |
||||
|
||||
/** |
||||
* Returns all `Feature` instances enabled in the caller `AcediaEnvironment`. |
||||
* |
||||
* @return All `Feature`s enabled in the caller `AcediaEnvironment`. |
||||
*/ |
||||
public final function array<Feature> GetEnabledFeatures() |
||||
{ |
||||
local int i; |
||||
for (i = 0; i < enabledFeatures.length; i += 1) { |
||||
enabledFeatures[i].NewRef(); |
||||
} |
||||
return enabledFeatures; |
||||
} |
||||
|
||||
// CleanRemove `Feature`s that got deallocated. |
||||
// This shouldn't happen unless someone messes up. |
||||
private final function CleanEnabledFeatures() |
||||
{ |
||||
local int i; |
||||
while (i < enabledFeatures.length) |
||||
{ |
||||
if ( enabledFeatures[i].GetLifeVersion() |
||||
!= enabledFeaturesLifeVersions[i]) |
||||
{ |
||||
enabledFeatures.Remove(i, 1); |
||||
} |
||||
else { |
||||
i += 1; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Checks if `Feature` of given class `featureClass` is enabled. |
||||
* |
||||
* NOTE: even if If feature of class `featureClass` is enabled, it's not |
||||
* necessarily that the instance you have reference to is enabled. |
||||
* Although unlikely, it is possible that someone spawned another instance |
||||
* of the same class that isn't considered enabled. If you want to check |
||||
* whether some particular instance of given class `featureClass` is enabled, |
||||
* use `IsFeatureEnabled()` method instead. |
||||
* |
||||
* @param featureClass Feature class to check for being enabled. |
||||
* @return `true` if feature of class `featureClass` is currently enabled and |
||||
* `false` otherwise. |
||||
*/ |
||||
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) |
||||
{ |
||||
local int i; |
||||
if (featureClass == none) { |
||||
return false; |
||||
} |
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
if (featureClass == enabledFeatures[i].class) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Checks if given `Feature` instance is enabled. |
||||
* |
||||
* If you want to check if any instance instance of given class |
||||
* `classToCheck` is enabled (and not `feature` specifically), use |
||||
* `IsFeatureClassEnabled()` method instead. |
||||
* |
||||
* @param feature Feature instance to check for being enabled. |
||||
* @return `true` if feature `feature` is currently enabled and |
||||
* `false` otherwise. |
||||
*/ |
||||
public final function bool IsFeatureEnabled(Feature feature) |
||||
{ |
||||
local int i; |
||||
if (feature == none) return false; |
||||
if (!feature.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
if (feature == enabledFeatures[i]) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Returns enabled `Feature` instance of the given class `featureClass`. |
||||
* |
||||
* @param featureClass Feature class to find enabled instance for. |
||||
* @return Enabled `Feature` instance of the given class `featureClass`. |
||||
* If no feature of `featureClass` is enabled, returns `none`. |
||||
*/ |
||||
public final function Feature GetEnabledFeature(class<Feature> featureClass) |
||||
{ |
||||
local int i; |
||||
if (featureClass == none) { |
||||
return none; |
||||
} |
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
if (featureClass == enabledFeatures[i].class) |
||||
{ |
||||
enabledFeatures[i].NewRef(); |
||||
return enabledFeatures[i]; |
||||
} |
||||
} |
||||
return none; |
||||
} |
||||
|
||||
/** |
||||
* Enables given `Feature` instance `newEnabledFeature` with a given config. |
||||
* |
||||
* @see `Feature::EnableMe()`. |
||||
* |
||||
* @param newEnabledFeature Instance to enable. |
||||
* @param configName Name of the config to enable `newEnabledFeature` |
||||
* feature with. `none` means "default" config (will be created, if |
||||
* necessary). |
||||
* @return `true` if given `newEnabledFeature` was enabled and `false` |
||||
* otherwise (including if feature of the same class has already been |
||||
* enabled). |
||||
*/ |
||||
public final function bool EnableFeature( |
||||
Feature newEnabledFeature, |
||||
BaseText configName) |
||||
{ |
||||
local int i; |
||||
if (newEnabledFeature == none) return false; |
||||
if (!newEnabledFeature.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
if (newEnabledFeature.class == enabledFeatures[i].class) |
||||
{ |
||||
if (newEnabledFeature == enabledFeatures[i]) |
||||
{ |
||||
_.logger |
||||
.Auto(warnFeatureAlreadyEnabled) |
||||
.Arg(_.text.FromClass(newEnabledFeature.class)); |
||||
} |
||||
else |
||||
{ |
||||
_.logger |
||||
.Auto(errFeatureClassAlreadyEnabled) |
||||
.Arg(_.text.FromClass(newEnabledFeature.class)); |
||||
} |
||||
return false; |
||||
} |
||||
} |
||||
newEnabledFeature.NewRef(); |
||||
enabledFeatures[enabledFeatures.length] = newEnabledFeature; |
||||
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] = |
||||
newEnabledFeature.GetLifeVersion(); |
||||
newEnabledFeature.EnableInternal(configName); |
||||
onFeatureEnabledSignal.Emit(newEnabledFeature); |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Disables given `Feature` instance `featureToDisable`. |
||||
* |
||||
* @see `Feature::EnableMe()`. |
||||
* |
||||
* @param featureToDisable Instance to disable. |
||||
* @return `true` if given `newEnabledFeature` was disabled and `false` |
||||
* otherwise (including if it already was disabled). |
||||
*/ |
||||
public final function bool DisableFeature(Feature featureToDisable) |
||||
{ |
||||
local int i; |
||||
if (featureToDisable == none) return false; |
||||
if (!featureToDisable.IsAllocated()) return false; |
||||
|
||||
CleanEnabledFeatures(); |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
if (featureToDisable == enabledFeatures[i]) |
||||
{ |
||||
enabledFeatures.Remove(i, 1); |
||||
enabledFeaturesLifeVersions.Remove(i, 1); |
||||
featureToDisable.DisableInternal(); |
||||
onFeatureDisabledSignal.Emit(featureToDisable.class); |
||||
_.memory.Free(featureToDisable); |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Disables all currently enabled `Feature`s. |
||||
* |
||||
* Mainly intended for the clean up when Acedia shuts down. |
||||
*/ |
||||
public final function DisableAllFeatures() |
||||
{ |
||||
local int i; |
||||
local array<Feature> featuresCopy; |
||||
|
||||
CleanEnabledFeatures(); |
||||
featuresCopy = enabledFeatures; |
||||
enabledFeatures.length = 0; |
||||
enabledFeaturesLifeVersions.length = 0; |
||||
for (i = 0; i < enabledFeatures.length; i += 1) |
||||
{ |
||||
featuresCopy[i].DisableInternal(); |
||||
onFeatureDisabledSignal.Emit(featuresCopy[i].class); |
||||
} |
||||
_.memory.FreeMany(featuresCopy); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
manifestSuffix = ".Manifest" |
||||
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") |
||||
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") |
||||
errNotRegistered = (l=LOG_Error,m="Package \"%2\" has failed to be registered.") |
||||
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.") |
||||
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.") |
||||
} |
@ -1,104 +0,0 @@
|
||||
/** |
||||
* Class for an object that will provide an access to a Acedia's functionality |
||||
* that is common for both clients and servers by giving a reference to this |
||||
* object to all Acedia's objects and actors, emulating a global API namespace. |
||||
* Copyright 2020-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 Global extends Object; |
||||
|
||||
// `Global` is expected to behave like a singleton and will store it's |
||||
// main instance in this variable's default value. |
||||
var protected Global myself; |
||||
|
||||
var public RefAPI ref; |
||||
var public BoxAPI box; |
||||
var public MathAPI math; |
||||
var public LoggerAPI logger; |
||||
var public CollectionsAPI collections; |
||||
var public AliasesAPI alias; |
||||
var public TextAPI text; |
||||
var public MemoryAPI memory; |
||||
var public ConsoleAPI console; |
||||
var public ChatAPI chat; |
||||
var public ColorAPI color; |
||||
var public UserAPI users; |
||||
var public PlayersAPI players; |
||||
var public JSONAPI json; |
||||
var public DBAPI db; |
||||
var public SchedulerAPI scheduler; |
||||
var public AvariceAPI avarice; |
||||
|
||||
var public AcediaEnvironment environment; |
||||
|
||||
public final static function Global GetInstance() |
||||
{ |
||||
if (default.myself == none) { |
||||
// `...Global`s are special and exist outside main Acedia's |
||||
// object infrastructure, so we allocate it without using API methods. |
||||
default.myself = new class'Global'; |
||||
default.myself.Initialize(); |
||||
} |
||||
return default.myself; |
||||
} |
||||
|
||||
protected function Initialize() |
||||
{ |
||||
// Special case that we cannot spawn with memory API since it obviously |
||||
// does not exist yet! |
||||
memory = new class'MemoryAPI'; |
||||
memory._constructor(); |
||||
// `TextAPI` and `CollectionsAPI` need to be loaded before `LoggerAPI` |
||||
ref = RefAPI(memory.Allocate(class'RefAPI')); |
||||
box = BoxAPI(memory.Allocate(class'BoxAPI')); |
||||
text = TextAPI(memory.Allocate(class'TextAPI')); |
||||
math = MathAPI(memory.Allocate(class'MathAPI')); |
||||
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); |
||||
logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); |
||||
color = ColorAPI(memory.Allocate(class'ColorAPI')); |
||||
alias = AliasesAPI(memory.Allocate(class'AliasesAPI')); |
||||
console = ConsoleAPI(memory.Allocate(class'ConsoleAPI')); |
||||
chat = ChatAPI(memory.Allocate(class'ChatAPI')); |
||||
users = UserAPI(memory.Allocate(class'UserAPI')); |
||||
players = PlayersAPI(memory.Allocate(class'PlayersAPI')); |
||||
json = JSONAPI(memory.Allocate(class'JSONAPI')); |
||||
db = DBAPI(memory.Allocate(class'DBAPI')); |
||||
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); |
||||
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); |
||||
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); |
||||
} |
||||
|
||||
public function DropCoreAPI() |
||||
{ |
||||
memory = none; |
||||
ref = none; |
||||
box = none; |
||||
text = none; |
||||
collections = none; |
||||
logger = none; |
||||
alias = none; |
||||
console = none; |
||||
chat = none; |
||||
color = none; |
||||
users = none; |
||||
players = none; |
||||
json = none; |
||||
db = none; |
||||
scheduler = none; |
||||
avarice = none; |
||||
default.myself = none; |
||||
} |
@ -1,76 +0,0 @@
|
||||
/** |
||||
* Base class for iterator, an auxiliary object for iterating through |
||||
* a set of objects obtained from some context-dependent source. |
||||
* 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 Iter extends AcediaObject |
||||
abstract; |
||||
|
||||
/** |
||||
* Iterators can filter objects they're iterating on by a presence or lack of |
||||
* a certain property, recording this choice requires 3 values, so `bool` |
||||
* isn't enough and we need to use this `enum` instead. |
||||
*/ |
||||
enum IterFilter |
||||
{ |
||||
// We don't use relevant property for filtering |
||||
ITF_Nothing, |
||||
// Iterated objects must have that property |
||||
ITF_Have, |
||||
// Iterated objects must not have that property |
||||
ITF_NotHave |
||||
}; |
||||
|
||||
/** |
||||
* Makes iterator pick next item. |
||||
* Use `HasFinished()` to check whether you have iterated all of them. |
||||
* |
||||
* @return Reference to caller `Iterator` to allow for method chaining. |
||||
*/ |
||||
public function Iter Next(); |
||||
|
||||
/** |
||||
* Returns current value pointed to by an iterator. |
||||
* |
||||
* Does not advance iteration: use `Next()` to pick next value. |
||||
* |
||||
* @return Current value being iterated over. If `Iterator()` has finished |
||||
* iterating over all values or was not initialized - returns `none`. |
||||
* Note that depending on context `none` values can also be returned, |
||||
* use `LeaveOnlyNotNone()` method to prevent that. |
||||
*/ |
||||
public function AcediaObject Get(); |
||||
|
||||
/** |
||||
* Checks if caller `Iterator` has finished iterating. |
||||
* |
||||
* @return `true` if caller `Iterator` has finished iterating or |
||||
* was not initialized. `false` otherwise. |
||||
*/ |
||||
public function bool HasFinished(); |
||||
|
||||
/** |
||||
* Makes caller iterator skip any `none` items during iteration. |
||||
* |
||||
* @return Reference to caller `Iterator` to allow for method chaining. |
||||
*/ |
||||
public function Iter LeaveOnlyNotNone(); |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue