diff --git a/sources/BaseAPI/API/Commands/Voting/Voting.uc b/sources/BaseAPI/API/Commands/Voting/Voting.uc
index 0393430..d6b85f4 100644
--- a/sources/BaseAPI/API/Commands/Voting/Voting.uc
+++ b/sources/BaseAPI/API/Commands/Voting/Voting.uc
@@ -20,102 +20,260 @@
* along with Acedia. If not, see .
*/
class Voting extends AcediaObject
- dependsOn(VotingModel);
+ 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.
+//! [`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
+//! 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.
//!
-//! 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.
+//! ## Usage
//!
-//! 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 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.
+//! 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.
///
-/// 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.
+/// 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.
///
-/// 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;
+/// 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 private const string votingStartedLine;
+/// 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 private const string votingSucceededLine;
+/// 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 private const string votingFailedLine;
+/// 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 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 policyAllowedToVoteGroups;
+var private array policyAllowedToSeeVotingGroups;
+var private array policyAllowedToForceVoting;
+var private bool policySpectatorsCanVote;
+
+/// Fake voters that are only used in debug mode to allow for simpler vote
+/// testing.
+var private array debugVoters;
+
+// Timings at which to announce how much time is left for this voting
+var private const array 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() {
- summaryTemplate = _.text.MakeTemplate_S(votesDistributionLine);
- playerVotedTemplate = _.text.MakeTemplate_S(playerVotedLine);
+ 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() {
- forcefullyEnded = false;
- endingHandled = false;
_.memory.Free(model);
- _.memory.Free(summaryTemplate);
- _.memory.Free(playerVotedTemplate);
model = none;
- summaryTemplate = 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;
}
-/// Override this to perform necessary logic after voting has succeeded.
-protected function Execute() {}
+/// Signal that will be emitted when voting ends.
+///
+/// # Slot description
+///
+/// bool (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.
+/// 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"));
+}
+
+/// 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;
@@ -127,81 +285,118 @@ public final static function Text GetPreferredName() {
return none;
}
-/// Starts caller [`Voting`] using policies, loaded from the given config.
-public final function Start(BaseText votingConfigName) {
+/// 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 MutableText howToVoteHint;
- local array voters;
-
- if (model != none) {
- return;
+ 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;
+ }
}
- 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,, P("voting"));
- voters[i].BorrowConsole().WriteLine(F(votingStartedLine));
- voters[i].BorrowConsole().WriteLine(howToVoteHint);
+ if (canForce) {
+ if (model.ForceEnding(type)) {
+ TryEnding(instigator);
+ return FEO_Success;
+ }
+ return FEO_NotApplicable;
}
- _.memory.FreeMany(voters);
- _.memory.Free(howToVoteHint);
+ _.memory.Free(id);
+ return FEO_Forbidden;
}
-// 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;
+/// 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 voters;
- 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));
+ 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;
}
- _.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));
+
+ // 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 {
- result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative));
+ _.memory.FreeMany(voters);
+ return SVR_Rejected;
}
- _.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;
+ // 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;
}
-/// Determines whether the [`Voting`] process has concluded.
+/// Checks if the [`Voting`] process has reached its conclusion.
///
-/// Note that this is different from the voting being active, as voting that has not yet started is
-/// also not concluded.
+/// 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 (forcefullyEnded || model.HasEnded());
+ return model.HasEnded();
}
-/// Returns the current voting status for the specified voter.
+/// 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 count, and this method will return [`PVS_NoVote`].
+/// 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`].
///
-/// 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`].
+/// 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);
@@ -209,12 +404,13 @@ public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) {
return PVS_NoVote;
}
-/// Specifies the [`UserID`]s that will be added as additional voters in debug mode.
+/// Adds specified [`UserID`]s 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.
+/// This method is intended for debugging purposes and only functions when
+/// the game is running in debug mode.
public final function SetDebugVoters(array newDebugVoters) {
local int i;
+ local array realVoters;
if(!_.environment.IsDebugging()) {
return;
@@ -227,108 +423,221 @@ public final function SetDebugVoters(array newDebugVoters) {
newDebugVoters[i].NewRef();
}
}
- _.memory.FreeMany(UpdateVoters());
+ 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.
+/// 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 allVoters;
+ local array realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
if (!_.environment.IsDebugging()) return VFR_NotAllowed;
- allVoters = UpdateVoters();
+ realVoters = FindAllVotingPlayers();
+ UpdateVoters(realVoters);
result = model.CastVote(voter, voteForSuccess);
- if (!TryEnding() && result == VFR_Success) {
- AnnounceNewVote(none, allVoters, voteForSuccess);
+ if (result == VFR_Success) {
+ AnnounceNewVote(none, voteForSuccess);
}
- _.memory.FreeMany(allVoters);
+ TryEnding();
+ _.memory.FreeMany(realVoters);
return result;
}
-/// Casts a vote for the specified player.
+/// Registers a vote on behalf of 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.
+/// 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 bool votingContinues;
local UserID voterID;
- local array allVoters;
+ local array realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
voterID = voter.GetUserID();
- allVoters = UpdateVoters();
+ realVoters = FindAllVotingPlayers();
+ UpdateVoters(realVoters);
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:
- }
+ 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(allVoters);
+ _.memory.FreeMany(realVoters);
return result;
}
/// Prints information about caller [`Voting`] to the given player.
public final function PrintVotingInfoFor(EPlayer requester) {
- local MutableText summaryPart;
+ local ConsoleWriter writer;
+ local MutableText summaryPart, timeRemaining;
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("."));
+ 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);
}
-/// Outputs message about new vote being submitted to all relevant voters.
-private final function AnnounceNewVote(EPlayer voter, array voters, bool voteForSuccess) {
+/// Override this to perform necessary logic after voting has succeeded.
+protected function Execute(HashTable arguments) {}
+
+/// Fill `currentAnnouncements` here!!!
+/// 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 MutableText playerVotedPart, summaryPart;
+ local array allPlayers;
+ local UserID nextID;
+ local MutableText playerVotedPart, playerVotedAnonymousPart, summaryPart;
- summaryTemplate.Reset();
- summaryTemplate.ArgInt(model.GetVotesFor());
- summaryTemplate.ArgInt(model.GetVotesAgainst());
- summaryPart = summaryTemplate.CollectFormattedM();
+ voteSummaryTemplate.Reset();
+ voteSummaryTemplate.ArgInt(model.GetVotesFor());
+ voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
+ summaryPart = voteSummaryTemplate.CollectFormattedM();
playerVotedTemplate.Reset();
+ playerVotedAnonymousTemplate.Reset();
if (voter != none) {
voterName = voter.GetName();
} else {
@@ -338,27 +647,41 @@ private final function AnnounceNewVote(EPlayer voter, array voters, boo
_.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();
- 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);
+ 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() {
- local MutableText outcomeMessage;
+/// 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;
@@ -366,72 +689,175 @@ private final function bool TryEnding() {
endingHandled = true;
if (model.GetStatus() == VPM_Success) {
- outcomeMessage = _.text.FromFormattedStringM(votingSucceededLine);
+ outcomeMessage = currentAnnouncements.succeeded;
} else {
- outcomeMessage = _.text.FromFormattedStringM(votingFailedLine);
+ outcomeMessage = currentAnnouncements.failed;
}
- AnnounceOutcome(outcomeMessage);
+ onVotingEndedSignal.Emit(model.GetStatus() == VPM_Success, usedArguments);
+ AnnounceOutcome(outcomeMessage, forcedBy);
if (model.GetStatus() == VPM_Success) {
- Execute();
+ Execute(usedArguments);
}
- _.memory.Free(outcomeMessage);
+ _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 FindAllVotingPlayers() {
+ local int i, j;
+ local bool userAllowedToVote;
+ local UserID nextID;
+ local array 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 array UpdateVoters() {
+private final function UpdateVoters(array votingPlayers) {
local int i;
- local UserID nextID;
- local array currentPlayers;
- local array potentialVoters;
+ local array votersIDs;
if (model == none) {
- return currentPlayers;
+ 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();
- potentialVoters[potentialVoters.length] = debugVoters[i];
+ 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 currentPlayers;
+
+ howToVoteHint = MakeHowToVoteHint();
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
nextID = currentPlayers[i].GetUserID();
- potentialVoters[potentialVoters.length] = nextID;
+ 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);
+ }
}
- model.UpdatePotentialVoters(potentialVoters);
- _.memory.FreeMany(potentialVoters);
- return currentPlayers;
+ _.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) {
+/// 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 MutableText summaryLine;
+ local Text playerName;
+ local MutableText editedOutcomeMessage, summaryLine;
+ local ConsoleWriter writer;
local array currentPlayers;
if (model == none) {
return;
}
- summaryTemplate.Reset();
- summaryTemplate.ArgInt(model.GetVotesFor());
- summaryTemplate.ArgInt(model.GetVotesAgainst());
- summaryLine = summaryTemplate.CollectFormattedM();
+ 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) {
- currentPlayers[i].BorrowConsole().WriteLine(outcomeMessage);
- currentPlayers[i].BorrowConsole().WriteLine(summaryLine);
- currentPlayers[i].Notify(outcomeMessage, summaryLine,, P("voting"));
+ writer = currentPlayers[i].BorrowConsole();
+ writer.WriteLine(editedOutcomeMessage);
+ writer.WriteLine(summaryLine);
+ currentPlayers[i].Notify(editedOutcomeMessage, summaryLine,, P("voting"));
}
_.memory.FreeMany(currentPlayers);
_.memory.Free(summaryLine);
}
defaultproperties {
+ // You can override these
preferredName = "test"
- infoLine = "Voting for test"
+ votingInfoLine = "Debug voting is running"
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}"
+ 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
}
\ No newline at end of file
diff --git a/sources/BaseAPI/API/Commands/Voting/VotingModel.uc b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc
index 4af8f00..4e5b3eb 100644
--- a/sources/BaseAPI/API/Commands/Voting/VotingModel.uc
+++ b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc
@@ -19,50 +19,30 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see .
*/
-class VotingModel extends AcediaObject;
+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.
+//! 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 [`Initialize()`] to set the required policies.
+//! 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. The method used to recount the votes will depend on
-//! the policies set during the previous [`Initialize()`] call.
+//! 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.
-
-/// Describes how [`VotingModel`] should react when a user performs potentially illegal actions.
-///
-/// Illegal here means that either corresponding operation won't be permitted or any vote made
-/// would be considered invalid.
-///
-/// Leaving means simply no longer being in a potential pool of voters, which includes actually
-/// leaving the game and simply losing rights to vote.
-enum VotingPolicies {
- /// Anything that can be considered illegal actions is prohibited.
- ///
- /// Leaving (or losing rights to vote) during voting will make a vote invalid.
- ///
- /// Changing vote is forbidden.
- VP_Restrictive,
- /// Leaving during voting is allowed. Changing a vote is not allowed.
- VP_CanLeave,
- /// Changing one's vote is allowed. If a user leaves during voting, their vote will be invalid.
- VP_CanChangeVote,
- /// Leaving during voting and changing a vote is allowed. Leaving means losing rights to vote.
- ///
- /// Currently, this policy allows all available options, but this may change in the future if
- /// more options are added.
- VP_CanLeaveAndChangeVote
-};
+//! 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 {
@@ -73,9 +53,7 @@ enum VotingModelStatus {
/// Voting has ended with majority for its success
VPM_Success,
/// Voting has ended with majority for its failure
- VPM_Failure,
- /// Voting has ended in a draw
- VPM_Draw
+ VPM_Failure
};
/// A result of user trying to make a vote
@@ -102,9 +80,20 @@ enum PlayerVoteStatus {
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;
-var private bool policyCanLeave, policyCanChangeVote;
+/// Specifies whether draw would count as a victory for corresponding voting.
+var private bool policyDrawWinsVoting;
var private array votesFor, votesAgainst;
/// Votes of people that voted before, but then were forbidden to vote
@@ -115,8 +104,6 @@ var private array allowedVoters;
protected function Constructor() {
status = VPM_Uninitialized;
- policyCanLeave = false;
- policyCanChangeVote = false;
}
protected function Finalizer() {
@@ -133,35 +120,42 @@ protected function Finalizer() {
}
/// Initializes voting by providing it with a set of policies to follow.
-public final function Initialize(VotingPolicies policies) {
- if (status == VPM_Uninitialized) {
- policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote);
- policyCanChangeVote =
- (policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote);
+///
+/// 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.
+/// 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.
+/// 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.
+/// 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 potentialVoters) {
local int i;
@@ -178,8 +172,7 @@ public final function UpdatePotentialVoters(array potentialVoters) {
/// Attempts to add a vote from specified user.
///
-/// Adding a vote can fail if [`voter`] isn't allowed to vote or has already voted and policies
-/// forbid changing that vote.
+/// 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;
@@ -190,15 +183,12 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
if (!IsVotingAllowedFor(voter)) {
return VFR_NotAllowed;
}
- currentVote = HasVoted(voter);
+ currentVote = GetVote(voter);
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor)
|| (!voteForSuccess && currentVote == PVS_VoteAgainst);
if (votesSameWay) {
return VFR_AlreadyVoted;
}
- if (!policyCanChangeVote && currentVote != PVS_NoVote) {
- return VFR_CannotChangeVote;
- }
EraseVote(voter);
voter.NewRef();
if (voteForSuccess) {
@@ -210,12 +200,11 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
return VFR_Success;
}
-/// Checks if the provided user is allowed to vote based on the current list of potential voters.
+/// 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()`].
-/// However, even if a user is on the list of potential voters, they may not be allowed to vote if
-/// they have already cast a vote and the voting policies do not allow vote changes.
+/// 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) {
@@ -234,9 +223,8 @@ public final function bool IsVotingAllowedFor(UserID voter) {
/// 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 will only count if policies allow voters to leave mid-vote.
-/// Otherwise, the method will return [`PVS_NoVote`].
+/// 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;
@@ -253,95 +241,100 @@ public final function PlayerVoteStatus GetVote(UserID voter) {
return PVS_VoteAgainst;
}
}
- if (policyCanLeave) {
- for (i = 0; i < storedVotesFor.length; i += 1) {
- if (voter.IsEqual(storedVotesFor[i])) {
- return PVS_VoteFor;
- }
- }
- for (i = 0; i < storedVotesAgainst.length; i += 1) {
- if (voter.IsEqual(storedVotesAgainst[i])) {
- return PVS_VoteAgainst;
- }
- }
- }
return PVS_NoVote;
}
/// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() {
- if (policyCanLeave) {
- return votesFor.length + storedVotesFor.length;
- } else {
- return votesFor.length;
- }
+ return votesFor.length;
}
/// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() {
- if (policyCanLeave) {
- return votesAgainst.length + storedVotesAgainst.length;
- } else {
- return votesAgainst.length;
- }
+ return votesAgainst.length;
}
/// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() {
- if (policyCanLeave) {
- return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length;
- } else {
- return allowedVoters.length;
- }
+ return allowedVoters.length;
}
-// Checks if provided user has already voted.
-// Only checks among users that are currently allowed to vote, even if their past vote still counts.
-private final function PlayerVoteStatus HasVoted(UserID voter) {
- local int i;
+/// 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;
- if (voter == none) {
- return PVS_NoVote;
- }
- for (i = 0; i < votesFor.length; i += 1) {
- if (voter.IsEqual(votesFor[i])) {
- return PVS_VoteFor;
- }
+ 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;
}
- for (i = 0; i < votesAgainst.length; i += 1) {
- if (voter.IsEqual(votesAgainst[i])) {
- return PVS_VoteAgainst;
- }
+ 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 PVS_NoVote;
+ return true;
}
private final function RecountVotes() {
- local bool canOverturn, everyoneVoted;
+ local MathApi.IntegerDivisionResult divisionResult;
+ local int winningScore, losingScore;
local int totalPossibleVotes;
- local int totalVotesFor, totalVotesAgainst;
- local int lowerVoteCount, upperVoteCount, undecidedVoteCount;
if (status != VPM_InProgress) {
return;
}
- totalVotesFor = GetVotesFor();
- totalVotesAgainst = GetVotesAgainst();
totalPossibleVotes = GetTotalPossibleVotes();
- lowerVoteCount = Min(totalVotesFor, totalVotesAgainst);
- upperVoteCount = Max(totalVotesFor, totalVotesAgainst);
- undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount);
- everyoneVoted = (undecidedVoteCount <= 0);
- canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount;
- if (everyoneVoted || !canOverturn) {
- if (totalVotesFor > totalVotesAgainst) {
- status = VPM_Success;
- } else if (totalVotesFor < totalVotesAgainst) {
- status = VPM_Failure;
+ 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 {
- status = VPM_Draw;
+ // 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) {