Browse Source

Add basic voting functionality

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
parent
commit
0432c1c074
  1. 2
      config/AcediaAliases_Colors.ini
  2. 8
      config/AcediaAliases_Commands.ini
  3. 5
      config/AcediaSystem.ini
  4. 29
      config/AcediaVoting.ini
  5. 166
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc
  6. 159
      sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc
  7. 2
      sources/BaseAPI/API/Commands/Command.uc
  8. 424
      sources/BaseAPI/API/Commands/Commands_Feature.uc
  9. 437
      sources/BaseAPI/API/Commands/Voting/Voting.uc
  10. 84
      sources/BaseAPI/API/Commands/Voting/VotingModel.uc
  11. 84
      sources/BaseAPI/API/Commands/Voting/VotingSettings.uc
  12. 12
      sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc
  13. 4
      sources/Color/ColorAPI.uc

2
config/AcediaAliases_Colors.ini

@ -8,7 +8,7 @@ record=(alias="TextSubHeader",value="rgb(147,112,219)")
record=(alias="TextPositive",value="rgb(60,220,20)")
record=(alias="TextNeutral",value="rgb(255,255,0)")
record=(alias="TextNegative",value="rgb(220,20,60)")
record=(alias="TextSubtle",value="rgb(128,128,128)")
record=(alias="TextSubtle",value="rgb(211,211,211)")
record=(alias="TextEmphasis",value="rgb(0,128,255)")
record=(alias="TextOk",value="rgb(0,255,0)")
record=(alias="TextWarning",value="rgb(255,128,0)")

8
config/AcediaAliases_Commands.ini

@ -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"

5
config/AcediaSystem.ini

@ -1,5 +1,8 @@
; Every single option in this config should be considered [ADVANCED].
; DO NOT CHANGE THEM unless you are sure you know what you're doing.
[AcediaCore.AcediaEnvironment]
debugMode=false
[AcediaCore.SideEffects]
; Acedia requires adding its own `GameRules` to listen to many different
; game events.
@ -150,7 +153,7 @@ maximumNotifyTime=20
TextDefault=(R=255,G=255,B=255,A=255)
TextHeader=(R=128,G=0,B=128,A=255)
TextSubHeader=(R=147,G=112,B=219,A=255)
TextSubtle=(R=128,G=128,B=128,A=255)
TextSubtle=(R=211,G=211,B=211,A=255)
TextEmphasis=(R=0,G=128,B=255,A=255)
TextPositive=(R=0,G=128,B=0,A=255)
TextNeutral=(R=255,G=255,B=0,A=255)

29
config/AcediaVoting.ini

@ -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"

166
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandFakers.uc

@ -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 {
}

159
sources/BaseAPI/API/Commands/BuiltInCommands/ACommandVote.uc

@ -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 {
}

2
sources/BaseAPI/API/Commands/Command.uc

@ -209,7 +209,7 @@ struct Data {
var protected array<Option> options;
var protected bool requiresTarget;
};
var private Data commandData;
var protected Data commandData;
// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.

424
sources/BaseAPI/API/Commands/Commands_Feature.uc

