863 lines
34 KiB
Ucode
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
|
|
} |