rott/kf_sources/AcediaCore/Classes/Voting.uc
2026-07-14 20:27:09 +07:00

863 lines
34 KiB
Ucode

/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Voting extends AcediaObject
dependsOn(VotingModel)
dependson(CommandAPI);
//! Class that describes a single voting option.
//!
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and
//! shouldn't be used separately from it.
//! You shouldn't allocate its instances directly unless you're working on
//! the [`Commands_Feature`]'s or related code.
//!
//! Generally, [`Voting`] will only update whenever its methods are called.
//! The only exception is when a time limit was specified, then
//! `TryAnnounceTimer()` will be called every tick, voting emulating countdown.
//!
//! ## Usage
//!
//! This class takes care of the voting process by itself, one only needs to
//! call [`Start()`] method.
//! If you wish to prematurely end voting (e.g. forcing it to end), then call
//! [`ForceEnding()`] method.
//!
//! When implementing your own voting:
//!
//! 1. You normally would override [`Execute()`] method to perform any
//! actions you want upon the voting success.
//! 2. If you want your voting to take any arguments, you should also
//! overload [`AddInfo()`].
//! 3. You can also override [`HandleVotingStart()`] method to setup custom
//! voting's messages inside `currentAnnouncements` or to reject
//! starting this voting altogether.
/// Describes possible results of [`ForceEnding()`] method.
enum ForceEndingOutcome {
/// Voting forcing was successful.
FEO_Success,
/// User is not allowed to force voting.
FEO_Forbidden,
/// No voting to end at this moment.
FEO_NotApplicable
};
/*******************************************************************************
* Voting settings that should be specified for child classes.
******************************************************************************/
/// During its lifecycle voting outputs several messages to players about its
/// current state.
/// This struct contains all such messages.
struct VotingAnnouncementSet {
/// Message that is displayed when voting starts, inviting others to vote
/// (e.g. "Voting to end trader has started")
var public Text started;
/// Message that is displayed once voting succeeds
/// (e.g. "{$TextPositive Voting to end trader was successful}")
var public Text succeeded;
/// Message that is displayed once voting succeeds
/// (e.g. "{$TextNegative Voting to end trader has failed}")
var public Text failed;
/// Message that is displayed when voting info is displayed mid-voting.
/// (e.g. "Voting to end trader currently active.")
var public Text info;
};
/// Variable that contains current messages that voting will use to communicate
/// its status to the players.
///
/// It is auto-filled with `string` values [`votingStartedLine`],
/// [`votingSucceededLine`], [`votingFailedLine`], [`votingInfoLine`].
/// If you want to change/customize these values based on voting arguments,
/// then override [`HandleVotingStart()`] method and set values inside of this
/// variable directly (they will all be `none` at this point).
var protected VotingAnnouncementSet currentAnnouncements;
/// Preferred name of this voting. Actual name is decided by
/// server owner/mod author.
///
/// Has to satisfy limitations described in the `BaseText::IsValidName()`
var protected const string preferredName;
/// Text that should be displayed when voting starts.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader has started" line, avoid adding formatting and
/// don't add comma/exclamation mark at the end.
var protected const string votingStartedLine;
/// Text that should be displayed when voting has ended with a success.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextPositive Voting to end trader was successful}" line, coloring
/// it in a positive color and adding comma/exclamation mark at the end.
var protected const string votingSucceededLine;
/// Text that should be displayed when voting has ended in a failure.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "{$TextNegative Voting to end trader has failed}" line, coloring it in
/// a negative color and adding comma/exclamation mark at the end.
var protected const string votingFailedLine;
/// Text that should be displayed when voting info is displayed mid-voting.
///
/// There isn't any hard limitations, but for the sake of uniformity try to
/// mimic "Voting to end trader is currently active." line, avoid adding
/// formatting and don't add comma/exclamation mark at the end.
var protected const string votingInfoLine;
/// Settings variable that defines a class to be used for this [`Voting`]'s
/// permissions config.
var protected const class<VotingPermissions> permissionsConfigClass;
/*******************************************************************************
* Variables that describe current state of the voting.
******************************************************************************/
/// Underlying voting model that does actual vote calculations.
var private VotingModel model;
/// How much time remains in the voting.
/// Both negative and zero values mean that countdown either ended or wasn't
/// started to begin with.
/// This value can only be decreased inside [`TryAnnounceTimer()`] event method;
/// voting end due to countdown is also expected to be handled there.
var private float remainingVotingTime;
/// Tracks index of the next timing inside [`announcementTimings`] to announce.
var private int nextTimingToAnnounce;
/// Records whether end of the voting announcement was already made.
var private bool endingHandled;
/// Arguments that this voting was called with.
var private HashTable usedArguments;
var private array<Text> policyAllowedToVoteGroups;
var private array<Text> policyAllowedToSeeVotingGroups;
var private array<Text> policyAllowedToForceVoting;
var private bool policySpectatorsCanVote;
/// Fake voters that are only used in debug mode to allow for simpler vote
/// testing.
var private array<UserID> debugVoters;
// Timings at which to announce how much time is left for this voting
var private const array<int> announcementTimings;
/*******************************************************************************
* Auxiliary variables (`string`s + templates from them) used for producing
* output to the user.
******************************************************************************/
/// Text that serves as a template for announcing current vote counts.
var private const string voteSummaryTemplateString;
/// Text that serves as a template for announcing player making a new vote.
var private const string playerVotedTemplateString, playerVotedAnonymousTemplateString;
var private const string timeRemaningAnnounceTemplateString;
// [`TextTemplate`]s made from the above `string` templates.
var private TextTemplate voteSummaryTemplate, playerVotedTemplate, playerVotedAnonymousTemplate;
var private TextTemplate timeRemaningAnnounceTemplate;
/// Text that is used instead of how to vote hint for players not allowed
/// to vote
var private const string cannotVoteHint;
/*******************************************************************************
* Signals.
******************************************************************************/
var private CommandsAPI_OnVotingEnded_Signal onVotingEndedSignal;
protected function Constructor() {
nextTimingToAnnounce = 0;
voteSummaryTemplate = _.text.MakeTemplate_S(voteSummaryTemplateString);
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedTemplateString);
playerVotedAnonymousTemplate = _.text.MakeTemplate_S(playerVotedAnonymousTemplateString);
timeRemaningAnnounceTemplate = _.text.MakeTemplate_S(timeRemaningAnnounceTemplateString);
onVotingEndedSignal = CommandsAPI_OnVotingEnded_Signal(
_.memory.Allocate(class'CommandsAPI_OnVotingEnded_Signal'));
}
protected function Finalizer() {
_.memory.Free(model);
model = none;
endingHandled = false;
policySpectatorsCanVote = false;
_.memory.Free2(usedArguments, onVotingEndedSignal);
usedArguments = none;
onVotingEndedSignal = none;
_server.unreal.OnTick(self).Disconnect();
_.memory.Free4(currentAnnouncements.started,
currentAnnouncements.succeeded,
currentAnnouncements.failed,
currentAnnouncements.info);
currentAnnouncements.started = none;
currentAnnouncements.succeeded = none;
currentAnnouncements.failed = none;
currentAnnouncements.info = none;
_.memory.Free4(voteSummaryTemplate,
playerVotedTemplate,
playerVotedAnonymousTemplate,
timeRemaningAnnounceTemplate);
voteSummaryTemplate = none;
playerVotedTemplate = none;
playerVotedAnonymousTemplate = none;
timeRemaningAnnounceTemplate = none;
_.memory.FreeMany(policyAllowedToVoteGroups);
_.memory.FreeMany(policyAllowedToSeeVotingGroups);
_.memory.FreeMany(policyAllowedToForceVoting);
policyAllowedToVoteGroups.length = 0;
policyAllowedToSeeVotingGroups.length = 0;
policyAllowedToForceVoting.length = 0;
}
/// Signal that will be emitted when voting ends.
///
/// # Slot description
///
/// bool <slot>(bool success, HashTable arguments)
///
/// ## Parameters
///
/// * [`success`]: `true` if voting ended successfully and `false` otherwise.
/// * [`arguments`]: Arguments with which voting was called.
public /*signal*/ function CommandsAPI_OnVotingEnded_Slot OnVotingEnded(AcediaObject receiver) {
return CommandsAPI_OnVotingEnded_Slot(onVotingEndedSignal.NewSlot(receiver));
}
/// Override this to specify arguments for your voting command.
///
/// This method is for adding arguments only.
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or
/// [`CommandDataBuilder::Option()`] methods, otherwise you'll cause unexpected
/// behavior for your mod's users.
public static function AddInfo(CommandDataBuilder builder) {
}
/// Loads permissions config with a given name for the caller [`Voting`] class.
///
/// Permission configs describe allowed usage of the [`Voting`].
/// Basic settings are contained inside [`VotingPermissions`], but votings
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Voting`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function VotingPermissions LoadConfig(BaseText configName) {
if (configName == none) return none;
if (default.permissionsConfigClass == none) return none;
default.permissionsConfigClass.static.Initialize();
// This creates default config if it is missing
default.permissionsConfigClass.static.NewConfig(configName);
return VotingPermissions(default.permissionsConfigClass.static
.GetConfigInstance(configName));
}
/// Returns name of this voting in the lower case.
///
/// If voting class was configured incorrectly (with a `preferredName`
/// that doesn't satisfy limitations, described in `BaseText::IsValidName()`),
/// then this method will return `none`.
public final static function Text GetPreferredName() {
local Text result;
result = __().text.FromString(Locs(default.preferredName));
if (result.IsValidName()) {
return result;
}
__().memory.Free(result);
return none;
}
/// Forcibly ends the voting, deciding winner depending on the argument.
/// By default decides result by the votes that already have been cast.
///
/// Only does anything if voting is currently in progress
/// (in `VPM_InProgress` state).
public final function ForceEndingOutcome ForceEnding(
EPlayer instigator,
VotingModel.ForceEndingType type
) {
local int i;
local UserID id;
local bool canForce;
if (model == none) return FEO_NotApplicable;
if (instigator == none) return FEO_Forbidden;
id = instigator.GetUserID();
if (id == none) return FEO_Forbidden;
for (i = 0; i < policyAllowedToForceVoting.length; i += 1) {
if (_.users.IsUserIDInGroup(id, policyAllowedToForceVoting[i])) {
canForce = true;
break;
}
}
if (canForce) {
if (model.ForceEnding(type)) {
TryEnding(instigator);
return FEO_Success;
}
return FEO_NotApplicable;
}
_.memory.Free(id);
return FEO_Forbidden;
}
/// Starts caller [`Voting`] using policies, loaded from the given config.
///
/// Provided config instance must not be `none`, otherwise method is guaranteed
/// to fail with `SVR_InvalidState`.
/// Method will also fail if voting was already started (even if it already
/// ended), there is no one eligible to vote or [`Voting`] itself has decided to
/// reject being started at this moment, with given arguments.
public final function CommandAPI.StartVotingResult Start(
VotingPermissions config,
HashTable arguments
) {
local bool hasDebugVoters;
local array<EPlayer> voters;
if (model != none) return SVR_InvalidState;
if (config == none) return SVR_InvalidState;
// Check whether we even have enough voters
ReadConfigIntoPolicies(config); // we need to know permission policies
voters = FindAllVotingPlayers();
hasDebugVoters = _.environment.IsDebugging()
&& class'ACommandFakers'.static.BorrowDebugVoters().length > 0;
if (voters.length == 0 && !hasDebugVoters) {
return SVR_NoVoters;
}
// Check if voting even wants to start with these arguments
if (HandleVotingStart(config, arguments)) {
// ^ this was supposed to pre-fill `currentAnnouncements` struct if
// it wanted to change any messages, so now is the good time to fill
// the rest with defaults/fallback
FillAnnouncementGaps();
if (arguments != none) {
arguments.NewRef();
usedArguments = arguments;
}
} else {
_.memory.FreeMany(voters);
return SVR_Rejected;
}
// Actually start voting
model = VotingModel(_.memory.Allocate(class'VotingModel'));
model.Start(config.drawEqualsSuccess);
// Inform new voting about fake voters, in case we're debugging
if (hasDebugVoters) {
// This method will call also `UpdateVoters()`
SetDebugVoters(class'ACommandFakers'.static.BorrowDebugVoters());
} else {
UpdateVoters(voters);
}
SetupCountdownTimer(config);
AnnounceStart();
_.memory.FreeMany(voters);
return SVR_Success;
}
/// Checks if the [`Voting`] process has reached its conclusion.
///
/// Please note that this differs from determining whether voting is currently
// active. Even voting that hasn't started is not considered concluded.
public final function bool HasEnded() {
if (model == none) {
return false;
}
return model.HasEnded();
}
/// Retrieves the current voting status for the specified voter.
///
/// If the voter was previously eligible to vote, cast a vote, but later had
/// their voting rights revoked, their vote will not be counted, and this method
/// will return [`PVS_NoVote`].
///
/// In case the voter regains their voting rights while the voting process is
/// still ongoing, their previous vote will be automatically reinstated by
/// the caller [`Voting`].
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) {
if (model != none) {
return model.GetVote(voter);
}
return PVS_NoVote;
}
/// Adds specified [`UserID`]s as additional voters in debug mode.
///
/// This method is intended for debugging purposes and only functions when
/// the game is running in debug mode.
public final function SetDebugVoters(array<UserID> newDebugVoters) {
local int i;
local array<EPlayer> realVoters;
if(!_.environment.IsDebugging()) {
return;
}
_.memory.FreeMany(debugVoters);
debugVoters.length = 0;
for (i = 0; i < newDebugVoters.length; i += 1) {
if (newDebugVoters[i] != none) {
debugVoters[debugVoters.length] = newDebugVoters[i];
newDebugVoters[i].NewRef();
}
}
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
_.memory.FreeMany(realVoters);
TryEnding();
}
/// Adds a new vote by a given [`UserID`].
///
/// NOTE: this method is intended for use only in debug mode, and is will not do
/// anything otherwise. This method silently adds a vote using the provided
/// [`UserID`], without any prompt or notification of updated voting status.
/// It was added to facilitate testing with fake [`UserID`]s, and is limited
/// to debug mode to prevent misuse and unintended behavior in production code.
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) {
local array<EPlayer> realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
if (!_.environment.IsDebugging()) return VFR_NotAllowed;
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
result = model.CastVote(voter, voteForSuccess);
if (result == VFR_Success) {
AnnounceNewVote(none, voteForSuccess);
}
TryEnding();
_.memory.FreeMany(realVoters);
return result;
}
/// Registers a vote on behalf of the specified player.
///
/// This method updates the voting status for the specified player and may
/// initiate the conclusion of the voting process.
/// After a vote is registered, the updated voting status is broadcast to all
/// players.
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) {
local UserID voterID;
local array<EPlayer> realVoters;
local VotingModel.VotingResult result;
if (model == none) return VFR_NotAllowed;
if (voter == none) return VFR_NotAllowed;
voterID = voter.GetUserID();
realVoters = FindAllVotingPlayers();
UpdateVoters(realVoters);
result = model.CastVote(voterID, voteForSuccess);
switch (result) {
case VFR_Success:
AnnounceNewVote(voter, voteForSuccess);
break;
case VFR_NotAllowed:
voter
.BorrowConsole()
.WriteLine(F("You are {$TextNegative not allowed} to vote right now."));
break;
case VFR_CannotChangeVote:
voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}."));
break;
case VFR_VotingEnded:
voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!"));
break;
default:
}
TryEnding();
_.memory.Free(voterID);
_.memory.FreeMany(realVoters);
return result;
}
/// Prints information about caller [`Voting`] to the given player.
public final function PrintVotingInfoFor(EPlayer requester) {
local ConsoleWriter writer;
local MutableText summaryPart, timeRemaining;
if (requester == none) {
return;
}
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryPart = voteSummaryTemplate.CollectFormattedM();
writer = requester.BorrowConsole();
writer.Write(currentAnnouncements.info);
writer.Write(P(". "));
writer.Write(summaryPart);
writer.WriteLine(P("."));
if (remainingVotingTime > 0) {
timeRemaining = _.text.FromIntM(int(Ceil(remainingVotingTime)));
writer.Write(P("Time remaining: "));
writer.Write(timeRemaining);
writer.WriteLine(P(" seconds."));
_.memory.Free(timeRemaining);
}
_.memory.Free(summaryPart);
}
/// Override this to perform necessary logic after voting has succeeded.
protected function Execute(HashTable arguments) {}
/// Override this method to:
///
/// 1. Specify any of the messages inside `currentAnnouncements` to fit passed
/// [`arguments`].
/// 2. Optionally reject starting this voting altogether by returning `false`
/// (returning `true` will allow voting to proceed).
protected function bool HandleVotingStart(VotingPermissions config, HashTable arguments) {
return true;
}
// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint.
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases
// aren't properly setup.
private final function MutableText MakeHowToVoteHint() {
local Text resolvedAlias;
local MutableText result;
result = P("Say ").MutableCopy();
resolvedAlias = _.alias.ResolveCommand(P("yes"));
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) {
result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive));
} else {
result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive));
}
_.memory.Free(resolvedAlias);
result.Append(P(" or "));
resolvedAlias = _.alias.ResolveCommand(P("no"));
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) {
result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative));
} else {
result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative));
}
_.memory.Free(resolvedAlias);
result.Append(P(" to vote"));
return result;
}
private final function ReadConfigIntoPolicies(VotingPermissions config) {
local int i;
if (config != none) {
policySpectatorsCanVote = config.allowSpectatorVoting;
for (i = 0; i < config.allowedToVoteGroup.length; i += 1) {
policyAllowedToVoteGroups[i] = _.text.FromString(config.allowedToVoteGroup[i]);
}
for (i = 0; i < config.allowedToSeeVotesGroup.length; i += 1) {
policyAllowedToSeeVotingGroups[i] = _.text.FromString(config.allowedToSeeVotesGroup[i]);
}
for (i = 0; i < config.allowedToForceGroup.length; i += 1) {
policyAllowedToForceVoting[i] = _.text.FromString(config.allowedToForceGroup[i]);
}
}
}
private final function SetupCountdownTimer(VotingPermissions config) {
if (config != none && config.votingTime > 0) {
remainingVotingTime = config.votingTime;
_server.unreal.OnTick(self).connect = TryAnnounceTimer;
nextTimingToAnnounce = 0;
while (nextTimingToAnnounce < announcementTimings.length) {
if (announcementTimings[nextTimingToAnnounce] <= remainingVotingTime) {
break;
}
nextTimingToAnnounce += 1;
}
}
}
private function TryAnnounceTimer(float delta, float dilationCoefficient) {
local MutableText message;
local ConsoleWriter writer;
if (remainingVotingTime <= 0) {
return;
}
remainingVotingTime -= delta / dilationCoefficient;
if (remainingVotingTime <= 0) {
model.ForceEnding();
TryEnding();
return;
}
if (nextTimingToAnnounce >= announcementTimings.length) {
return;
}
if (announcementTimings[nextTimingToAnnounce] > int(remainingVotingTime)) {
writer = _.console.ForAll();
timeRemaningAnnounceTemplate.Reset();
timeRemaningAnnounceTemplate.ArgInt(announcementTimings[nextTimingToAnnounce]);
message = timeRemaningAnnounceTemplate.CollectFormattedM();
writer.WriteLine(message);
_.memory.Free(writer);
nextTimingToAnnounce += 1;
}
}
/// Outputs message about new vote being submitted to all relevant voters.
private final function AnnounceNewVote(EPlayer voter, bool voteForSuccess) {
local int i, j;
local bool playerAllowedToSee;
local Text voterName;
local array<EPlayer> allPlayers;
local UserID nextID;
local MutableText playerVotedPart, playerVotedAnonymousPart, summaryPart;
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryPart = voteSummaryTemplate.CollectFormattedM();
playerVotedTemplate.Reset();
playerVotedAnonymousTemplate.Reset();
if (voter != none) {
voterName = voter.GetName();
} else {
voterName = P("DEBUG:FAKER").Copy();
}
playerVotedTemplate.TextArg(P("player_name"), voterName, true);
_.memory.Free(voterName);
if (voteForSuccess) {
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextPositive for}"));
} else {
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
playerVotedAnonymousTemplate.TextArg(P("vote_type"), F("{$TextNegative against}"));
}
playerVotedPart = playerVotedTemplate.CollectFormattedM();
playerVotedAnonymousPart = playerVotedAnonymousTemplate.CollectFormattedM();
allPlayers = _.players.GetAll();
for (i = 0; i < allPlayers.length; i += 1) {
nextID = allPlayers[i].GetUserID();
playerAllowedToSee = false;
for (j = 0; j < policyAllowedToSeeVotingGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToSeeVotingGroups[j])) {
playerAllowedToSee = true;
break;
}
}
if (playerAllowedToSee) {
allPlayers[i].BorrowConsole().Write(playerVotedPart);
} else {
allPlayers[i].BorrowConsole().Write(playerVotedAnonymousPart);
}
allPlayers[i].BorrowConsole().Write(P(". ")).Write(summaryPart).WriteLine(P("."));
_.memory.Free(nextID);
}
_.memory.Free3(playerVotedPart, playerVotedAnonymousPart, summaryPart);
_.memory.FreeMany(allPlayers);
}
/// Tries to end voting.
///
/// Returns `true` iff this method was called for the first time after
/// the voting concluded.
private final function bool TryEnding(optional EPlayer forcedBy) {
local Text outcomeMessage;
if (model == none) return false;
if (endingHandled) return false;
if (!HasEnded()) return false;
endingHandled = true;
if (model.GetStatus() == VPM_Success) {
outcomeMessage = currentAnnouncements.succeeded;
} else {
outcomeMessage = currentAnnouncements.failed;
}
onVotingEndedSignal.Emit(model.GetStatus() == VPM_Success, usedArguments);
AnnounceOutcome(outcomeMessage, forcedBy);
if (model.GetStatus() == VPM_Success) {
Execute(usedArguments);
}
_server.unreal.OnTick(self).Disconnect();
return true;
}
private final function FillAnnouncementGaps() {
if (currentAnnouncements.started == none) {
currentAnnouncements.started = _.text.FromFormattedString(votingStartedLine);
}
if (currentAnnouncements.succeeded == none) {
currentAnnouncements.succeeded = _.text.FromFormattedString(votingSucceededLine);
}
if (currentAnnouncements.failed == none) {
currentAnnouncements.failed = _.text.FromFormattedString(votingFailedLine);
}
if (currentAnnouncements.info == none) {
currentAnnouncements.info = _.text.FromFormattedString(votingInfoLine);
}
}
private final function array<EPlayer> FindAllVotingPlayers() {
local int i, j;
local bool userAllowedToVote;
local UserID nextID;
local array<EPlayer> currentPlayers, voterPlayers;
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
if (!policySpectatorsCanVote && currentPlayers[i].IsSpectator()) {
continue;
}
nextID = currentPlayers[i].GetUserID();
userAllowedToVote = false;
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
userAllowedToVote = true;
break;
}
}
if (userAllowedToVote) {
currentPlayers[i].NewRef();
voterPlayers[voterPlayers.length] = currentPlayers[i];
}
_.memory.Free(nextID);
}
_.memory.FreeMany(currentPlayers);
return voterPlayers;
}
/// Updates the inner voting model with current list of players allowed to vote.
/// Also returns said list.
private final function UpdateVoters(array<EPlayer> votingPlayers) {
local int i;
local array<UserID> votersIDs;
if (model == none) {
return;
}
for (i = 0; i < votingPlayers.length; i += 1) {
votersIDs[votersIDs.length] = votingPlayers[i].GetUserID();
}
for (i = 0; i < debugVoters.length; i += 1) {
debugVoters[i].NewRef();
votersIDs[votersIDs.length] = debugVoters[i];
}
model.UpdatePotentialVoters(votersIDs);
_.memory.FreeMany(votersIDs);
}
/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceStart() {
local int i, j;
local bool playerAllowedToSee;
local UserID nextID;
local MutableText howToVoteHint;
local array<EPlayer> currentPlayers;
howToVoteHint = MakeHowToVoteHint();
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
nextID = currentPlayers[i].GetUserID();
playerAllowedToSee = false;
for (j = 0; j < policyAllowedToVoteGroups.length; j += 1) {
if (_.users.IsUserIDInGroup(nextID, policyAllowedToVoteGroups[j])) {
playerAllowedToSee = true;
break;
}
}
_.memory.Free(nextID);
if (playerAllowedToSee) {
currentPlayers[i].Notify(currentAnnouncements.started, howToVoteHint,, P("voting"));
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
currentPlayers[i].BorrowConsole().WriteLine(howToVoteHint);
} else {
currentPlayers[i].Notify(currentAnnouncements.started, F(cannotVoteHint),, P("voting"));
currentPlayers[i].BorrowConsole().WriteLine(currentAnnouncements.started);
}
}
_.memory.Free(howToVoteHint);
_.memory.FreeMany(currentPlayers);
}
/// Prints given voting outcome message in console and publishes it as
/// a notification.
private final function AnnounceOutcome(BaseText outcomeMessage, optional EPlayer forcedBy) {
local int i;
local Text playerName;
local MutableText editedOutcomeMessage, summaryLine;
local ConsoleWriter writer;
local array<EPlayer> currentPlayers;
if (model == none) {
return;
}
if (outcomeMessage != none) {
editedOutcomeMessage = outcomeMessage.MutableCopy();
}
if (editedOutcomeMessage != none && forcedBy != none) {
editedOutcomeMessage.Append(F(" {$TextEmphasis (forced by }"));
playerName = forcedBy.GetName();
editedOutcomeMessage.Append(playerName, _.text.FormattingFromColor(_.color.Gray));
_.memory.Free(playerName);
editedOutcomeMessage.Append(F("{$TextEmphasis )}"));
}
voteSummaryTemplate.Reset();
voteSummaryTemplate.ArgInt(model.GetVotesFor());
voteSummaryTemplate.ArgInt(model.GetVotesAgainst());
summaryLine = voteSummaryTemplate.CollectFormattedM();
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
writer = currentPlayers[i].BorrowConsole();
writer.Write(editedOutcomeMessage);
writer.Write(P(" / "));
writer.WriteLine(summaryLine);
currentPlayers[i].Notify(editedOutcomeMessage, summaryLine,, P("voting"));
}
_.memory.FreeMany(currentPlayers);
_.memory.Free(summaryLine);
}
defaultproperties {
// You can override these
preferredName = "test"
votingInfoLine = "Debug voting is running"
votingStartedLine = "Test voting has started"
votingSucceededLine = "{$TextPositive Test voting passed}"
votingFailedLine = "{$TextNegative Test voting has failed}"
permissionsConfigClass = class'VotingPermissions'
// You cannot override these
voteSummaryTemplateString = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}"
playerVotedTemplateString = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting"
playerVotedAnonymousTemplateString = "Someone has voted %%vote_type%% passing test voting"
timeRemaningAnnounceTemplateString = "Time remaining for voting: %1 seconds"
cannotVoteHint = "{$TextNegative You aren't allowed to vote :(}"
announcementTimings(0) = 60
announcementTimings(1) = 30
announcementTimings(2) = 15
announcementTimings(3) = 10
announcementTimings(4) = 5
announcementTimings(5) = 4
announcementTimings(6) = 3
announcementTimings(7) = 2
announcementTimings(8) = 1
}