Browse Source
Just a basic implementation with some temporary data in the future base class for testing purposes. Doesn't yet respect any voting settings, but supports fake voters for debugging.pull/12/head
Anton Tarasenko
2 years ago
13 changed files with 1295 additions and 127 deletions
@ -0,0 +1,8 @@ |
|||||||
|
; This config file allows you to configure command aliases. |
||||||
|
; Remember that aliases are case-insensitive. |
||||||
|
[AcediaCore.CommandAliasSource] |
||||||
|
record=(alias="yes",value="vote yes") |
||||||
|
record=(alias="no",value="vote no") |
||||||
|
|
||||||
|
[help CommandAliases] |
||||||
|
Alias="hlp" |
@ -0,0 +1,29 @@ |
|||||||
|
[default VotingSettings] |
||||||
|
;= Determines the duration of the voting period, specified in seconds. |
||||||
|
votingTime=30 |
||||||
|
;= Minimal amount of rounds player has to play before being allowed to vote. |
||||||
|
;= If every player played the same amount of rounds, then voting is allowed even if they played |
||||||
|
;= less that specified value |
||||||
|
;= `0` to disable. |
||||||
|
minimalRoundsToVote=1 |
||||||
|
;= Determines whether spectators are allowed to vote. |
||||||
|
allowSpectatorVoting=false |
||||||
|
;= Specifies which group(s) of players are allowed to see who makes what vote. |
||||||
|
allowedToSeeVotesGroup="admin" |
||||||
|
allowedToSeeVotesGroup="moderator" |
||||||
|
;= Specifies which group(s) of players are allowed to vote. |
||||||
|
allowedToVoteGroup="everybody" |
||||||
|
|
||||||
|
[moderator VotingSettings] |
||||||
|
votingTime=30 |
||||||
|
minimalRoundsToVote=0 |
||||||
|
allowSpectatorVoting=true |
||||||
|
allowedToSeeVotesGroup="admin" |
||||||
|
allowedToVoteGroup="moderator" |
||||||
|
allowedToVoteGroup="admin" |
||||||
|
|
||||||
|
[admin VotingSettings] |
||||||
|
votingTime=30 |
||||||
|
minimalRoundsToVote=0 |
||||||
|
allowSpectatorVoting=true |
||||||
|
allowedToVoteGroup="admin" |
@ -0,0 +1,166 @@ |
|||||||
|
/** |
||||||
|
* Author: dkanus |
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||||
|
* License: GPL |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class ACommandFakers extends Command |
||||||
|
dependsOn(VotingModel); |
||||||
|
|
||||||
|
var private array<UserID> fakers; |
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) { |
||||||
|
builder.Name(P("fakers")); |
||||||
|
builder.Group(P("debug")); |
||||||
|
builder.Summary(P("Adds fake voters for testing \"vote\" command.")); |
||||||
|
builder.Describe(P("Displays current fake voters.")); |
||||||
|
|
||||||
|
builder.SubCommand(P("amount")); |
||||||
|
builder.Describe(P("Specify amount of faker that are allowed to vote.")); |
||||||
|
builder.ParamInteger(P("fakers_amount")); |
||||||
|
|
||||||
|
builder.SubCommand(P("vote")); |
||||||
|
builder.Describe(P("Make a vote as a faker.")); |
||||||
|
builder.ParamInteger(P("faker_number")); |
||||||
|
builder.ParamBoolean(P("vote_for")); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Executed(CallData arguments, EPlayer instigator) { |
||||||
|
if (arguments.subCommandName.IsEmpty()) { |
||||||
|
DisplayCurrentFakers(); |
||||||
|
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) { |
||||||
|
ChangeAmount(arguments.parameters.GetInt(P("fakers_amount"))); |
||||||
|
} else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) { |
||||||
|
CastVote( |
||||||
|
arguments.parameters.GetInt(P("faker_number")), |
||||||
|
arguments.parameters.GetBool(P("vote_for"))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public final function UpdateFakersForVoting() { |
||||||
|
local Voting currentVoting; |
||||||
|
|
||||||
|
currentVoting = GetCurrentVoting(); |
||||||
|
if (currentVoting != none) { |
||||||
|
currentVoting.SetDebugVoters(fakers); |
||||||
|
} |
||||||
|
_.memory.Free(currentVoting); |
||||||
|
} |
||||||
|
|
||||||
|
private final function CastVote(int fakerID, bool voteFor) { |
||||||
|
local Voting currentVoting; |
||||||
|
|
||||||
|
if (fakerID < 0 || fakerID >= fakers.length) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.WriteLine(P("Faker number is out of bounds.")); |
||||||
|
return; |
||||||
|
} |
||||||
|
currentVoting = GetCurrentVoting(); |
||||||
|
if (currentVoting == none) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.WriteLine(P("There is no voting active right now.")); |
||||||
|
return; |
||||||
|
} |
||||||
|
currentVoting.CastVoteByID(fakers[fakerID], voteFor); |
||||||
|
_.memory.Free(currentVoting); |
||||||
|
} |
||||||
|
|
||||||
|
private final function ChangeAmount(int newAmount) { |
||||||
|
local int i; |
||||||
|
local Text nextIDName; |
||||||
|
local UserID nextID; |
||||||
|
|
||||||
|
if (newAmount < 0) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.WriteLine(P("Cannot specify negative amount.")); |
||||||
|
} |
||||||
|
if (newAmount == fakers.length) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextNeutral) |
||||||
|
.WriteLine(P("Specified same amount of fakers.")); |
||||||
|
} else if (newAmount > fakers.length) { |
||||||
|
for (i = fakers.length; i < newAmount; i += 1) { |
||||||
|
nextIDName = _.text.FromString("DEBUG:FAKER:" $ i); |
||||||
|
nextID = UserID(__().memory.Allocate(class'UserID')); |
||||||
|
nextID.Initialize(nextIDName); |
||||||
|
_.memory.Free(nextIDName); |
||||||
|
fakers[fakers.length] = nextID; |
||||||
|
} |
||||||
|
} else { |
||||||
|
for (i = fakers.length - 1; i >= newAmount; i -= 1) { |
||||||
|
_.memory.Free(fakers[i]); |
||||||
|
} |
||||||
|
fakers.length = newAmount; |
||||||
|
} |
||||||
|
UpdateFakersForVoting(); |
||||||
|
} |
||||||
|
|
||||||
|
private function Voting GetCurrentVoting() { |
||||||
|
local Commands_Feature feature; |
||||||
|
local Voting result; |
||||||
|
|
||||||
|
feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); |
||||||
|
if (feature != none) { |
||||||
|
result = feature.GetCurrentVoting(); |
||||||
|
feature.FreeSelf(); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private function DisplayCurrentFakers() { |
||||||
|
local int i; |
||||||
|
local VotingModel.PlayerVoteStatus nextVoteStatus; |
||||||
|
local MutableText nextNumber; |
||||||
|
local Voting currentVoting; |
||||||
|
|
||||||
|
if (fakers.length <= 0) { |
||||||
|
callerConsole.WriteLine(P("No fakers!")); |
||||||
|
return; |
||||||
|
} |
||||||
|
currentVoting = GetCurrentVoting(); |
||||||
|
for (i = 0; i < fakers.length; i += 1) { |
||||||
|
nextNumber = _.text.FromIntM(i); |
||||||
|
callerConsole |
||||||
|
.Write(P("Faker #")) |
||||||
|
.Write(nextNumber) |
||||||
|
.Write(P(": ")); |
||||||
|
if (currentVoting != none) { |
||||||
|
nextVoteStatus = currentVoting.GetVote(fakers[i]); |
||||||
|
} |
||||||
|
switch (nextVoteStatus) { |
||||||
|
case PVS_NoVote: |
||||||
|
callerConsole.WriteLine(P("no vote")); |
||||||
|
break; |
||||||
|
case PVS_VoteFor: |
||||||
|
callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for")); |
||||||
|
break; |
||||||
|
case PVS_VoteAgainst: |
||||||
|
callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against")); |
||||||
|
break; |
||||||
|
default: |
||||||
|
callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!")); |
||||||
|
} |
||||||
|
_.memory.Free(nextNumber); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties { |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
/** |
||||||
|
* Author: dkanus |
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||||
|
* License: GPL |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class ACommandVote extends Command; |
||||||
|
|
||||||
|
var private CommandDataBuilder dataBuilder; |
||||||
|
|
||||||
|
protected function Constructor() { |
||||||
|
ResetVotingInfo(); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() { |
||||||
|
super.Finalizer(); |
||||||
|
_.memory.Free(dataBuilder); |
||||||
|
dataBuilder = none; |
||||||
|
} |
||||||
|
|
||||||
|
protected function BuildData(CommandDataBuilder builder) { |
||||||
|
builder.Name(P("vote")); |
||||||
|
builder.Group(P("core")); |
||||||
|
builder.Summary(P("Allows players to initiate any available voting." |
||||||
|
@ "Votings themselves are added as sub-commands.")); |
||||||
|
builder.Describe(P("Default command simply displaces information about current vote.")); |
||||||
|
|
||||||
|
dataBuilder.SubCommand(P("yes")); |
||||||
|
builder.Describe(P("Vote `yes` on the current vote.")); |
||||||
|
dataBuilder.SubCommand(P("no")); |
||||||
|
builder.Describe(P("Vote `no` on the current vote.")); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Executed(CallData arguments, EPlayer instigator) { |
||||||
|
local Voting currentVoting; |
||||||
|
local Commands_Feature feature; |
||||||
|
|
||||||
|
feature = Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); |
||||||
|
if (feature == none) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.WriteLine(P("Feature responsible for commands and voting isn't enabled." |
||||||
|
@ "This is unexpected, something broke terribly.")); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
currentVoting = feature.GetCurrentVoting(); |
||||||
|
} |
||||||
|
if (arguments.subCommandName.IsEmpty()) { |
||||||
|
DisplayInfoAboutVoting(instigator, currentVoting); |
||||||
|
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) { |
||||||
|
CastVote(currentVoting, instigator, true); |
||||||
|
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) { |
||||||
|
CastVote(currentVoting, instigator, false); |
||||||
|
} else { |
||||||
|
StartVoting(arguments.subCommandName, feature, currentVoting, instigator); |
||||||
|
} |
||||||
|
_.memory.Free(feature); |
||||||
|
_.memory.Free(currentVoting); |
||||||
|
} |
||||||
|
|
||||||
|
/// Adds sub-command information about given voting with a given name. |
||||||
|
public final function AddVotingInfo(BaseText processName, class<Voting> processClass) { |
||||||
|
if (processName == none) return; |
||||||
|
if (processClass == none) return; |
||||||
|
if (dataBuilder == none) return; |
||||||
|
|
||||||
|
dataBuilder.SubCommand(processName); |
||||||
|
processClass.static.AddInfo(dataBuilder); |
||||||
|
commandData = dataBuilder.BorrowData(); |
||||||
|
} |
||||||
|
|
||||||
|
/// Clears all sub-command information added from [`Voting`]s. |
||||||
|
public final function ResetVotingInfo() { |
||||||
|
_.memory.Free(dataBuilder); |
||||||
|
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder')); |
||||||
|
BuildData(dataBuilder); |
||||||
|
commandData = dataBuilder.BorrowData(); |
||||||
|
} |
||||||
|
|
||||||
|
private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) { |
||||||
|
if (currentVoting == none) { |
||||||
|
callerConsole.WriteLine(P("No voting is active right now.")); |
||||||
|
} else { |
||||||
|
currentVoting.PrintVotingInfoFor(instigator); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) { |
||||||
|
if (currentVoting != none) { |
||||||
|
currentVoting.CastVote(voter, voteForSuccess); |
||||||
|
} else { |
||||||
|
callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now.")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Assumes all arguments aren't `none`. |
||||||
|
private final function StartVoting( |
||||||
|
BaseText votingName, |
||||||
|
Commands_Feature feature, |
||||||
|
Voting currentVoting, |
||||||
|
EPlayer instigator |
||||||
|
) { |
||||||
|
local Command fakersCommand; |
||||||
|
local Voting newVoting; |
||||||
|
local Commands_Feature.StartVotingResult result; |
||||||
|
|
||||||
|
result = feature.StartVoting(votingName); |
||||||
|
// Handle errors |
||||||
|
if (result == SVR_UnknownVoting) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.Write(P("Unknown voting option \"")) |
||||||
|
.Write(votingName) |
||||||
|
.WriteLine(P("\"")); |
||||||
|
return; |
||||||
|
} else if (result == SVR_AlreadyInProgress) { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextWarning) |
||||||
|
.WriteLine(P("Another voting is already in progress!")); |
||||||
|
return; |
||||||
|
} |
||||||
|
// Inform new voting about fake voters, in case we're debugging |
||||||
|
if (currentVoting == none && _.environment.IsDebugging()) { |
||||||
|
fakersCommand = feature.GetCommand(P("fakers")); |
||||||
|
if (fakersCommand != none && fakersCommand.class == class'ACommandFakers') { |
||||||
|
ACommandFakers(fakersCommand).UpdateFakersForVoting(); |
||||||
|
} |
||||||
|
_.memory.Free(fakersCommand); |
||||||
|
} |
||||||
|
// Cast a vote from instigator |
||||||
|
newVoting = feature.GetCurrentVoting(); |
||||||
|
if (newVoting != none) { |
||||||
|
newVoting.CastVote(instigator, true); |
||||||
|
} else { |
||||||
|
callerConsole |
||||||
|
.UseColor(_.color.TextFailure) |
||||||
|
.WriteLine(P("Voting should be available, but it isn't." |
||||||
|
@ "This is unexpected, something broke terribly.")); |
||||||
|
} |
||||||
|
_.memory.Free(newVoting); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties { |
||||||
|
} |
@ -0,0 +1,437 @@ |
|||||||
|
/** |
||||||
|
* Author: dkanus |
||||||
|
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore |
||||||
|
* License: GPL |
||||||
|
* Copyright 2023 Anton Tarasenko |
||||||
|
*------------------------------------------------------------------------------ |
||||||
|
* This file is part of Acedia. |
||||||
|
* |
||||||
|
* Acedia is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation, version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* Acedia is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License |
||||||
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
*/ |
||||||
|
class Voting extends AcediaObject |
||||||
|
dependsOn(VotingModel); |
||||||
|
|
||||||
|
//! Class that describes a single voting option. |
||||||
|
//! |
||||||
|
//! [`Voting`] is created to be heavily integrated with [`Commands_Feature`] and shouldn't be used |
||||||
|
//! separately from it. |
||||||
|
//! You shouldn't allocate its instances directly unless you're working on |
||||||
|
//! the [`Commands_Feature`]'s or related code. |
||||||
|
//! |
||||||
|
//! # Usage |
||||||
|
//! |
||||||
|
//! This class takes care of the voting process by itself, one only needs to call [`Start()`] method. |
||||||
|
//! If you wish to prematurely end voting (e.g. forcing it to end), then call [`Stop()`] method. |
||||||
|
//! |
||||||
|
//! Note that there is no method to handle ending of [`Voting`], since all necessary logic is |
||||||
|
//! expected to be performed internally. |
||||||
|
//! [`Commands_Feature`] does this check lazily (only when user wants to start another voting) |
||||||
|
//! via [`HasEnded()`] method. |
||||||
|
|
||||||
|
/// Records whether end-of-the voting methods were already called. |
||||||
|
var private bool endingHandled; |
||||||
|
/// Records whether voting was forcefully ended |
||||||
|
var private bool forcefullyEnded; |
||||||
|
/// Underlying voting model that does actual vote calculations. |
||||||
|
var private VotingModel model; |
||||||
|
|
||||||
|
/// Fake voters that are only used in debug mode to allow for simpler vote testing. |
||||||
|
var private array<UserID> debugVoters; |
||||||
|
|
||||||
|
/// Text that serves as a template for announcing current vote counts. Don't change this. |
||||||
|
var private const string votesDistributionLine; |
||||||
|
/// Text that serves as a template for announcing player making a new vote. Don't change this. |
||||||
|
var private const string playerVotedLine; |
||||||
|
// [`TextTemplate`]s made from the above `string` templates. |
||||||
|
var private TextTemplate summaryTemplate, playerVotedTemplate; |
||||||
|
|
||||||
|
// TODO: check these limitations |
||||||
|
/// Name of this voting. |
||||||
|
/// |
||||||
|
/// Has to satisfy limitations described in the `BaseText::IsValidName()` |
||||||
|
var private const string preferredName; |
||||||
|
/// Text that should be displayed when voting info is displayed mid-voting. |
||||||
|
/// |
||||||
|
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||||
|
/// "Voting to end trader currently active." line, avoid adding formatting and don't |
||||||
|
/// add comma/exclamation mark at the end. |
||||||
|
var private const string infoLine; |
||||||
|
/// Text that should be displayed when voting starts. |
||||||
|
/// |
||||||
|
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||||
|
/// "Voting to end trader has started" line, avoid adding formatting and don't |
||||||
|
/// add comma/exclamation mark at the end. |
||||||
|
var private const string votingStartedLine; |
||||||
|
/// Text that should be displayed when voting has ended with a success. |
||||||
|
/// |
||||||
|
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||||
|
/// "{$TextPositive Voting to end trader was successful!}" line, coloring it in a positive color and |
||||||
|
/// adding comma/exclamation mark at the end. |
||||||
|
var private const string votingSucceededLine; |
||||||
|
/// Text that should be displayed when voting has ended in a failure. |
||||||
|
/// |
||||||
|
/// There isn't any hard limitations, but for the sake of uniformity try to mimic |
||||||
|
/// "{$TextNegative Voting to end trader has failed!}" line, coloring it in a negative color and |
||||||
|
/// adding comma/exclamation mark at the end. |
||||||
|
var private const string votingFailedLine; |
||||||
|
|
||||||
|
protected function Constructor() { |
||||||
|
summaryTemplate = _.text.MakeTemplate_S(votesDistributionLine); |
||||||
|
playerVotedTemplate = _.text.MakeTemplate_S(playerVotedLine); |
||||||
|
} |
||||||
|
|
||||||
|
protected function Finalizer() { |
||||||
|
forcefullyEnded = false; |
||||||
|
endingHandled = false; |
||||||
|
_.memory.Free(model); |
||||||
|
_.memory.Free(summaryTemplate); |
||||||
|
_.memory.Free(playerVotedTemplate); |
||||||
|
model = none; |
||||||
|
summaryTemplate = none; |
||||||
|
playerVotedTemplate = none; |
||||||
|
} |
||||||
|
|
||||||
|
/// Override this to perform necessary logic after voting has succeeded. |
||||||
|
protected function Execute() {} |
||||||
|
|
||||||
|
/// Override this to specify arguments for your voting command. |
||||||
|
/// |
||||||
|
/// This method is for adding arguments only. |
||||||
|
/// DO NOT call [`CommandDataBuilder::SubCommand()`] or [`CommandDataBuilder::Option()`] methods, |
||||||
|
/// otherwise you'll cause unexpected behavior for your mod's users. |
||||||
|
public static function AddInfo(CommandDataBuilder builder) { |
||||||
|
builder.OptionalParams(); |
||||||
|
builder.ParamText(P("message")); |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns name of this voting in the lower case. |
||||||
|
public final static function Text GetPreferredName() { |
||||||
|
local Text result; |
||||||
|
|
||||||
|
result = __().text.FromString(Locs(default.preferredName)); |
||||||
|
if (result.IsValidName()) { |
||||||
|
return result; |
||||||
|
} |
||||||
|
__().memory.Free(result); |
||||||
|
return none; |
||||||
|
} |
||||||
|
|
||||||
|
/// Starts caller [`Voting`] using policies, loaded from the given config. |
||||||
|
public final function Start(BaseText votingConfigName) { |
||||||
|
local int i; |
||||||
|
local MutableText howToVoteHint; |
||||||
|
local array<EPlayer> voters; |
||||||
|
|
||||||
|
if (model != none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
model = VotingModel(_.memory.Allocate(class'VotingModel')); |
||||||
|
model.Initialize(VP_CanChangeVote); |
||||||
|
voters = UpdateVoters(); |
||||||
|
howToVoteHint = MakeHowToVoteHint(); |
||||||
|
for (i = 0; i < voters.length; i += 1) { |
||||||
|
voters[i].Notify(F(votingStartedLine), howToVoteHint); |
||||||
|
voters[i].BorrowConsole().WriteLine(F(votingStartedLine)); |
||||||
|
voters[i].BorrowConsole().WriteLine(howToVoteHint); |
||||||
|
} |
||||||
|
_.memory.FreeMany(voters); |
||||||
|
_.memory.Free(howToVoteHint); |
||||||
|
} |
||||||
|
|
||||||
|
// Assembles "Say {$TextPositive !yes} or {$TextNegative !no} to vote" hint. |
||||||
|
// Replaces "!yes"/"!no" with "!vote yes"/"!vote no" if corresponding aliases aren't properly setup. |
||||||
|
private final function MutableText MakeHowToVoteHint() { |
||||||
|
local Text resolvedAlias; |
||||||
|
local MutableText result; |
||||||
|
|
||||||
|
result = P("Say ").MutableCopy(); |
||||||
|
resolvedAlias = _.alias.ResolveCommand(P("yes")); |
||||||
|
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.yes"), SCASE_SENSITIVE)) { |
||||||
|
result.Append(P("!yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
||||||
|
} else { |
||||||
|
result.Append(P("!vote yes"), _.text.FormattingFromColor(_.color.TextPositive)); |
||||||
|
} |
||||||
|
_.memory.Free(resolvedAlias); |
||||||
|
result.Append(P(" or ")); |
||||||
|
resolvedAlias = _.alias.ResolveCommand(P("no")); |
||||||
|
if (resolvedAlias != none && resolvedAlias.Compare(P("vote.no"), SCASE_SENSITIVE)) { |
||||||
|
result.Append(P("!no"), _.text.FormattingFromColor(_.color.TextNegative)); |
||||||
|
} else { |
||||||
|
result.Append(P("!vote no"), _.text.FormattingFromColor(_.color.TextNegative)); |
||||||
|
} |
||||||
|
_.memory.Free(resolvedAlias); |
||||||
|
result.Append(P(" to vote")); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/// Forcefully stops [`Voting`]. |
||||||
|
/// |
||||||
|
/// Only works if [`Voting`] has already started, but didn't yet ended (see [`HasEnded()`]). |
||||||
|
public final function Stop() { |
||||||
|
if (model != none) { |
||||||
|
forcefullyEnded = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Determines whether the [`Voting`] process has concluded. |
||||||
|
/// |
||||||
|
/// Note that this is different from the voting being active, as voting that has not yet started is |
||||||
|
/// also not concluded. |
||||||
|
public final function bool HasEnded() { |
||||||
|
if (model == none) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return (forcefullyEnded || model.HasEnded()); |
||||||
|
} |
||||||
|
|
||||||
|
/// Returns the current voting status for the specified voter. |
||||||
|
/// |
||||||
|
/// If the voter was previously eligible to vote, cast a vote, but later had their voting rights |
||||||
|
/// revoked, their vote will not count, and this method will return [`PVS_NoVote`]. |
||||||
|
/// |
||||||
|
/// However, if the player regains their voting rights while the voting process is still ongoing, |
||||||
|
/// their previous vote will be automatically restored by the caller [`Voting`]. |
||||||
|
public final function VotingModel.PlayerVoteStatus GetVote(UserID voter) { |
||||||
|
if (model != none) { |
||||||
|
return model.GetVote(voter); |
||||||
|
} |
||||||
|
return PVS_NoVote; |
||||||
|
} |
||||||
|
|
||||||
|
/// Specifies the [`UserID`]s that will be added as additional voters in debug mode. |
||||||
|
/// |
||||||
|
/// This method should only be used for debugging purposes and will only function if the game is |
||||||
|
/// running in debug mode. |
||||||
|
public final function SetDebugVoters(array<UserID> newDebugVoters) { |
||||||
|
local int i; |
||||||
|
|
||||||
|
if(!_.environment.IsDebugging()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
_.memory.FreeMany(debugVoters); |
||||||
|
debugVoters.length = 0; |
||||||
|
for (i = 0; i < newDebugVoters.length; i += 1) { |
||||||
|
if (newDebugVoters[i] != none) { |
||||||
|
debugVoters[debugVoters.length] = newDebugVoters[i]; |
||||||
|
newDebugVoters[i].NewRef(); |
||||||
|
} |
||||||
|
} |
||||||
|
_.memory.FreeMany(UpdateVoters()); |
||||||
|
TryEnding(); |
||||||
|
} |
||||||
|
|
||||||
|
/// Adds a new vote by a given [`UserID`]. |
||||||
|
/// |
||||||
|
/// NOTE: this method is intended for use only in debug mode, and is will not do anything otherwise. |
||||||
|
/// This method silently adds a vote using the provided [`UserID`], without any prompt or |
||||||
|
/// notification of updated voting status. |
||||||
|
/// It was added to facilitate testing with fake [`UserID`]s, and is limited to debug mode to |
||||||
|
/// prevent misuse and unintended behavior in production code. |
||||||
|
public final function VotingModel.VotingResult CastVoteByID(UserID voter, bool voteForSuccess) { |
||||||
|
local array<EPlayer> allVoters; |
||||||
|
local VotingModel.VotingResult result; |
||||||
|
|
||||||
|
if (model == none) return VFR_NotAllowed; |
||||||
|
if (voter == none) return VFR_NotAllowed; |
||||||
|
if (!_.environment.IsDebugging()) return VFR_NotAllowed; |
||||||
|
|
||||||
|
allVoters = UpdateVoters(); |
||||||
|
result = model.CastVote(voter, voteForSuccess); |
||||||
|
if (!TryEnding() && result == VFR_Success) { |
||||||
|
AnnounceNewVote(none, allVoters, voteForSuccess); |
||||||
|
} |
||||||
|
_.memory.FreeMany(allVoters); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/// Casts a vote for the specified player. |
||||||
|
/// |
||||||
|
/// This method will update the voting status for the specified player and may trigger the end of |
||||||
|
/// the voting process. |
||||||
|
/// After a vote is cast, the updated voting status will be broadcast to all players. |
||||||
|
public final function VotingModel.VotingResult CastVote(EPlayer voter, bool voteForSuccess) { |
||||||
|
local bool votingContinues; |
||||||
|
local UserID voterID; |
||||||
|
local array<EPlayer> allVoters; |
||||||
|
local VotingModel.VotingResult result; |
||||||
|
|
||||||
|
if (model == none) return VFR_NotAllowed; |
||||||
|
if (voter == none) return VFR_NotAllowed; |
||||||
|
|
||||||
|
voterID = voter.GetUserID(); |
||||||
|
allVoters = UpdateVoters(); |
||||||
|
result = model.CastVote(voterID, voteForSuccess); |
||||||
|
votingContinues = !TryEnding(); |
||||||
|
if (votingContinues) { |
||||||
|
switch (result) { |
||||||
|
case VFR_Success: |
||||||
|
AnnounceNewVote(voter, allVoters, voteForSuccess); |
||||||
|
break; |
||||||
|
case VFR_NotAllowed: |
||||||
|
voter |
||||||
|
.BorrowConsole() |
||||||
|
.WriteLine(F("You are {$TextNegative not allowed} to vote right now.")); |
||||||
|
break; |
||||||
|
case VFR_CannotChangeVote: |
||||||
|
voter.BorrowConsole().WriteLine(F("Changing vote is {$TextNegative forbidden}.")); |
||||||
|
break; |
||||||
|
case VFR_VotingEnded: |
||||||
|
voter.BorrowConsole().WriteLine(F("Voting has already {$TextNegative ended}!")); |
||||||
|
break; |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
_.memory.Free(voterID); |
||||||
|
_.memory.FreeMany(allVoters); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/// Prints information about caller [`Voting`] to the given player. |
||||||
|
public final function PrintVotingInfoFor(EPlayer requester) { |
||||||
|
local MutableText summaryPart; |
||||||
|
|
||||||
|
if (requester == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
summaryTemplate.Reset(); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||||
|
summaryPart = summaryTemplate.CollectFormattedM(); |
||||||
|
requester |
||||||
|
.BorrowConsole() |
||||||
|
.Write(F(infoLine)) |
||||||
|
.Write(P(". ")) |
||||||
|
.Write(summaryPart) |
||||||
|
.WriteLine(P(".")); |
||||||
|
_.memory.Free(summaryPart); |
||||||
|
} |
||||||
|
|
||||||
|
/// Outputs message about new vote being submitted to all relevant voters. |
||||||
|
private final function AnnounceNewVote(EPlayer voter, array<EPlayer> voters, bool voteForSuccess) { |
||||||
|
local int i; |
||||||
|
local Text voterName; |
||||||
|
local MutableText playerVotedPart, summaryPart; |
||||||
|
|
||||||
|
summaryTemplate.Reset(); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||||
|
summaryPart = summaryTemplate.CollectFormattedM(); |
||||||
|
|
||||||
|
playerVotedTemplate.Reset(); |
||||||
|
if (voter != none) { |
||||||
|
voterName = voter.GetName(); |
||||||
|
} else { |
||||||
|
voterName = P("DEBUG:FAKER").Copy(); |
||||||
|
} |
||||||
|
playerVotedTemplate.TextArg(P("player_name"), voterName, true); |
||||||
|
_.memory.Free(voterName); |
||||||
|
if (voteForSuccess) { |
||||||
|
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextPositive for}")); |
||||||
|
} else { |
||||||
|
playerVotedTemplate.TextArg(P("vote_type"), F("{$TextNegative against}")); |
||||||
|
} |
||||||
|
playerVotedPart = playerVotedTemplate.CollectFormattedM(); |
||||||
|
for (i = 0; i < voters.length; i += 1) { |
||||||
|
voters[i] |
||||||
|
.BorrowConsole() |
||||||
|
.Write(playerVotedPart) |
||||||
|
.Write(P(". ")) |
||||||
|
.Write(summaryPart) |
||||||
|
.WriteLine(P(".")); |
||||||
|
} |
||||||
|
_.memory.Free(playerVotedPart); |
||||||
|
_.memory.Free(summaryPart); |
||||||
|
} |
||||||
|
|
||||||
|
/// Tries to end voting. |
||||||
|
/// |
||||||
|
/// Returns `true` iff this method was called for the first time after the voting concluded. |
||||||
|
private final function bool TryEnding() { |
||||||
|
local MutableText outcomeMessage; |
||||||
|
|
||||||
|
if (model == none) return false; |
||||||
|
if (endingHandled) return false; |
||||||
|
if (!HasEnded()) return false; |
||||||
|
|
||||||
|
endingHandled = true; |
||||||
|
if (model.GetStatus() == VPM_Success) { |
||||||
|
outcomeMessage = _.text.FromFormattedStringM(votingSucceededLine); |
||||||
|
} else { |
||||||
|
outcomeMessage = _.text.FromFormattedStringM(votingFailedLine); |
||||||
|
} |
||||||
|
AnnounceOutcome(outcomeMessage); |
||||||
|
if (model.GetStatus() == VPM_Success) { |
||||||
|
Execute(); |
||||||
|
} |
||||||
|
_.memory.Free(outcomeMessage); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/// Updates the inner voting model with current list of players allowed to vote. |
||||||
|
/// Also returns said list. |
||||||
|
private final function array<EPlayer> UpdateVoters() { |
||||||
|
local int i; |
||||||
|
local UserID nextID; |
||||||
|
local array<EPlayer> currentPlayers; |
||||||
|
local array<UserID> potentialVoters; |
||||||
|
|
||||||
|
if (model == none) { |
||||||
|
return currentPlayers; |
||||||
|
} |
||||||
|
for (i = 0; i < debugVoters.length; i += 1) { |
||||||
|
debugVoters[i].NewRef(); |
||||||
|
potentialVoters[potentialVoters.length] = debugVoters[i]; |
||||||
|
} |
||||||
|
currentPlayers = _.players.GetAll(); |
||||||
|
for (i = 0; i < currentPlayers.length; i += 1) { |
||||||
|
nextID = currentPlayers[i].GetUserID(); |
||||||
|
potentialVoters[potentialVoters.length] = nextID; |
||||||
|
} |
||||||
|
model.UpdatePotentialVoters(potentialVoters); |
||||||
|
_.memory.FreeMany(potentialVoters); |
||||||
|
return currentPlayers; |
||||||
|
} |
||||||
|
|
||||||
|
/// Prints given voting outcome message in console and publishes it as a notification. |
||||||
|
private final function AnnounceOutcome(BaseText outcomeMessage) { |
||||||
|
local int i; |
||||||
|
local MutableText summaryLine; |
||||||
|
local array<EPlayer> currentPlayers; |
||||||
|
|
||||||
|
if (model == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
summaryTemplate.Reset(); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesFor()); |
||||||
|
summaryTemplate.ArgInt(model.GetVotesAgainst()); |
||||||
|
summaryLine = summaryTemplate.CollectFormattedM(); |
||||||
|
currentPlayers = _.players.GetAll(); |
||||||
|
for (i = 0; i < currentPlayers.length; i += 1) { |
||||||
|
currentPlayers[i].BorrowConsole().WriteLine(outcomeMessage); |
||||||
|
currentPlayers[i].BorrowConsole().WriteLine(summaryLine); |
||||||
|
currentPlayers[i].Notify(outcomeMessage, summaryLine); |
||||||
|
} |
||||||
|
_.memory.FreeMany(currentPlayers); |
||||||
|
_.memory.Free(summaryLine); |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties { |
||||||
|
preferredName = "test" |
||||||
|
infoLine = "Voting for test" |
||||||
|
votingStartedLine = "Test voting has started" |
||||||
|
votingSucceededLine = "{$TextPositive Test voting passed!}" |
||||||
|
votingFailedLine = "{$TextNegative Test voting has failed...}" |
||||||
|
playerVotedLine = "Player {$TextSubtle %%player_name%%} has voted %%vote_type%% passing test voting" |
||||||
|
votesDistributionLine = "Vote tally: {$TextPositive %1} vs {$TextNegative %2}" |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
class VotingSettings extends FeatureConfig |
||||||
|
perobjectconfig |
||||||
|
config(AcediaVoting); |
||||||
|
|
||||||
|
/// Determines the duration of the voting period, specified in seconds. |
||||||
|
var public config float votingTime; |
||||||
|
/// Minimal amount of rounds player has to play before being allowed to vote. |
||||||
|
/// |
||||||
|
/// If every player played the same amount of rounds, then voting is allowed even if they played |
||||||
|
/// less that specified value. |
||||||
|
/// |
||||||
|
/// `0` to disable. |
||||||
|
var public config int minimalRoundsToVote; |
||||||
|
/// Determines whether spectators are allowed to vote. |
||||||
|
var public config bool allowSpectatorVoting; |
||||||
|
/// Specifies which group(s) of players are allowed to see who makes what vote. |
||||||
|
var public config array<string> allowedToSeeVotesGroup; |
||||||
|
/// Specifies which group(s) of players are allowed to vote. |
||||||
|
var public config array<string> allowedToVoteGroup; |
||||||
|
|
||||||
|
protected function HashTable ToData() { |
||||||
|
local int i; |
||||||
|
local HashTable data; |
||||||
|
local ArrayList arrayOfTexts; |
||||||
|
|
||||||
|
data = __().collections.EmptyHashTable(); |
||||||
|
data.SetFloat(P("votingTime"), votingTime); |
||||||
|
data.SetInt(P("minimalRoundsToVote"), minimalRoundsToVote); |
||||||
|
data.SetBool(P("allowSpectatorVoting"), allowSpectatorVoting); |
||||||
|
|
||||||
|
arrayOfTexts = _.collections.EmptyArrayList(); |
||||||
|
for (i = 0; i < allowedToSeeVotesGroup.length; i += 1) { |
||||||
|
arrayOfTexts.AddString(allowedToSeeVotesGroup[i]); |
||||||
|
} |
||||||
|
data.SetItem(P("allowedToSeeVotesGroup"), arrayOfTexts); |
||||||
|
_.memory.Free(arrayOfTexts); |
||||||
|
|
||||||
|
arrayOfTexts = _.collections.EmptyArrayList(); |
||||||
|
for (i = 0; i < allowedToVoteGroup.length; i += 1) { |
||||||
|
arrayOfTexts.AddString(allowedToVoteGroup[i]); |
||||||
|
} |
||||||
|
data.SetItem(P("allowedToVoteGroup"), arrayOfTexts); |
||||||
|
_.memory.Free(arrayOfTexts); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
protected function FromData(HashTable source) { |
||||||
|
local int i; |
||||||
|
local ArrayList arrayOfTexts; |
||||||
|
|
||||||
|
if (source == none) { |
||||||
|
return; |
||||||
|
} |
||||||
|
votingTime = source.GetFloat(P("votingTime"), 30.0); |
||||||
|
minimalRoundsToVote = source.GetInt(P("minimalRoundsToVote"), 1); |
||||||
|
allowSpectatorVoting = source.GetBool(P("allowSpectatorVoting"), false); |
||||||
|
|
||||||
|
allowedToSeeVotesGroup.length = 0; |
||||||
|
arrayOfTexts = source.GetArrayList(P("allowedToSeeVotesGroup")); |
||||||
|
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||||
|
allowedToSeeVotesGroup[allowedToSeeVotesGroup.length] = arrayOfTexts.GetString(i); |
||||||
|
} |
||||||
|
_.memory.Free(arrayOfTexts); |
||||||
|
|
||||||
|
allowedToVoteGroup.length = 0; |
||||||
|
arrayOfTexts = source.GetArrayList(P("allowedToVoteGroup")); |
||||||
|
for (i = 0; i < arrayOfTexts.GetLength(); i += 1) { |
||||||
|
allowedToVoteGroup[allowedToVoteGroup.length] = arrayOfTexts.GetString(i); |
||||||
|
} |
||||||
|
_.memory.Free(arrayOfTexts); |
||||||
|
} |
||||||
|
|
||||||
|
protected function DefaultIt() { |
||||||
|
votingTime = 30.0; |
||||||
|
minimalRoundsToVote = 1; |
||||||
|
allowSpectatorVoting = false; |
||||||
|
allowedToSeeVotesGroup.length = 0; |
||||||
|
allowedToVoteGroup.length = 0; |
||||||
|
allowedToVoteGroup[0] = "everybody"; |
||||||
|
} |
||||||
|
|
||||||
|
defaultproperties { |
||||||
|
configName = "AcediaVoting" |
||||||
|
} |
Loading…
Reference in new issue