@ -38,48 +38,82 @@ class Commands_Feature extends Feature;
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input.
// Auxiliary struct for passing name of the command to call plus, optionally, additional
// sub-command name.
//
// Normally sub-command name is parsed by the command itself, however command aliases can try to
// enforce one.
/// Pairs [`Voting`] class with a name its registered under in lower case for quick search.
struct NamedVoting {
var public class<Voting> processClass;
/// Must be guaranteed to not be `none` and lower case as an invariant
var public Text processName;
};
/// Auxiliary struct for passing name of the command to call plus, optionally, additional
/// sub-command name.
///
/// Normally sub-command name is parsed by the command itself, however command aliases can try to
/// enforce one.
struct CommandCallPair {
var MutableText commandName;
// In case it is enforced by an alias
/// In case it is enforced by an alias
var MutableText subCommandName;
};
// Delimiters that always separate command name from it's parameters
/// Describes possible outcomes of starting a voting by its name
enum StartVotingResult {
/// Voting was successfully started
SVR_Success,
/// Voting wasn't started because another one was still in progress
SVR_AlreadyInProgress,
/// Voting wasn't started because voting with that name hasn't been registered
SVR_UnknownVoting
};
/// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
// Keys should be deallocated when their entry is removed.
/// Registered commands, recorded as (<command_name>, <command_instance>) pairs.
/// Keys should be deallocated when their entry is removed.
var private HashTable registeredCommands;
// `HashTable` of "<command_group_name>" <-> `ArrayList` of commands pairs to allow quick fetch of
// commands belonging to a single group
/// [`HashTable`] of "<command_group_name>" <-> [`ArrayList`] of commands pairs to allow quick fetch
/// of commands belonging to a single group
var private HashTable groupedCommands;
// When this flag is set to true, mutate input becomes available despite `useMutateInput` flag to
// allow to unlock server in case of an error
/// [`Voting`]s that were already successfully loaded, ensuring that each has a unique name
var private array<NamedVoting> loadedVotings;
/// Currently running voting process.
/// This feature doesn't actively track when voting ends, so reference can be non-`none` even if
/// voting has already ended.
var private Voting currentVoting;
/// An array of [`Voting`] objects that have been successfully loaded and
/// each object has a unique name.
var private array<NamedVoting> registeredVotings;
/// When this flag is set to true, mutate input becomes available despite [`useMutateInput`] flag to
/// allow to unlock server in case of an error
var private bool emergencyEnabledMutate;
// Setting this to `true` enables players to input commands right in the chat by prepending them
// with `chatCommandPrefix`.
// Default is `true`.
/// Setting this to `true` enables players to input commands right in the chat by prepending them
/// with [`chatCommandPrefix`].
/// Default is `true`.
var private /*config*/ bool useChatInput;
// Setting this to `true` enables players to input commands with "mutate" console command.
// Default is `true`.
/// Setting this to `true` enables players to input commands with "mutate" console command.
/// Default is `true`.
var private /*config*/ bool useMutateInput;
// Chat messages, prepended by this prefix will be treated as commands.
// Default is "!". Empty values are also treated as "!".
/// Chat messages, prepended by this prefix will be treated as commands.
/// Default is "!". Empty values are also treated as "!".
var private /*config*/ Text chatCommandPrefix;
var LoggerAPI.Definition errCommandDuplicate, errServerAPIUnavailable;
var LoggerAPI.Definition errVotingWithSameNameAlreadyRegistered, errYesNoVotingNamesReserved;
protected function OnEnabled() {
registeredCommands = _.collections.EmptyHashTable();
groupedCommands = _.collections.EmptyHashTable();
RegisterCommand(class'ACommandHelp');
RegisterCommand(class'ACommandNotify');
RegisterCommand(class'ACommandVote');
if (_.environment.IsDebugging()) {
RegisterCommand(class'ACommandFakers');
}
RegisterVotingClass(class'Voting');
// Macro selector
commandDelimiters[0] = _.text.FromString("@");
// Key selector
@ -118,6 +152,7 @@ protected function OnDisabled() {
groupedCommands = none;
chatCommandPrefix = none;
commandDelimiters.length = 0;
ReleaseNameVotingsArray(/*out*/ registeredVotings);
}
protected function SwapConfig(FeatureConfig config)
@ -134,95 +169,6 @@ protected function SwapConfig(FeatureConfig config)
useMutateInput = newConfig.useMutateInput;
}
// Parses command's name into `CommandCallPair` - sub-command is filled in case
// specified name is an alias with specified sub-command name.
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
local Text resolvedValue;
local MutableText userSpecifiedName;
local CommandCallPair result;
local Text.Character dotCharacter;
if (parser == none) return result;
if (!parser.Ok()) return result;
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
// This isn't an alias
if (resolvedValue == none) {
result.commandName = userSpecifiedName;
return result;
}
// It is an alias - parse it
dotCharacter = _.text.GetCharacter(".");
resolvedValue.Parse()
.MUntil(result.commandName, dotCharacter)
.MatchS(".")
.MUntil(result.subCommandName, dotCharacter)
.FreeSelf();
if (result.subCommandName.IsEmpty()) {
result.subCommandName.FreeSelf();
result.subCommandName = none;
}
resolvedValue.FreeSelf();
return result;
}
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) {
local Parser parser;
// We are only interested in messages that start with `chatCommandPrefix`
parser = _.text.Parse(message);
if (!parser.Match(chatCommandPrefix).Ok()) {
parser.FreeSelf();
return true;
}
// Pass input to command feature
HandleInputWith(parser, sender);
parser.FreeSelf();
return false;
}
private function HandleMutate(string command, PlayerController sendingPlayer) {
local Parser parser;
local EPlayer sender;
// A lot of other mutators use these commands
if (command ~= "help") return;
if (command ~= "version") return;
if (command ~= "status") return;
if (command ~= "credits") return;
parser = _.text.ParseString(command);
sender = _.players.FromController(sendingPlayer);
HandleInputWith(parser, sender);
sender.FreeSelf();
parser.FreeSelf();
}
private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup) {
local int i;
local ArrayList groupArray;
local Command nextCommand;
groupArray = groupedCommands.GetArrayList(commandGroup);
if (groupArray == none) {
return;
}
while (i < groupArray.GetLength()) {
nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none && nextCommand.class == commandClass) {
groupArray.RemoveIndex(i);
} else {
i += 1;
}
_.memory.Free(nextCommand);
}
if (groupArray.GetLength() == 0) {
groupedCommands.RemoveItem(commandGroup);
}
_.memory.Free(groupArray);
}
/// This method allows to forcefully enable `Command_Feature` along with "mutate" input in case
/// something goes wrong.
///
@ -295,6 +241,152 @@ public final static function Text GetChatPrefix() {
return none;
}
/// Returns `true` iff some voting is currently active.
public final function bool IsVotingRunning() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
return (currentVoting != none);
}
/// Returns instance of the active voting.
///
/// `none` iff no voting is currently active.
public final function Voting GetCurrentVoting() {
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
if (currentVoting != none) {
currentVoting.NewRef();
}
return currentVoting;
}
/// `true` if voting under the given name (case-insensitive) is already registered.
public final function bool IsVotingRegistered(BaseText processName) {
local int i;
for (i = 0; i < registeredVotings.length; i += 1) {
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) {
return true;
}
}
return false;
}
/// Returns class of the [`Voting`] registered under given name.
public final function StartVotingResult StartVoting(BaseText processName) {
local int i;
local Text votingSettingsName;
local class<Voting> processClass;
if (currentVoting != none && currentVoting.HasEnded()) {
_.memory.Free(currentVoting);
currentVoting = none;
}
if (currentVoting != none) {
return SVR_AlreadyInProgress;
}
for (i = 0; i < registeredVotings.length; i += 1) {
if (processName.Compare(registeredVotings[i].processName, SCASE_INSENSITIVE)) {
processClass = registeredVotings[i].processClass;
}
}
if (processClass == none) {
return SVR_UnknownVoting;
}
currentVoting = Voting(_.memory.Allocate(processClass));
currentVoting.Start(votingSettingsName);
return SVR_Success;
}
/// Registers a new voting class to be accessible through the [`Commands_Feature`].
///
/// When a voting class is registered, players can access it using the standard AcediaCore's "vote"
/// command.
/// However, note that registering a voting class is not mandatory for it to be usable.
/// In fact, if you want to prevent players from initiating a particular voting, you should avoid
/// registering it in this feature.
public final function RegisterVotingClass(class<Voting> newVotingClass) {
local int i;
local ACommandVote votingCommand;
local NamedVoting newRecord;
local Text votingName;
if (newVotingClass == none) return;
votingCommand = GetVotingCommand();
if (votingCommand == none) return;
// We can freely release this reference here, since another reference is guaranteed to be kept in registered command
_.memory.Free(votingCommand);
// But just to make sure
if (!votingCommand.IsAllocated()) return;
// First we check whether we already added this class
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processClass == newVotingClass) {
return;
}
}
votingName = newVotingClass.static.GetPreferredName();
if (votingName.Compare(P("yes")) || votingName.Compare(P("no"))) {
_.logger.Auto(errYesNoVotingNamesReserved).ArgClass(newVotingClass).Arg(votingName);
return;
}
// Check for duplicates
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processName.Compare(votingName)) {
_.logger
.Auto(errVotingWithSameNameAlreadyRegistered)
.ArgClass(newVotingClass)
.Arg(votingName)
.ArgClass(registeredVotings[i].processClass);
return;
}
}
newRecord.processClass = newVotingClass;
newRecord.processName = votingName;
registeredVotings[registeredVotings.length] = newRecord;
votingCommand.AddVotingInfo(votingName, newVotingClass);
}
/// Unregisters a voting class from the [`Commands_Feature`], preventing players from accessing it
/// through the standard AcediaCore "vote" command.
///
/// This method does not stop any existing voting processes associated with the unregistered class.
///
/// Use this method to remove a voting class that is no longer needed or to prevent players from
/// initiating a particular voting. Note that removing a voting class is a linear operation that may
/// take some time if many votings are currently registered. It is not expected to be a common
/// operation and should be used sparingly.
public final function RemoveVotingClass(class<Voting> newVotingClass) {
local int i;
local ACommandVote votingCommand;
if (newVotingClass == none) {
return;
}
for (i = 0; i < registeredVotings.length; i += 1) {
if (registeredVotings[i].processClass == newVotingClass) {
_.memory.Free(registeredVotings[i].processName);
registeredVotings.Remove(i, 1);
}
}
votingCommand = GetVotingCommand();
if (votingCommand == none) {
return;
}
// Simply rebuild the whole voting set from scratch
votingCommand.ResetVotingInfo();
for (i = 0; i < registeredVotings.length; i += 1) {
votingCommand.AddVotingInfo(
registeredVotings[i].processName,
registeredVotings[i].processClass);
}
_.memory.Free(votingCommand);
}
/// Registers given command class, making it available.
///
/// # Errors
@ -506,8 +598,122 @@ public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer)
return errorOccured;
}
private final function ACommandVote GetVotingCommand() {
local AcediaObject registeredAsVote;
if (registeredCommands != none) {
registeredAsVote = registeredCommands.GetItem(P("vote"));
if (registeredAsVote != none && registeredAsVote.class == class'ACommandVote') {
return ACommandVote(registeredAsVote);
}
_.memory.Free(registeredAsVote);
}
return none;
}
// Parses command's name into `CommandCallPair` - sub-command is filled in case
// specified name is an alias with specified sub-command name.
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
local Text resolvedValue;
local MutableText userSpecifiedName;
local CommandCallPair result;
local Text.Character dotCharacter;
if (parser == none) return result;
if (!parser.Ok()) return result;
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
// This isn't an alias
if (resolvedValue == none) {
result.commandName = userSpecifiedName;
return result;
}
// It is an alias - parse it
dotCharacter = _.text.GetCharacter(".");
resolvedValue.Parse()
.MUntil(result.commandName, dotCharacter)
.MatchS(".")
.MUntil(result.subCommandName, dotCharacter)
.FreeSelf();
if (result.subCommandName.IsEmpty()) {
result.subCommandName.FreeSelf();
result.subCommandName = none;
}
resolvedValue.FreeSelf();
return result;
}
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) {
local Parser parser;
// We are only interested in messages that start with `chatCommandPrefix`
parser = _.text.Parse(message);
if (!parser.Match(chatCommandPrefix).Ok()) {
parser.FreeSelf();
return true;
}
// Pass input to command feature
HandleInputWith(parser, sender);
parser.FreeSelf();
return false;
}
private function HandleMutate(string command, PlayerController sendingPlayer) {
local Parser parser;
local EPlayer sender;
// A lot of other mutators use these commands
if (command ~= "help") return;
if (command ~= "version") return;
if (command ~= "status") return;
if (command ~= "credits") return;
parser = _.text.ParseString(command);
sender = _.players.FromController(sendingPlayer);
HandleInputWith(parser, sender);
sender.FreeSelf();
parser.FreeSelf();
}
private final function RemoveClassFromGroup(class<Command> commandClass, BaseText commandGroup) {
local int i;
local ArrayList groupArray;
local Command nextCommand;
groupArray = groupedCommands.GetArrayList(commandGroup);
if (groupArray == none) {
return;
}
while (i < groupArray.GetLength()) {
nextCommand = Command(groupArray.GetItem(i));
if (nextCommand != none && nextCommand.class == commandClass) {
groupArray.RemoveIndex(i);
} else {
i += 1;
}
_.memory.Free(nextCommand);
}
if (groupArray.GetLength() == 0) {
groupedCommands.RemoveItem(commandGroup);
}
_.memory.Free(groupArray);
}
private final function ReleaseNameVotingsArray(out array<NamedVoting> toRelease) {
local int i;
for (i = 0; i < toRelease.length; i += 1) {
_.memory.Free(toRelease[i].processName);
toRelease[i].processName = none;
}
toRelease.length = 0;
}
defaultproperties {
configClass = class'Commands'
errCommandDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Command `%3` with the same name will be ignored.")
errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
errVotingWithSameNameAlreadyRegistered = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the name \"%2\" when voting process `%3` is already registered. This is likely caused by conflicting mods.")
errYesNoVotingNamesReserved = (l=LOG_Error,m="Attempting to register voting process with class `%1` under the reserved name \"%2\". This is an issue with the mod that provided the voting, please contact its author.")
}

