11using System ;
2+ using System . Collections . Concurrent ;
23using System . Collections . Generic ;
34using System . Diagnostics ;
45using System . Linq ;
@@ -70,6 +71,12 @@ public class ServerBattle : Battle
7071
7172 public CommandPoll ActivePoll { get ; private set ; }
7273
74+ // Dictionary tracking cooldown for each user that fails a poll
75+ private readonly ConcurrentDictionary < string , DateTime > pollFailCooldowns = new ConcurrentDictionary < string , DateTime > ( ) ;
76+
77+ // Returns the count of non-spectators in the current battle
78+ public int NonSpectatorPlayerCount => Users . Values . Count ( x => x != null && ! x . IsSpectator ) ;
79+
7380 public bool IsAutohost { get ; private set ; }
7481 public bool IsDefaultGame { get ; private set ; } = true ;
7582 public bool IsCbalEnabled { get ; private set ; } = true ;
@@ -174,7 +181,7 @@ public List<string> GetAllUserNames()
174181 {
175182 var ret = Users . Select ( x => x . Key ) . ToList ( ) ;
176183 if ( spring . IsRunning ) ret . AddRange ( spring . Context . ActualPlayers . Select ( x => x . Name ) ) ;
177- return ret . Distinct ( ) . ToList ( ) ;
184+ return ret . Distinct ( ) . Where ( x => x != null ) . ToList ( ) ;
178185 }
179186
180187 public BattleCommand GetCommandByName ( string name )
@@ -652,13 +659,48 @@ public async Task<bool> StartVote(Func<string, string> eligibilitySelector, List
652659 await Respond ( creator , $ "Please wait, another poll already in progress: { ActivePoll . Topic } ") ;
653660 return false ;
654661 }
662+
663+ // Check if the user is on cooldown due to a failed poll
664+ if ( creator != null && IsOnPollCooldown ( creator ? . User , out var remain ) )
665+ {
666+ await Respond ( creator , $ "You cannot start a vote for { remain } seconds.") ;
667+ return false ;
668+ }
669+
670+
655671 await poll . Setup ( eligibilitySelector , options , creator , topic ) ;
656672 ActivePoll = poll ;
657673 pollTimer . Interval = timeout * 1000 ;
658674 pollTimer . Enabled = true ;
659675 return true ;
660676 }
661677
678+
679+ private bool IsUserModerator ( string username )
680+ {
681+ if ( Users . TryGetValue ( username , out var ubs ) && ( ubs ? . LobbyUser ? . IsAdmin == true ) )
682+ return true ;
683+ if ( server . ConnectedUsers . TryGetValue ( username , out var con ) && ( con ? . User ? . IsAdmin == true ) ) // command can be sent by someone not in the battle
684+ return true ;
685+ return false ;
686+ }
687+
688+
689+ private bool IsOnPollCooldown ( string username , out int remainSeconds )
690+ {
691+ remainSeconds = 0 ;
692+ if ( pollFailCooldowns . TryGetValue ( username , out var blockedUntil ) )
693+ {
694+ var diff = blockedUntil - DateTime . UtcNow ;
695+ if ( diff . TotalSeconds > 0 )
696+ {
697+ remainSeconds = ( int ) Math . Ceiling ( diff . TotalSeconds ) ;
698+ return true ;
699+ }
700+ }
701+ return false ;
702+ }
703+
662704
663705 public async void StopVote ( )
664706 {
@@ -669,7 +711,26 @@ public async void StopVote()
669711 if ( ActivePoll != null ) await ActivePoll . End ( false ) ;
670712 if ( pollTimer != null ) pollTimer . Enabled = false ;
671713 ActivePoll = null ;
714+
715+ // Let the poll announce results or do final DB logging
672716 await oldPoll ? . PublishResult ( ) ;
717+
718+
719+ // 1) Did the poll pass?
720+ bool pollPassed = oldPoll ? . Outcome ? . ChosenOption != null ;
721+
722+ // 2) Who started this poll?
723+ string creatorName = oldPoll ? . Creator ? . User ;
724+
725+ // 3) If poll failed and conditions are met => apply 30s cooldown
726+ if ( ! string . IsNullOrEmpty ( creatorName ) && // user is known
727+ ! pollPassed // poll is a failure
728+ && IsAutohost // only relevant in autohost
729+ && NonSpectatorPlayerCount >= 10
730+ && ! IsUserModerator ( creatorName ) )
731+ {
732+ pollFailCooldowns [ creatorName ] = DateTime . UtcNow . AddSeconds ( 30 ) ;
733+ }
673734 }
674735 catch ( Exception ex )
675736 {
0 commit comments