/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class 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 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() { 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 (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 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 newDebugVoters) { local int i; local array 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 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 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 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 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 UpdateVoters(array votingPlayers) { local int i; local array 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 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 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 }