@@ -235,6 +235,7 @@ def __init__(self, game, alliance_size: int, api_short: str, full_game_name: str
235235 self .password_channel_id = None # type: int | None
236236 self .elo_history : list = []
237237 self .game_scores : list = []
238+ self .clear_vote_task : Optional [asyncio .Task ] = None
238239
239240 try :
240241 self .game_icon = game_logos [game ]
@@ -335,6 +336,49 @@ async def on_timeout(self):
335336 self .stop ()
336337
337338
339+ class ClearVoteView (View ):
340+ def __init__ (self , ranked_cog , guild : discord .Guild , match : XrcGame ):
341+ super ().__init__ (timeout = 120 )
342+ self .ranked_cog = ranked_cog
343+ self .guild = guild
344+ self .match = match
345+ self .votes : set = set ()
346+ self .total_players = len (match .game .red | match .game .blue )
347+ self .cleared = False
348+ self .message : Optional [discord .Message ] = None
349+
350+ async def interaction_check (self , interaction : discord .Interaction ) -> bool :
351+ if self .match .red_role in interaction .user .roles or self .match .blue_role in interaction .user .roles :
352+ return True
353+ await interaction .response .send_message ("You're not in this match." , ephemeral = True )
354+ return False
355+
356+ @discord .ui .button (label = "Vote to Clear" , style = ButtonStyle .red )
357+ async def vote_clear (self , interaction : discord .Interaction , button : discord .ui .Button ):
358+ if interaction .user .id in self .votes :
359+ await interaction .response .send_message ("You already voted to clear." , ephemeral = True )
360+ return
361+ self .votes .add (interaction .user .id )
362+ needed = self .total_players // 2 + 1
363+ if len (self .votes ) >= needed :
364+ self .cleared = True
365+ self .stop ()
366+ await interaction .response .send_message (
367+ f"Match cleared by vote ({ len (self .votes )} /{ self .total_players } )." )
368+ await self .ranked_cog .do_clear_match (self .guild , self .match )
369+ else :
370+ await interaction .response .send_message (
371+ f"Vote recorded ({ len (self .votes )} /{ self .total_players } , need { needed } to clear)." ,
372+ ephemeral = True )
373+
374+ async def on_timeout (self ):
375+ if not self .cleared and self .message :
376+ try :
377+ await self .message .edit (content = "Auto-clear vote expired." , view = None )
378+ except Exception :
379+ pass
380+
381+
338382async def remove_roles (guild : discord .Guild , qdata : XrcGame ):
339383 for role in [qdata .red_role , qdata .blue_role ]:
340384 if role :
@@ -1023,7 +1067,49 @@ async def move_player(player, channel):
10231067
10241068 asyncio .create_task (self .update_ranked_display ())
10251069
1070+ if match .clear_vote_task and not match .clear_vote_task .done ():
1071+ match .clear_vote_task .cancel ()
1072+ match .clear_vote_task = asyncio .create_task (self ._auto_clear_check (match , ctx .guild ))
1073+
1074+ async def _auto_clear_check (self , match : XrcGame , guild : discord .Guild ):
1075+ await asyncio .sleep (600 )
1076+
1077+ is_active = any (match in queue .matches for queue in game_queues .values ())
1078+ if not is_active or match .red_series >= 2 or match .blue_series >= 2 :
1079+ return
1080+
1081+ if not match .password_channel_id :
1082+ return
1083+ password_channel = guild .get_channel (match .password_channel_id )
1084+ if not password_channel :
1085+ return
1086+
1087+ total = len (match .game .red | match .game .blue )
1088+ needed = total // 2 + 1
1089+ embed = discord .Embed (
1090+ title = "Match Inactivity" ,
1091+ description = (
1092+ f"This match has been inactive for 10 minutes.\n "
1093+ f"Vote to clear if the match is not happening.\n "
1094+ f"**{ needed } /{ total } ** votes needed to clear."
1095+ ),
1096+ color = discord .Color .orange ()
1097+ )
1098+ view = ClearVoteView (self , guild , match )
1099+ try :
1100+ msg = await password_channel .send (
1101+ f"{ match .red_role .mention } { match .blue_role .mention } " ,
1102+ embed = embed ,
1103+ view = view
1104+ )
1105+ view .message = msg
1106+ except Exception as e :
1107+ logger .error (f"Error sending auto-clear vote for { match .full_game_name } : { e } " )
1108+
10261109 async def do_clear_match (self , guild : discord .Guild , match : XrcGame ):
1110+ if match .clear_vote_task and not match .clear_vote_task .done ():
1111+ match .clear_vote_task .cancel ()
1112+
10271113 if match .server_port :
10281114 server_actions = self .bot .get_cog ('ServerActions' )
10291115 server_actions .stop_server_process (match .server_port )
@@ -1689,6 +1775,12 @@ async def submit(self, interaction: discord.Interaction, red_score: int, blue_sc
16891775 embed = summary_embed
16901776 )
16911777 await self .handle_game_end (interaction , qdata , current_match , embed )
1778+ else :
1779+ if current_match .clear_vote_task and not current_match .clear_vote_task .done ():
1780+ current_match .clear_vote_task .cancel ()
1781+ current_match .clear_vote_task = asyncio .create_task (
1782+ self ._auto_clear_check (current_match , interaction .guild )
1783+ )
16921784
16931785 # Helper methods
16941786 def find_current_match (self , user_roles ):
@@ -1793,7 +1885,9 @@ def fmt(names, totals):
17931885 return embed
17941886
17951887 async def handle_game_end (self , interaction , qdata , current_match , embed ):
1796- # Get lobby channel
1888+ if current_match .clear_vote_task and not current_match .clear_vote_task .done ():
1889+ current_match .clear_vote_task .cancel ()
1890+
17971891 lobby = self .bot .get_channel (LOBBY_VC_ID )
17981892
17991893 # Move all members to lobby, then delete voice channels
0 commit comments