Browse Source
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.pull/12/head
Anton Tarasenko
2 years ago
13 changed files with 1295 additions and 127 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,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" |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class ACommandFakers extends Command |
||||
dependsOn(VotingModel); |
||||
|
||||
var private array<UserID> 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 { |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<Voting> 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 { |
||||
} |
@ -0,0 +1,437 @@
|
||||
/** |
||||
* 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); |
||||
|
||||
//! 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. |
||||
//! |
||||
//! # 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 [`Stop()`] method. |
||||
//! |
||||
//! Note that there is no method to handle ending of [`Voting`], since all necessary logic is |
||||
//! expected to be performed internally. |
||||
//! [`Commands_Feature`] does this check lazily (only when user wants to start another voting) |
||||
//! via [`HasEnded()`] method. |
||||
|
||||
/// Records whether end-of-the voting methods were already called. |
||||
var private bool endingHandled; |
||||
/// Records whether voting was forcefully ended |
||||
var private bool forcefullyEnded; |
||||
/// Underlying voting model that does actual vote calculations. |
||||
var private VotingModel model; |
||||
|
||||
/// Fake voters that are only used in debug mode to allow for simpler vote testing. |
||||
var private array<UserID> debugVoters; |
||||
|
||||
/// Text that serves as a template for announcing current vote counts. Don't change this. |
||||
var private const string votesDistributionLine; |
||||
/// Text that serves as a template for announcing player making a new vote. Don't change this. |
||||
var private const string playerVotedLine; |
||||
// [`TextTemplate`]s made from the above `string` templates. |
||||
var private TextTemplate summaryTemplate, playerVotedTemplate; |
||||
|
||||
// TODO: check these limitations |
||||
/// Name of this voting. |
||||
/// |
||||
/// Has to satisfy limitations described in the `BaseText::IsValidName()` |
||||
var private const string preferredName; |
||||
/// 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 currently active." line, avoid adding formatting and don't |
||||
/// add comma/exclamation mark at the end. |
||||
var private const string infoLine; |
||||
/// 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 private 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 private 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 private const string votingFailedLine; |
||||
|
||||
protected function Constructor() { |
||||
summaryTemplate = _.text.MakeTemplate_S(votesDistributionLine); |
||||
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedLine); |
||||
} |
||||
|
||||
protected function Finalizer() { |
||||
forcefullyEnded = false; |
||||
endingHandled = false; |
||||
_.memory.Free(model); |
||||
_.memory.Free(summaryTemplate); |
||||
_.memory.Free(playerVotedTemplate); |
||||
model = none; |
||||
summaryTemplate = none; |
||||
playerVotedTemplate = none; |
||||
} |
||||
|
||||
/// Override this to perform necessary logic after voting has succeeded. |
||||
protected function Execute() {} |
||||
|
||||
/// 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) { |
||||
builder.OptionalParams(); |
||||
builder.ParamText(P("message")); |
||||
} |
||||
|
||||
/// Returns name of this voting in the lower case. |
||||
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; |
||||
} |
||||
|
||||
/// Starts caller [`Voting`] using policies, loaded from the given config. |
||||
public final function Start(BaseText votingConfigName) { |
||||
local int i; |
||||
local MutableText howToVoteHint; |
||||
local array<EPlayer> voters; |
||||
|
||||
if (model != none) { |
||||
return; |
||||
} |
||||
model = VotingModel(_.memory.Allocate(class'VotingModel')); |
||||
model.Initialize(VP_CanChangeVote); |
||||
voters = UpdateVoters(); |
||||
howToVoteHint = MakeHowToVoteHint(); |
||||
for (i = 0; i < voters.length; i += 1) { |
||||
voters[i].Notify(F(votingStartedLine), howToVoteHint); |
||||
voters[i].BorrowConsole().WriteLine(F(votingStartedLine)); |
||||
voters[i].BorrowConsole().WriteLine(howToVoteHint); |
||||
} |
||||
_.memory.FreeMany(voters); |
||||
_.memory.Free(howToVoteHint); |
||||
} |
||||
|
||||
// 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; |
||||
} |
||||
|
||||
/// Forcefully stops [`Voting`]. |
||||
/// |
||||
/// Only works if [`Voting`] has already started, but didn't yet ended (see [`HasEnded()`]). |
||||
public final function Stop() { |
||||
if (model != none) { |
||||
forcefullyEnded = true; |
||||
} |
||||
} |
||||
|
||||
/// Determines whether the [`Voting`] process has concluded. |
||||
/// |
||||
/// Note that this is different from the voting being active, as voting that has not yet started is |
||||
/// also not concluded. |
||||
public final function bool HasEnded() { |
||||
if (model == none) { |
||||
return false; |
||||
} |
||||
return (forcefullyEnded || model.HasEnded()); |
||||
} |
||||
|
||||
/// Returns 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 count, and this method will return [`PVS_NoVote`]. |
||||
/// |
||||
/// However, if the player regains their voting rights while the voting process is still ongoing, |
||||
/// their previous vote will be automatically restored by the caller [`Voting`]. |
||||
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) { |
||||
if (model != none) { |
||||
return model.GetVote(voter); |
||||
} |
||||
return PVS_NoVote; |
||||
} |
||||
|
||||
/// Specifies the [`UserID`]s that will be added as additional voters in debug mode. |
||||
/// |
||||
/// This method should only be used for debugging purposes and will only function if the game is |
||||
/// running in debug mode. |
||||
public final function SetDebugVoters(array<UserID> newDebugVoters) { |
||||
local int i; |
||||
|
||||
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(); |
||||
} |
||||
} |
||||
_.memory.FreeMany(UpdateVoters()); |
||||
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> allVoters; |
||||
local VotingModel.VotingResult result; |
||||
|
||||
if (model == none) return VFR_NotAllowed; |
||||
if (voter == none) return VFR_NotAllowed; |
||||
if (!_.environment.IsDebugging()) return VFR_NotAllowed; |
||||
|
||||
allVoters = UpdateVoters(); |
||||
result = model.CastVote(voter, voteForSuccess); |
||||
if (!TryEnding() && result == VFR_Success) { |
||||
AnnounceNewVote(none, allVoters, voteForSuccess); |
||||
} |
||||
_.memory.FreeMany(allVoters); |
||||
return result; |
||||
} |
||||
|
||||
/// Casts a vote for the specified player. |
||||
/// |
||||
/// This method will update the voting status for the specified player and may trigger the end of |
||||
/// the voting process. |
||||
/// After a vote is cast, the updated voting status will be broadcast to all players. |
||||
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) { |
||||
local bool votingContinues; |
||||
local UserID voterID; |
||||
local array<EPlayer> allVoters; |
||||
local VotingModel.VotingResult result; |
||||
|
||||
if (model == none) return VFR_NotAllowed; |
||||
if (voter == none) return VFR_NotAllowed; |
||||
|
||||
voterID = voter.GetUserID(); |
||||
allVoters = UpdateVoters(); |
||||
result = model.CastVote(voterID, voteForSuccess); |
||||
votingContinues = !TryEnding(); |
||||
if (votingContinues) { |
||||
switch (result) { |
||||
case VFR_Success: |
||||
AnnounceNewVote(voter, allVoters, 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: |
||||
} |
||||
} |
||||
_.memory.Free(voterID); |
||||
_.memory.FreeMany(allVoters); |
||||
return result; |
||||
} |
||||
|
||||
/// Prints information about caller [`Voting`] to the given player. |
||||
public final function PrintVotingInfoFor(EPlayer requester) { |
||||
local MutableText summaryPart; |
||||
|
||||
if (requester == none) { |
||||
return; |
||||
} |
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryPart = summaryTemplate.CollectFormattedM(); |
||||
requester |
||||
.BorrowConsole() |
||||
.Write(F(infoLine)) |
||||
.Write(P(". ")) |
||||
.Write(summaryPart) |
||||
.WriteLine(P(".")); |
||||
_.memory.Free(summaryPart); |
||||
} |
||||
|
||||
/// Outputs message about new vote being submitted to all relevant voters. |
||||
private final function AnnounceNewVote(EPlayer voter, array<EPlayer> voters, bool voteForSuccess) { |
||||
local int i; |
||||
local Text voterName; |
||||
local MutableText playerVotedPart, summaryPart; |
||||
|
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryPart = summaryTemplate.CollectFormattedM(); |
||||
|
||||
playerVotedTemplate.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}")); |
||||
} else { |
||||
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}")); |
||||
} |
||||
playerVotedPart = playerVotedTemplate.CollectFormattedM(); |
||||
for (i = 0; i < voters.length; i += 1) { |
||||
voters[i] |
||||
.BorrowConsole() |
||||
.Write(playerVotedPart) |
||||
.Write(P(". ")) |
||||
.Write(summaryPart) |
||||
.WriteLine(P(".")); |
||||
} |
||||
_.memory.Free(playerVotedPart); |
||||
_.memory.Free(summaryPart); |
||||
} |
||||
|
||||
/// Tries to end voting. |
||||
/// |
||||
/// Returns `true` iff this method was called for the first time after the voting concluded. |
||||
private final function bool TryEnding() { |
||||
local MutableText outcomeMessage; |
||||
|
||||
if (model == none) return false; |
||||
if (endingHandled) return false; |
||||
if (!HasEnded()) return false; |
||||
|
||||
endingHandled = true; |
||||
if (model.GetStatus() == VPM_Success) { |
||||
outcomeMessage = _.text.FromFormattedStringM(votingSucceededLine); |
||||
} else { |
||||
outcomeMessage = _.text.FromFormattedStringM(votingFailedLine); |
||||
} |
||||
AnnounceOutcome(outcomeMessage); |
||||
if (model.GetStatus() == VPM_Success) { |
||||
Execute(); |
||||
} |
||||
_.memory.Free(outcomeMessage); |
||||
return true; |
||||
} |
||||
|
||||
/// Updates the inner voting model with current list of players allowed to vote. |
||||
/// Also returns said list. |
||||
private final function array<EPlayer> UpdateVoters() { |
||||
local int i; |
||||
local UserID nextID; |
||||
local array<EPlayer> currentPlayers; |
||||
local array<UserID> potentialVoters; |
||||
|
||||
if (model == none) { |
||||
return currentPlayers; |
||||
} |
||||
for (i = 0; i < debugVoters.length; i += 1) { |
||||
debugVoters[i].NewRef(); |
||||
potentialVoters[potentialVoters.length] = debugVoters[i]; |
||||
} |
||||
currentPlayers = _.players.GetAll(); |
||||
for (i = 0; i < currentPlayers.length; i += 1) { |
||||
nextID = currentPlayers[i].GetUserID(); |
||||
potentialVoters[potentialVoters.length] = nextID; |
||||
} |
||||
model.UpdatePotentialVoters(potentialVoters); |
||||
_.memory.FreeMany(potentialVoters); |
||||
return currentPlayers; |
||||
} |
||||
|
||||
/// Prints given voting outcome message in console and publishes it as a notification. |
||||
private final function AnnounceOutcome(BaseText outcomeMessage) { |
||||
local int i; |
||||
local MutableText summaryLine; |
||||
local array<EPlayer> currentPlayers; |
||||
|
||||
if (model == none) { |
||||
return; |
||||
} |
||||
summaryTemplate.Reset(); |
||||
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||
summaryLine = summaryTemplate.CollectFormattedM(); |
||||
currentPlayers = _.players.GetAll(); |
||||
for (i = 0; i < currentPlayers.length; i += 1) { |
||||
currentPlayers[i].BorrowConsole().WriteLine(outcomeMessage); |
||||
currentPlayers[i].BorrowConsole().WriteLine(summaryLine); |
||||
currentPlayers[i].Notify(outcomeMessage, summaryLine); |
||||
} |
||||
_.memory.FreeMany(currentPlayers); |
||||
_.memory.Free(summaryLine); |
||||
} |
||||
|
||||
defaultproperties { |
||||
preferredName = "test" |
||||
infoLine = "Voting for test" |
||||
votingStartedLine = "Test voting has started" |
||||
votingSucceededLine = "{$TextPositive Test voting passed!}" |
||||
votingFailedLine = "{$TextNegative Test voting has failed...}" |
||||
playerVotedLine = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting" |
||||
votesDistributionLine = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}" |
||||
} |
@ -0,0 +1,84 @@
|
||||
class VotingSettings extends FeatureConfig |
||||
perobjectconfig |
||||
config(AcediaVoting); |
||||
|
||||
/// Determines the duration of the voting period, specified in seconds. |
||||
var public config float votingTime; |
||||
/// 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. |
||||
var public config int minimalRoundsToVote; |
||||
/// Determines whether spectators are allowed to vote. |
||||
var public config bool allowSpectatorVoting; |
||||
/// Specifies which group(s) of players are allowed to see who makes what vote. |
||||
var public config array<string> allowedToSeeVotesGroup; |
||||
/// Specifies which group(s) of players are allowed to vote. |
||||
var public config array<string> allowedToVoteGroup; |
||||
|
||||
protected function HashTable ToData() { |
||||
local int i; |
||||
local HashTable data; |
||||
local ArrayList arrayOfTexts; |
||||
|
||||
data = __().collections.EmptyHashTable(); |
||||
data.SetFloat(P("votingTime"), votingTime); |
||||
data.SetInt(P("minimalRoundsToVote"), minimalRoundsToVote); |
||||
data.SetBool(P("allowSpectatorVoting"), allowSpectatorVoting); |
||||
|
||||
arrayOfTexts = _.collections.EmptyArrayList(); |
||||
for (i = 0; i < allowedToSeeVotesGroup.length; i += 1) { |
||||
arrayOfTexts.AddString(allowedToSeeVotesGroup[i]); |
||||
} |
||||
data.SetItem(P("allowedToSeeVotesGroup"), arrayOfTexts); |
||||
_.memory.Free(arrayOfTexts); |
||||
|
||||
arrayOfTexts = _.collections.EmptyArrayList(); |
||||
for (i = 0; i < allowedToVoteGroup.length; i += 1) { |
||||
arrayOfTexts.AddString(allowedToVoteGroup[i]); |
||||
} |
||||
data.SetItem(P("allowedToVoteGroup"), arrayOfTexts); |
||||
_.memory.Free(arrayOfTexts); |
||||
return data; |
||||
} |
||||
|
||||
protected function FromData(HashTable source) { |
||||
local int i; |
||||
local ArrayList arrayOfTexts; |
||||
|
||||
if (source == none) { |
||||
return; |
||||
} |
||||
votingTime = source.GetFloat(P("votingTime"), 30.0); |
||||
minimalRoundsToVote = source.GetInt(P("minimalRoundsToVote"), 1); |
||||
allowSpectatorVoting = source.GetBool(P("allowSpectatorVoting"), false); |
||||
|
||||
allowedToSeeVotesGroup.length = 0; |
||||
arrayOfTexts = source.GetArrayList(P("allowedToSeeVotesGroup")); |
||||
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||
allowedToSeeVotesGroup[allowedToSeeVotesGroup.length] = arrayOfTexts.GetString(i); |
||||
} |
||||
_.memory.Free(arrayOfTexts); |
||||
|
||||
allowedToVoteGroup.length = 0; |
||||
arrayOfTexts = source.GetArrayList(P("allowedToVoteGroup")); |
||||
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||
allowedToVoteGroup[allowedToVoteGroup.length] = arrayOfTexts.GetString(i); |
||||
} |
||||
_.memory.Free(arrayOfTexts); |
||||
} |
||||
|
||||
protected function DefaultIt() { |
||||
votingTime = 30.0; |
||||
minimalRoundsToVote = 1; |
||||
allowSpectatorVoting = false; |
||||
allowedToSeeVotesGroup.length = 0; |
||||
allowedToVoteGroup.length = 0; |
||||
allowedToVoteGroup[0] = "everybody"; |
||||
} |
||||
|
||||
defaultproperties { |
||||
configName = "AcediaVoting" |
||||
} |
Loading…
Reference in new issue