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

682 lines
22 KiB
Ucode

class ScrnVotingHandlerMut extends Mutator
Config(ScrnVoting);
var globalconfig int VoteCountDown;
var globalconfig float VotePercent, VotePercentCountDown;
const VERSION = 40600;
var localized string strVersion;
var protected bool bVoteInProgress;
var protected int VoteSecondsLeft;
var protected array <PlayerController> VotersYes, VotersNo;
var localized string strVoteInitiated, strVotePassed, strVoteFailed, strVoteFailedInitiator, strVoteFailedPlayer, strVoteTimeout,
strVoteInProgress, strNoVoteInProgress, strAlreadyVoted, strVoteUnknown, strVoteIllegal, strVoteHasNoEffect,
strForcedByAdmin, strVotedYes, strVotedNo, strVoteStatus, strSpectatorsCantVote, strOtherTeamVote;
var localized string strTimeout, strPlayerBlocked;
var string VoteInfo; // user-friendly information about current vore
var protected int VoteIndex;
var protected string VoteValue; // value to be set, if vote passes
var protected ScrnVotingOptions CurrentVotingObject; //objects that controls current vote value
var transient PlayerController VoteInitiator;
var transient TeamInfo VotedTeam;
var PlayerController TeamCaptains[2];
var transient PlayerController VotedPlayer;
var protected transient bool bVotedPlayer; // if true, VotedPlayer is used in the current vote
var protected array<ScrnVotingOptions> VotingOptions;
var array <localized string> HelpInfo;
var private bool bHelpPrepared;
var class<VHReplicationInfo> VHReplicationInfoClass;
var VHReplicationInfo VHRI;
var byte VoteID;
var globalconfig float VoteCoolDown;
var PlayerController FailedVoter;
var float FailedVoterBlockTime; // FailedVoter is unable to start a new vote till this time
// Players who can't start voting. Can be used by voting options
var array<PlayerController> BlockedVoters;
struct SVoteGroup
{
var String GroupName;
var ScrnVotingOptions VO;
};
var array<SVoteGroup> VoteGroups;
static function string GetVersionStr()
{
local String msg, s;
local int v, sub_v;
msg = default.strVersion;
v = VERSION / 100;
sub_v = VERSION % 100;
s = String(int(v%100));
if ( len(s) == 1 )
s = "0" $ s;
if ( sub_v > 0 )
s @= "(BETA "$sub_v$")";
ReplaceText(msg, "%n", s);
s = String(v/100);
ReplaceText(msg, "%m",s);
return msg;
}
function Mutate(string MutateString, PlayerController Sender)
{
if ( MutateString ~= "vote" )
Vote("", Sender);
else if ( left(MutateString, 5) ~= "vote " )
Vote(Right(MutateString, len(MutateString)-5), Sender);
else {
super.Mutate(MutateString, Sender);
if ( MutateString ~= "version" )
Sender.ClientMessage(GetVersionStr());
}
}
static function ScrnVotingHandlerMut GetVotingHandler(GameInfo Game)
{
local Mutator mut;
for ( mut = Game.BaseMutator; mut != none; mut = mut.NextMutator ) {
if ( ScrnVotingHandlerMut(mut) != none )
return ScrnVotingHandlerMut(mut);
}
return none;
}
function ScrnVotingOptions AddVotingOptions(class<ScrnVotingOptions> VO_Class)
{
local int i;
local ScrnVotingOptions VO;
// don't add same class twice
for ( i=0; i < VotingOptions.length; ++i ) {
if ( VotingOptions[i].class == VO_Class )
return VotingOptions[i];
}
VO = spawn(VO_Class, self);
if ( VO != none ) {
VO.VotingHandler = self;
VotingOptions[VotingOptions.length] = VO;
VO.InitGroups();
}
return VO;
}
function RemoveVotingOptions(class<ScrnVotingOptions> VO_Class, optional bool bChildsToo)
{
local int i, j;
for ( i=0; i < VotingOptions.length; ++i ) {
if ( VotingOptions[i].class == VO_Class || (bChildsToo && ClassIsChildOf(VotingOptions[i].class, VO_Class)) ) {
// unregister groups too
for ( j=0; j<VoteGroups.length; ++j) {
if ( VoteGroups[j].VO == VotingOptions[i] )
VoteGroups.remove(j--, 1);
}
VotingOptions.remove(i--, 1);
}
}
}
function ScrnVotingOptions GetGroupOptions(string GroupName)
{
local int i;
for ( i=0; i<VoteGroups.length; ++i ) {
if ( VoteGroups[i].GroupName ~= GroupName )
return VoteGroups[i].VO;
}
return none;
}
function RegisterGroup(string GroupName, ScrnVotingOptions VO)
{
local int i;
if ( InStr(GroupName, " ") != -1 ) {
warn("Voting group can not have spaces: '" $ GroupName $ "'");
return;
}
if ( GetGroupOptions(GroupName) != none )
return; // already registered
i = VoteGroups.length;
VoteGroups.insert(i, 1);
VoteGroups[i].GroupName = GroupName;
VoteGroups[i].VO = VO;
}
static function string Trim(string str)
{
local int l;
l = len(str);
while ( l > 0 && left(str,1) == " " )
str = right(str, --l);
while ( l > 0 && right(str,1) == " " )
str = left(str, --l);
return str;
}
function bool IsVoteInProgress()
{
return bVoteInProgress;
}
function string GetVoteStatus(optional PlayerController Voter, optional bool bVotedYes)
{
local string result;
if ( !bVoteInProgress )
return strNoVoteInProgress;
if ( Voter != none && Voter.PlayerReplicationInfo != none ) {
if ( bVotedYes )
result = strVotedYes;
else
result = strVotedNo;
ReplaceText(result, "%p", Voter.PlayerReplicationInfo.PlayerName);
}
else {
result = strVoteStatus;
}
ReplaceText(result, "%v", VoteInfo);
ReplaceText(result, "%y", String(VotersYes.length));
ReplaceText(result, "%n", String(VotersNo.length));
return result;
}
// returns number of players who are able to vote
function int MaxVoters()
{
local int result;
if ( VotedTeam != none )
result = VotedTeam.Size;
else
result = Level.Game.NumPlayers;
return max(result, 1);
}
function Vote(string VoteString, PlayerController Sender)
{
local int i, idx;
local string k, v, g;
local ScrnVotingOptions VO;
if ( Sender == none || Sender.PlayerReplicationInfo == none )
return; // wtf?
VoteString = caps(Trim(VoteString));
if ( VoteString == "" || VoteString == "HELP" || VoteString ~= "INFO" || VoteString ~= "?") {
SendHelp(Sender);
}
else if ( VoteString == "STATUS" ) {
Sender.ClientMessage(GetVoteStatus());
}
else if ( Sender.PlayerReplicationInfo.bOnlySpectator && !Sender.PlayerReplicationInfo.bAdmin ) {
Sender.ClientMessage(strSpectatorsCantVote);
}
else if ( VoteString == "YES" || VoteString == "TRYYES" ) {
if ( bVoteInProgress ) {
if ( Sender.PlayerReplicationInfo.bAdmin ) {
VotePassed(Sender.PlayerReplicationInfo.PlayerName);
return;
}
else if ( VotedTeam != none ) {
if ( VotedTeam != Sender.PlayerReplicationInfo.Team ) {
if ( VoteString != "TRYYES" )
Sender.ClientMessage(strOtherTeamVote);
return;
}
else if ( VotedTeam.TeamIndex < 2 && TeamCaptains[VotedTeam.TeamIndex] == Sender ) {
VotePassed(Sender.PlayerReplicationInfo.PlayerName);
return;
}
}
for ( i = 0; i < VotersNo.length; i++ ) {
if ( VotersNo[i] == Sender ) {
VotersNo.remove(i, 1);
break;
}
}
for ( i = 0; i < VotersYes.length; i++ ) {
if ( VotersYes[i] == Sender ) {
Sender.ClientMessage(strAlreadyVoted);
return;
}
}
VotersYes[VotersYes.length] = Sender;
BroadcastMessage(GetVoteStatus(Sender, true));
if ( float(VotersYes.length) / MaxVoters() * 100.0 >= VotePercent )
VotePassed();
else {
VHRI.UpdateVoteStatus(self, VHRI.VS_INPROGRESS, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
}
else if ( VoteString != "TRYYES" )
Sender.ClientMessage(strNoVoteInProgress);
}
else if ( VoteString == "NO" || VoteString == "TRYNO" ) {
if ( bVoteInProgress ) {
if ( Sender.PlayerReplicationInfo.bAdmin ) {
VoteInfo @= strForcedByAdmin;
ReplaceText(VoteInfo, "%p", Sender.PlayerReplicationInfo.PlayerName);
VoteFailed();
return;
}
else if ( VotedTeam != none ) {
if ( VotedTeam != Sender.PlayerReplicationInfo.Team ) {
if ( VoteString != "TRYNO" )
Sender.ClientMessage(strOtherTeamVote);
return;
}
else if ( VotedTeam.TeamIndex < 2 && TeamCaptains[VotedTeam.TeamIndex] == Sender ) {
VoteInfo @= strForcedByAdmin;
ReplaceText(VoteInfo, "%p", Sender.PlayerReplicationInfo.PlayerName);
VoteFailed();
return;
}
}
for ( i = 0; i < VotersYes.length; i++ ) {
if ( VotersYes[i] == Sender ) {
VotersYes.remove(i, 1);
break;
}
}
for ( i = 0; i < VotersNo.length; i++ ) {
if ( VotersNo[i] == Sender ) {
Sender.ClientMessage(strAlreadyVoted);
return;
}
}
VotersNo[VotersNo.length] = Sender;
BroadcastMessage(GetVoteStatus(Sender, false));
if ( float(MaxVoters() - VotersNo.length) / MaxVoters() * 100.0 < VotePercent )
VoteFailed(); // vote fails when it can't theoretically pass the vote
else
VHRI.UpdateVoteStatus(self, VHRI.VS_INPROGRESS, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
else if ( VoteString != "TRYNO" )
Sender.ClientMessage(strNoVoteInProgress);
}
else if ( bVoteInProgress )
Sender.ClientMessage(strVoteInProgress);
else if ( !MayStartVoting(Sender))
Sender.ClientMessage(strPlayerBlocked);
else {
VoteInfo = "";
VotedPlayer = none;
VotedTeam = none;
if ( Divide(VoteString, " ", k, v) ) {
k = trim(k);
v = trim(v);
}
else {
k = VoteString;
v = "";
}
// check if first is a group
VO = GetGroupOptions(k);
if ( VO != none ) {
// first word is a group, then second is a key and others - value
g = k;
if ( Divide(v, " ", k, v) ) {
k = trim(k);
v = trim(v);
}
else {
k = v;
v = "";
}
if ( k == "" || k == "HELP" || k ~= "?" )
VO.SendGroupHelp(Sender, g);
else {
idx = VO.GetGroupVoteIndex(Sender, g, k, v, VoteInfo);
switch (idx) {
case VO.VOTE_UNKNOWN:
Sender.ClientMessage(strVoteIllegal);
VO.SendGroupHelp(Sender, g);
break;
case VO.VOTE_ILLEGAL:
Sender.ClientMessage(strVoteIllegal);
break;
case VO.VOTE_NOEFECT:
Sender.ClientMessage(strVoteHasNoEffect);
break;
case VO.VOTE_LOCAL:
return;
default:
CurrentVotingObject = VO;
VoteIndex = idx;
VoteValue = v;
if ( VoteInfo == "" )
VoteInfo = g @ k @ v;
StartVoting(Sender);
}
}
}
else {
// no group found - do global vote check
for ( i = 0; i < VotingOptions.length; ++i ) {
if ( VotingOptions[i] == none || !VotingOptions[i].bAcceptsGlobalVotes )
continue;
VO = VotingOptions[i];
idx = VO.GetVoteIndex(Sender, k, v, VoteInfo);
if ( idx != VO.VOTE_UNKNOWN ) {
switch (idx) {
case VO.VOTE_ILLEGAL:
Sender.ClientMessage(strVoteIllegal);
break;
case VO.VOTE_NOEFECT:
Sender.ClientMessage(strVoteHasNoEffect);
break;
case VO.VOTE_LOCAL:
return;
default:
CurrentVotingObject = VO;
VoteIndex = idx;
VoteValue = v;
if ( VoteInfo == "" )
VoteInfo = k @ v;
StartVoting(Sender);
}
return;
}
}
if ( VO != none && idx == VO.VOTE_UNKNOWN ) {
Sender.ClientMessage(strVoteUnknown);
}
}
}
}
function bool MayStartVoting(PlayerController Sender)
{
local int i;
if ( Sender.PlayerReplicationInfo == none )
return false;
// admins can always vote
if ( Sender.PlayerReplicationInfo.bAdmin )
return true;
// spectators can't vote
if ( Sender.PlayerReplicationInfo.bOnlySpectator )
return false;
if ( Level.Game.NumPlayers == 1 )
return true;
if ( Sender == FailedVoter && Level.TimeSeconds < FailedVoterBlockTime )
return false;
for ( i=0; i<BlockedVoters.length; ++i ) {
if ( BlockedVoters[i] == Sender)
return false;
}
return true;
}
function BroadcastMessage(string msg)
{
local Controller P;
local PlayerController Player;
for ( P = Level.ControllerList; P != none; P = P.nextController ) {
Player = PlayerController(P);
if ( Player != none ) {
Player.ClientMessage(msg);
}
}
}
function StartVoting(PlayerController Initiator)
{
local string msg;
if ( Initiator == none || Initiator.PlayerReplicationInfo == none )
return;
VoteInitiator = Initiator;
bVotedPlayer = VotedPlayer != none;
VotersYes.Length = 1;
VotersYes[0] = Initiator;
VotersNo.Length = 0;
bVoteInProgress = true;
VoteID++;
if ( Level.NetMode == NM_Standalone || Initiator.PlayerReplicationInfo.bAdmin ) {
VotePassed(Initiator.PlayerReplicationInfo.PlayerName);
}
else if ( MaxVoters() == 1 && !Initiator.PlayerReplicationInfo.bOnlySpectator ) {
VotePassed();
}
else if ( VotedTeam != none && VotedTeam.TeamIndex < 2 && TeamCaptains[VotedTeam.TeamIndex] == Initiator ) {
VotePassed(Initiator.PlayerReplicationInfo.PlayerName);
}
else {
msg = strVoteInitiated;
ReplaceText(msg, "%p", Initiator.PlayerReplicationInfo.PlayerName);
ReplaceText(msg, "%v", VoteInfo);
BroadcastMessage(chr(27)$chr(200)$chr(200)$chr(1)$msg);
VHRI.UpdateVoteStatus(self, VHRI.VS_INPROGRESS, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
VoteSecondsLeft = VoteCountDown;
SetTimer(1, true);
}
}
function Timer()
{
if ( VoteInitiator == none ) {
EndVoting();
BroadcastMessage(chr(27)$chr(200)$chr(1)$chr(1)$strVoteFailedInitiator);
VHRI.UpdateVoteStatus(self, VHRI.VS_FAILED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
else if ( bVotedPlayer && (VotedPlayer == none || VotedPlayer.PlayerReplicationInfo == none) ) {
EndVoting();
BroadcastMessage(chr(27)$chr(200)$chr(1)$chr(1)$strVoteFailedPlayer);
VHRI.UpdateVoteStatus(self, VHRI.VS_FAILED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
else if ( --VoteSecondsLeft <= 0 ) {
if ( float(VotersYes.length) / MaxVoters() * 100.0 >= VotePercentCountDown ) {
VotePassed(strTimeout);
}
else {
EndVoting();
BroadcastMessage(strVoteTimeout);
VHRI.UpdateVoteStatus(self, VHRI.VS_FAILED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
}
}
function EndVoting()
{
bVoteInProgress = false;
SetTimer(0, false);
}
function VotePassed(optional String ForcedByPlayerName)
{
local string msg;
if ( !bVoteInProgress )
return;
EndVoting();
if ( VoteInitiator == none ) {
BroadcastMessage(chr(27)$chr(200)$chr(1)$chr(1)$strVoteFailedInitiator);
VHRI.UpdateVoteStatus(self, VHRI.VS_FAILED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
return;
}
CurrentVotingObject.ApplyVoteValue(VoteIndex, VoteValue);
msg = strVotePassed;
ReplaceText(msg, "%v", VoteInfo);
if ( ForcedByPlayerName != "" ) {
msg @= strForcedByAdmin;
ReplaceText(msg, "%p", ForcedByPlayerName);
}
BroadcastMessage(chr(27)$chr(1)$chr(200)$chr(1)$msg);
VHRI.UpdateVoteStatus(self, VHRI.VS_PASSED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
function VoteFailed()
{
if ( VoteCoolDown > 0 ) {
FailedVoter = VoteInitiator;
FailedVoterBlockTime = Level.TimeSeconds + VoteCoolDown;
}
EndVoting();
BroadcastMessage(chr(27)$chr(200)$chr(1)$chr(1)$strVoteFailed);
VHRI.UpdateVoteStatus(self, VHRI.VS_FAILED, VoteInfo, VotersYes.length, VotersNo.length, VoteID);
}
static function string ParseHelpLine(string s)
{
ReplaceText(s, "%r", chr(27)$chr(200)$chr(1)$chr(1));
ReplaceText(s, "%g", chr(27)$chr(1)$chr(200)$chr(1));
ReplaceText(s, "%b", chr(27)$chr(1)$chr(100)$chr(200));
ReplaceText(s, "%w", chr(27)$chr(200)$chr(200)$chr(200));
ReplaceText(s, "%y", chr(27)$chr(200)$chr(200)$chr(1));
ReplaceText(s, "%p", chr(27)$chr(200)$chr(1)$chr(200));
ReplaceText(s, "%k", chr(27)$chr(64)$chr(64)$chr(64));
return s;
}
function PrepareHelp()
{
local int i, j, l;
local ScrnVotingOptions VO;
bHelpPrepared = true;
l = HelpInfo.length;
// add help infos from voting options
for ( j=0; j < VotingOptions.length; ++j ) {
VO = VotingOptions[j];
if ( VO != none) {
HelpInfo.insert(l, VO.HelpInfo.length);
for ( i = 0; i < VO.HelpInfo.length; ++i) {
HelpInfo[l++] = VO.HelpInfo[i];
}
}
}
for ( i = 0; i < l; ++i) {
HelpInfo[i] = ParseHelpLine(HelpInfo[i]);
}
}
function SendHelp(PlayerController Sender)
{
local int i;
if ( !bHelpPrepared )
PrepareHelp();
for ( i = 0; i < HelpInfo.length; ++i)
Sender.ClientMessage(HelpInfo[i]);
}
simulated function PostBeginPlay()
{
if ( Level.NetMode == NM_Client ) {
log("Voting Handler should be used only on the server side", class.outer.name);
Destroy();
return;
}
VHRI = Spawn(VHReplicationInfoClass, self);
VHRI.mutRef = Self;
}
function ServerTraveling(string URL, bool bItems)
{
if (NextMutator != None)
NextMutator.ServerTraveling(URL,bItems);
if ( VHRI != none ) {
VHRI.Destroy();
VHRI = none;
}
}
//returns true, if specifield vote is in progress
//pass -1 to VIndex to check all votes from specified ScrnVotingOptions object
function bool IsMyVotingRunning(ScrnVotingOptions VO, int VIndex)
{
return bVoteInProgress && CurrentVotingObject == VO && (VoteIndex == VIndex || VIndex == -1);
}
defaultproperties
{
VoteCountDown=70
VotePercent=51.000000
VotePercentCountDown=51.000000
strVersion="ScrN Voting Handler v%m.%n"
strVoteInitiated="%p initiated a vote: %v."
strVotePassed="Vote passed: %v"
strVoteFailed="Vote failed"
strVoteFailedInitiator="Vote failed due to initiator's disconnect"
strVoteFailedPlayer="Vote failed due to player's disconnect"
strVoteTimeout="Vote failed due to timeout"
strVoteInProgress="Another vote in progress"
strNoVoteInProgress="No vote in progress"
strAlreadyVoted="You've already voted"
strVoteUnknown="Unknown vote"
strVoteIllegal="Illegal vote arguments"
strVoteHasNoEffect="Vote has no effect"
strForcedByAdmin="(forced by %p)"
strVotedYes="%p voted YES on: %v (+%y -%n)"
strVotedNo="%p voted NO on: %v (+%y -%n)"
strVoteStatus="Current vote: %v (+%y -%n)"
strSpectatorsCantVote="Spectators can not vote!"
strOtherTeamVote="Can't participate in other team's voting!"
strTimeout="TIMEOUT"
strPlayerBlocked="You are disallowed to start a voting"
HelpInfo(0)="%bVoting Options:"
HelpInfo(1)="%gHELP %w Show this information"
HelpInfo(2)="%gYES%w|%gNO %w Accept|Decline current vote"
HelpInfo(3)="%gSTATUS %w Show status of the current vote"
VHReplicationInfoClass=Class'ScrnVotingHandlerV4.VHReplicationInfo'
VoteCoolDown=10.000000
bAddToServerPackages=True
GroupName="SCRN-VOTE"
FriendlyName="ScrN Voting Handler"
Description="Allows voting via MUTATE VOTE console command"
}