437
sources/BaseAPI/API/Commands/Voting/Voting.uc

@ -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}"
}

84
sources/BaseAPI/API/Commands/Voting/VotingModel.uc

@ -142,6 +142,14 @@ public final function Initialize(VotingPolicies policies) {
status = VPM_InProgress;
}
/// Returns whether voting has already concluded.
///
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check
/// whether either of them was enough to conclude the voting result.
public final function bool HasEnded() {
return (status != VPM_Uninitialized && status != VPM_InProgress);
}
/// Returns current status of voting.
///
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check
@ -224,6 +232,69 @@ public final function bool IsVotingAllowedFor(UserID voter) {
return false;
}
/// Returns the current vote status for the given voter.
///
/// If the voter was previously allowed to vote, voted, and had their right to vote revoked, their
/// vote will only count if policies allow voters to leave mid-vote.
/// Otherwise, the method will return [`PVS_NoVote`].
public final function PlayerVoteStatus GetVote(UserID voter) {
local int i;
if (voter == none) {
return PVS_NoVote;
}
for (i = 0; i < votesFor.length; i += 1) {
if (voter.IsEqual(votesFor[i])) {
return PVS_VoteFor;
}
}
for (i = 0; i < votesAgainst.length; i += 1) {
if (voter.IsEqual(votesAgainst[i])) {
return PVS_VoteAgainst;
}
}
if (policyCanLeave) {
for (i = 0; i < storedVotesFor.length; i += 1) {
if (voter.IsEqual(storedVotesFor[i])) {
return PVS_VoteFor;
}
}
for (i = 0; i < storedVotesAgainst.length; i += 1) {
if (voter.IsEqual(storedVotesAgainst[i])) {
return PVS_VoteAgainst;
}
}
}
return PVS_NoVote;
}
/// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() {
if (policyCanLeave) {
return votesFor.length + storedVotesFor.length;
} else {
return votesFor.length;
}
}
/// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() {
if (policyCanLeave) {
return votesAgainst.length + storedVotesAgainst.length;
} else {
return votesAgainst.length;
}
}
/// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() {
if (policyCanLeave) {
return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length;
} else {
return allowedVoters.length;
}
}
// Checks if provided user has already voted.
// Only checks among users that are currently allowed to vote, even if their past vote still counts.
private final function PlayerVoteStatus HasVoted(UserID voter) {
@ -254,16 +325,9 @@ private final function RecountVotes() {
if (status != VPM_InProgress) {
return;
}
if (policyCanLeave) {
totalVotesFor = votesFor.length + storedVotesFor.length;
totalVotesAgainst = votesAgainst.length + storedVotesAgainst.length;
totalPossibleVotes =
allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length;
} else {
totalVotesFor = votesFor.length;
totalVotesAgainst = votesAgainst.length;
totalPossibleVotes = allowedVoters.length;
}
totalVotesFor = GetVotesFor();
totalVotesAgainst = GetVotesAgainst();
totalPossibleVotes = GetTotalPossibleVotes();
lowerVoteCount = Min(totalVotesFor, totalVotesAgainst);
upperVoteCount = Max(totalVotesFor, totalVotesAgainst);
undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount);

84
sources/BaseAPI/API/Commands/Voting/VotingSettings.uc

@ -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"
}

