From 0432c1c07410eade282cd7a89880be4319c7e55c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Thu, 30 Mar 2023 00:40:31 +0700 Subject: [PATCH] Add basic voting functionality Just a basic implementation with some temporary data in the future base class for testing purposes. Doesn't yet respect any voting settings, but supports fake voters for debugging. --- config/AcediaAliases_Colors.ini | 2 +- config/AcediaAliases_Commands.ini | 8 + config/AcediaSystem.ini | 5 +- config/AcediaVoting.ini | 29 ++ .../BuiltInCommands/ACommandFakers.uc | 166 +++++++ .../Commands/BuiltInCommands/ACommandVote.uc | 159 +++++++ sources/BaseAPI/API/Commands/Command.uc | 2 +- .../BaseAPI/API/Commands/Commands_Feature.uc | 430 ++++++++++++----- sources/BaseAPI/API/Commands/Voting/Voting.uc | 437 ++++++++++++++++++ .../API/Commands/Voting/VotingModel.uc | 84 +++- .../API/Commands/Voting/VotingSettings.uc | 84 ++++ .../AcediaEnvironment/AcediaEnvironment.uc | 12 + sources/Color/ColorAPI.uc | 4 +- 13 files changed, 1295 insertions(+), 127 deletions(-) create mode 100644 config/AcediaVoting.ini create mode 100644 sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc create mode 100644 sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc create mode 100644 sources/BaseAPI/API/Commands/Voting/Voting.uc create mode 100644 sources/BaseAPI/API/Commands/Voting/VotingSettings.uc diff --git a/config/AcediaAliases_Colors.ini b/config/AcediaAliases_Colors.ini index 913130d..ed48d09 100644 --- a/config/AcediaAliases_Colors.ini +++ b/config/AcediaAliases_Colors.ini @@ -8,7 +8,7 @@ record=(alias="TextSubHeader",value="rgb(147,112,219)") record=(alias="TextPositive",value="rgb(60,220,20)") record=(alias="TextNeutral",value="rgb(255,255,0)") record=(alias="TextNegative",value="rgb(220,20,60)") -record=(alias="TextSubtle",value="rgb(128,128,128)") +record=(alias="TextSubtle",value="rgb(211,211,211)") record=(alias="TextEmphasis",value="rgb(0,128,255)") record=(alias="TextOk",value="rgb(0,255,0)") record=(alias="TextWarning",value="rgb(255,128,0)") diff --git a/config/AcediaAliases_Commands.ini b/config/AcediaAliases_Commands.ini index e69de29..012eb51 100644 --- a/config/AcediaAliases_Commands.ini +++ b/config/AcediaAliases_Commands.ini @@ -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" \ No newline at end of file diff --git a/config/AcediaSystem.ini b/config/AcediaSystem.ini index 5bfd2d6..a42cf4e 100644 --- a/config/AcediaSystem.ini +++ b/config/AcediaSystem.ini @@ -1,5 +1,8 @@ ; Every single option in this config should be considered [ADVANCED]. ; DO NOT CHANGE THEM unless you are sure you know what you're doing. +[AcediaCore.AcediaEnvironment] +debugMode=false + [AcediaCore.SideEffects] ; Acedia requires adding its own `GameRules` to listen to many different ; game events. @@ -150,7 +153,7 @@ maximumNotifyTime=20 TextDefault=(R=255,G=255,B=255,A=255) TextHeader=(R=128,G=0,B=128,A=255) TextSubHeader=(R=147,G=112,B=219,A=255) -TextSubtle=(R=128,G=128,B=128,A=255) +TextSubtle=(R=211,G=211,B=211,A=255) TextEmphasis=(R=0,G=128,B=255,A=255) TextPositive=(R=0,G=128,B=0,A=255) TextNeutral=(R=255,G=255,B=0,A=255) diff --git a/config/AcediaVoting.ini b/config/AcediaVoting.ini new file mode 100644 index 0000000..1326e6d --- /dev/null +++ b/config/AcediaVoting.ini @@ -0,0 +1,29 @@ +[default VotingSettings] +;= Determines the duration of the voting period, specified in seconds. +votingTime=30 +;= Minimal amount of rounds player has to play before being allowed to vote. +;= If every player played the same amount of rounds, then voting is allowed even if they played +;= less that specified value +;= `0` to disable. +minimalRoundsToVote=1 +;= 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 +minimalRoundsToVote=0 +allowSpectatorVoting=true +allowedToSeeVotesGroup="admin" +allowedToVoteGroup="moderator" +allowedToVoteGroup="admin" + +[admin VotingSettings] +votingTime=30 +minimalRoundsToVote=0 +allowSpectatorVoting=true +allowedToVoteGroup="admin" \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc new file mode 100644 index 0000000..9e146bd --- /dev/null +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc @@ -0,0 +1,166 @@ +/** + * 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 . + */ +class ACommandFakers extends Command + dependsOn(VotingModel); + +var private array fakers; + +protected function BuildData(CommandDataBuilder builder) { + builder.Name(P("fakers")); + 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) { + 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 function UpdateFakersForVoting() { + local Voting currentVoting; + + currentVoting = GetCurrentVoting(); + if (currentVoting != none) { + currentVoting.SetDebugVoters(fakers); + } + _.memory.Free(currentVoting); +} + +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 = 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; + + 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; + } + UpdateFakersForVoting(); +} + +private function Voting GetCurrentVoting() { + local Commands_Feature feature; + local Voting result; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) { + result = feature.GetCurrentVoting(); + feature.FreeSelf(); + } + return result; +} + +private function DisplayCurrentFakers() { + local int i; + local VotingModel.PlayerVoteStatus nextVoteStatus; + local MutableText nextNumber; + local Voting currentVoting; + + if (fakers.length <= 0) { + callerConsole.WriteLine(P("No fakers!")); + return; + } + currentVoting = 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 { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc new file mode 100644 index 0000000..d11f7e5 --- /dev/null +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc @@ -0,0 +1,159 @@ +/** + * 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 . + */ +class ACommandVote extends Command; + +var private CommandDataBuilder dataBuilder; + +protected function Constructor() { + ResetVotingInfo(); +} + +protected function Finalizer() { + super.Finalizer(); + _.memory.Free(dataBuilder); + dataBuilder = none; +} + +protected function BuildData(CommandDataBuilder builder) { + builder.Name(P("vote")); + builder.Group(P("core")); + builder.Summary(P("Allows players to initiate any available voting." + @ "Votings themselves are added 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.")); +} + +protected function Executed(CallData arguments, EPlayer instigator) { + local Voting currentVoting; + local Commands_Feature feature; + + feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature == none) { + callerConsole + .UseColor(_.color.TextFailure) + .WriteLine(P("Feature responsible for commands and voting isn't enabled." + @ "This is unexpected, something broke terribly.")); + return; + } else { + currentVoting = feature.GetCurrentVoting(); + } + if (arguments.subCommandName.IsEmpty()) { + DisplayInfoAboutVoting(instigator, currentVoting); + } else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) { + CastVote(currentVoting, instigator, true); + } else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) { + CastVote(currentVoting, instigator, false); + } else { + StartVoting(arguments.subCommandName, feature, currentVoting, instigator); + } + _.memory.Free(feature); + _.memory.Free(currentVoting); +} + +/// Adds sub-command information about given voting with a given name. +public final function AddVotingInfo(BaseText processName, class processClass) { + if (processName == none) return; + if (processClass == none) return; + if (dataBuilder == none) return; + + dataBuilder.SubCommand(processName); + processClass.static.AddInfo(dataBuilder); + commandData = dataBuilder.BorrowData(); +} + +/// 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 StartVoting( + BaseText votingName, + Commands_Feature feature, + Voting currentVoting, + EPlayer instigator +) { + local Command fakersCommand; + local Voting newVoting; + local Commands_Feature.StartVotingResult result; + + result = feature.StartVoting(votingName); + // Handle errors + if (result == SVR_UnknownVoting) { + callerConsole + .UseColor(_.color.TextFailure) + .Write(P("Unknown voting option \"")) + .Write(votingName) + .WriteLine(P("\"")); + return; + } else if (result == SVR_AlreadyInProgress) { + callerConsole + .UseColor(_.color.TextWarning) + .WriteLine(P("Another voting is already in progress!")); + return; + } + // Inform new voting about fake voters, in case we're debugging + if (currentVoting == none && _.environment.IsDebugging()) { + fakersCommand = feature.GetCommand(P("fakers")); + if (fakersCommand != none && fakersCommand.class == class'ACommandFakers') { + ACommandFakers(fakersCommand).UpdateFakersForVoting(); + } + _.memory.Free(fakersCommand); + } + // Cast a vote from instigator + newVoting = feature.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); +} + +defaultproperties { +} \ No newline at end of file diff --git a/sources/BaseAPI/API/Commands/Command.uc b/sources/BaseAPI/API/Commands/Command.uc index 0c60987..c46c831 100644 --- a/sources/BaseAPI/API/Commands/Command.uc +++ b/sources/BaseAPI/API/Commands/Command.uc @@ -209,7 +209,7 @@ struct Data { var protected array