12
sources/BaseAPI/AcediaEnvironment/AcediaEnvironment.uc

@ -39,6 +39,8 @@ var private array<int> enabledFeaturesLifeVersions;
var private string manifestSuffix;
var private const config bool debugMode;
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
@ -214,6 +216,15 @@ private final function ReadManifest(class<_manifest> manifestClass) {
}
}
/// Returns `true` iff AcediaCore is running in the debug mode.
///
/// AcediaCore's debug mode allows features to enable functionality that is only useful during
/// development.
/// Whether AcediaCore is running in a debug mode is decided at launch and cannot be changed.
public final function bool IsDebugging() {
return debugMode;
}
/// Returns all packages registered in the caller [`AcediaEnvironment`].
public final function array< class<_manifest> > GetAvailablePackages() {
return availablePackages;
@ -390,6 +401,7 @@ public final function DisableAllFeatures() {
defaultproperties
{
manifestSuffix = ".Manifest"
debugMode = true
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")

4
sources/Color/ColorAPI.uc

@ -1070,13 +1070,13 @@ public final function bool ResolveShortTagColor(
}
return false;
}
//LightGray=(R=211,G=211,B=211,A=255)
defaultproperties
{
TextDefault=(R=255,G=255,B=255,A=255)
TextHeader=(R=128,G=0,B=128,A=255)
TextSubHeader=(R=147,G=112,B=219,A=255)
TextSubtle=(R=128,G=128,B=128,A=255)
TextSubtle=(R=211,G=211,B=211,A=255)
TextEmphasis=(R=0,G=128,B=255,A=255)
TextPositive=(R=60,G=220,B=20,A=255)
TextNeutral=(R=255,G=255,B=0,A=255)

Loading…
Cancel
Save