From 63593c12dae3d2854bec27f8d084de24e4f7917a Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Wed, 12 Apr 2023 16:53:18 -0400 Subject: [PATCH 01/18] Start work to automatically shut down inactive servers --- cogs/ranked.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 0b33e41..9513653 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -163,7 +163,8 @@ def create_game(game_type): logger.info(offset) logger.info(qdata.game_size) qsize = qdata.queue.qsize() - players = [qdata.queue.get() for _ in range(qsize)] + players = [qdata.queue.get() + for _ in range(qsize)] # type: list[discord.Member] qdata.game = Game(players[0 + offset:qdata.game_size + offset]) for player in players[0:offset]: qdata.queue.put(player) @@ -1116,7 +1117,7 @@ async def rules(self, interaction: discord.Interaction): class Game: - def __init__(self, players): + def __init__(self, players: list[discord.Member]): self.players = set(players) if len(players) > 2: self.captains = random.sample(self.players, 2) @@ -1230,6 +1231,39 @@ async def setup(bot: commands.Bot) -> None: ) +def shutdown_server_inactivity(server: int): + # if server is in a ranked queue, clear the match + for queue in game_queues.values(): + if queue.server_port == server: + # queue.clear_match() # TODO: clear match + # TODO: send message to players + # TODO: punish players that dodged + return + + # otherwise, just stop the process + stop_server_process(server) + + +def server_has_players(server: int) -> bool: + return True # TODO: read players from xrc server stdout + + +# TODO: warn_server_inactivity should happen if a ranked match isn't full - not just if it is empty +def warn_server_inactivity(server: int): + # if server is in a ranked queue, send a message to the players + for queue in game_queues.values(): + if queue.server_port == server: + if queue.game: + for player in queue.game.players: + # send a message to the players + task = asyncio.create_task(player.send( + "Your ranked match has been inactive - if it does not start within 10 minutes, it will be cancelled.")) + task.add_done_callback(lambda _: logger.info( + f"Sent message to {player.name}")) + pass + return + + @repeat(every(1).hour) def check_queue_joins(): """every hour, check if any queue_joins are older than 2 hours @@ -1250,6 +1284,30 @@ def check_queue_joins(): queue_joins.pop(item, None) +@repeat(every(10).minutes) +def check_empty_servers(): + """every 10 minutes, check if any servers are empty + if it is empty, add it to the list of empty servers + if it was empty last time we checked, stop the server + if it is not empty, remove it from the list of empty servers""" + + # remove servers that have closed + for server in empty_servers.copy(): + if server not in servers_active: + empty_servers.remove(server) + + for server in servers_active: + if server not in empty_servers: + if not server_has_players(server): + empty_servers.append(server) + warn_server_inactivity(server) + + else: + if not server_has_players(server): + shutdown_server_inactivity(server) + empty_servers.remove(server) + + class ScheduleThread(Thread): @classmethod def run(cls): From ebd17a706aa98cb335cbed37d180b540d72cc6c1 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 09:11:38 -0400 Subject: [PATCH 02/18] Refactor commands to be callable by cron tasks --- cogs/ranked.py | 130 +++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 52 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 9513653..c101b65 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -28,6 +28,8 @@ logger.fatal('SRC_API_TOKEN not found') raise RuntimeError('SRC_API_TOKEN not found') +GUILD_ID = 637407041048281098 + team_size = 6 team_size_alt = 4 approved_channels = [824691989366046750, 712297302857089025, @@ -114,21 +116,6 @@ } -async def remove_roles(ctx, qdata): - # Remove any current roles - - red_check = get(ctx.user.guild.roles, name=f"Red {qdata.full_game_name}") - blue_check = get(ctx.user.guild.roles, name=f"Blue {qdata.full_game_name}") - for player in red_check.members: - to_change = get(ctx.user.guild.roles, name="Ranked Red") - await player.remove_roles(to_change) - for player in blue_check.members: - to_change = get(ctx.user.guild.roles, name="Ranked Blue") - await player.remove_roles(to_change) - await qdata.red_role.delete() - await qdata.blue_role.delete() - - class XrcGame(): def __init__(self, game, alliance_size: int, api_short: str, full_game_name: str): self.queue = PlayerQueue() @@ -157,6 +144,17 @@ def __init__(self, game, alliance_size: int, api_short: str, full_game_name: str self.game_icon = None +async def remove_roles(guild: discord.Guild, qdata: XrcGame): + # Remove any current roles + + red_check = get(guild.roles, name=f"Red {qdata.full_game_name}") + blue_check = get(guild.roles, name=f"Blue {qdata.full_game_name}") + if red_check: + await red_check.delete() + if blue_check: + await blue_check.delete() + + def create_game(game_type): qdata = game_queues[game_type] offset = qdata.queue.qsize() - qdata.game_size @@ -775,7 +773,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i if qdata.red_series == 2: # await self.queue_auto(interaction) await interaction.followup.send("🟥 Red Wins! 🟥") - await remove_roles(interaction, qdata) + await remove_roles(interaction.user.guild, qdata) if qdata.server_port: stop_server_process(qdata.server_port) @@ -794,7 +792,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i elif qdata.blue_series == 2: # await self.queue_auto(interaction) await interaction.followup.send("🟦 Blue Wins! 🟦") - await remove_roles(interaction, qdata) + await remove_roles(interaction.user.guild, qdata) if qdata.server_port: stop_server_process(qdata.server_port) @@ -1087,28 +1085,30 @@ async def clearmatch(self, interaction: discord.Interaction, game: str): if (isinstance(interaction.user, discord.Member) and 699094822132121662 in [y.id for y in interaction.user.roles]): - if qdata.server_port: - stop_server_process(qdata.server_port) + await self.do_clear_match(interaction.user.guild, qdata) + await interaction.response.send_message("Cleared successfully!") + else: + await interaction.response.send_message("You don't have permission to do that!", ephemeral=True) - qdata.red_series = 2 - qdata.blue_series = 2 + async def do_clear_match(self, guild: discord.Guild, qdata: XrcGame): + if qdata.server_port: + stop_server_process(qdata.server_port) - await remove_roles(interaction, qdata) + qdata.red_series = 2 + qdata.blue_series = 2 - # kick to lobby - lobby = self.bot.get_channel(824692700364275743) - if qdata.red_channel: - for member in qdata.red_channel.members: - await member.move_to(lobby) - await qdata.red_channel.delete() - if qdata.blue_channel: - for member in qdata.blue_channel.members: - await member.move_to(lobby) - await qdata.blue_channel.delete() + await remove_roles(guild, qdata) - await interaction.response.send_message("Cleared successfully!") - else: - await interaction.response.send_message("You don't have permission to do that!", ephemeral=True) + # kick to lobby + lobby = self.bot.get_channel(824692700364275743) + if qdata.red_channel: + for member in qdata.red_channel.members: + await member.move_to(lobby) + await qdata.red_channel.delete() + if qdata.blue_channel: + for member in qdata.blue_channel.members: + await member.move_to(lobby) + await qdata.blue_channel.delete() @app_commands.command(name="rules", description="Posts a link the the rules") async def rules(self, interaction: discord.Interaction): @@ -1223,20 +1223,39 @@ def __contains__(self, item: discord.Member): game_queues = {game['short_code']: XrcGame( game['game'], game['players_per_alliance'], game['short_code'], game['name']) for game in games} +cog = None # type: Ranked | None +guild = None # type: discord.Guild | None + async def setup(bot: commands.Bot) -> None: + cog = Ranked(bot) + guild = bot.get_guild(GUILD_ID) + assert guild is not None + await bot.add_cog( - Ranked(bot), - guilds=[discord.Object(id=637407041048281098)] + cog, + guilds=[guild] ) + # background thread for running schedule tasks + ScheduleThread().start() + def shutdown_server_inactivity(server: int): # if server is in a ranked queue, clear the match for queue in game_queues.values(): if queue.server_port == server: - # queue.clear_match() # TODO: clear match - # TODO: send message to players + if cog and guild: + task = asyncio.create_task(cog.do_clear_match(guild, queue)) + task.add_done_callback(lambda _: logger.info( + f"Match cleared for server {server} due to inactivity")) + + if queue.game: + for player in queue.game.players: + # send a message to the players + asyncio.create_task(player.send( + "Your ranked match has been cancelled due to inactivity.")) + # TODO: punish players that dodged return @@ -1245,10 +1264,22 @@ def shutdown_server_inactivity(server: int): def server_has_players(server: int) -> bool: - return True # TODO: read players from xrc server stdout + """ + Check if the server has players on it + For casual matches, this is just if at least one player is present + For ranked matches, this is if the match is full + """ + needed_players = 1 + for queue in game_queues.values(): + if queue.server_port == server: + needed_players = queue.game_size + break + + # TODO: read players from xrc server stdout + + return True -# TODO: warn_server_inactivity should happen if a ranked match isn't full - not just if it is empty def warn_server_inactivity(server: int): # if server is in a ranked queue, send a message to the players for queue in game_queues.values(): @@ -1256,10 +1287,8 @@ def warn_server_inactivity(server: int): if queue.game: for player in queue.game.players: # send a message to the players - task = asyncio.create_task(player.send( - "Your ranked match has been inactive - if it does not start within 10 minutes, it will be cancelled.")) - task.add_done_callback(lambda _: logger.info( - f"Sent message to {player.name}")) + asyncio.create_task(player.send( + "Your ranked match has been inactive - if all players are not present within 10 minutes, the match will be cancelled.")) pass return @@ -1275,14 +1304,15 @@ def check_queue_joins(): if player in queue: queue.remove(player) # send a message to the player - task = asyncio.create_task(player.send( + asyncio.create_task(player.send( "You have been removed from a queue because you have been in the queue for more than 2 hours.")) - task.add_done_callback(lambda _: logger.info( - f"Sent message to {player.name}")) to_remove.append((queue, player)) for item in to_remove: queue_joins.pop(item, None) + if cog: + asyncio.create_task(cog.update_ranked_display()) + @repeat(every(10).minutes) def check_empty_servers(): @@ -1313,7 +1343,3 @@ class ScheduleThread(Thread): def run(cls): run_pending() sleep(1) - - -# background thread for running schedule tasks -ScheduleThread().start() From 3a9bf611cd8a57a00fdb42e0cdd171898ccba007 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Thu, 13 Apr 2023 22:57:20 -0400 Subject: [PATCH 03/18] Remove player from all other queues when starting a match Fixes #16 --- cogs/ranked.py | 75 ++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index c101b65..f538106 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -29,12 +29,13 @@ raise RuntimeError('SRC_API_TOKEN not found') GUILD_ID = 637407041048281098 +QUEUE_CHANNEL = 824691989366046750 team_size = 6 team_size_alt = 4 approved_channels = [824691989366046750, 712297302857089025, 650967104933330947, 754569102873460776, 754569222260129832] -header = {"x-api-key": SRC_API_TOKEN} +HEADER = {"x-api-key": SRC_API_TOKEN} PORTS = [11115, 11116, 11117, 11118, 11119, 11120] # dictionary mapping port number to process of running server @@ -158,8 +159,6 @@ async def remove_roles(guild: discord.Guild, qdata: XrcGame): def create_game(game_type): qdata = game_queues[game_type] offset = qdata.queue.qsize() - qdata.game_size - logger.info(offset) - logger.info(qdata.game_size) qsize = qdata.queue.qsize() players = [qdata.queue.get() for _ in range(qsize)] # type: list[discord.Member] @@ -169,6 +168,14 @@ def create_game(game_type): players = [qdata.queue.get() for _ in range(qdata.queue.qsize())] for player in players: qdata.queue.put(player) + + # Remove selected players from all other queues + for game in game_queues.values(): + if game.game_type != game_type: + for player in qdata.game.players: + if player in game.queue: + game.queue.remove(player) + return qdata @@ -424,10 +431,10 @@ async def q(self, interaction: discord.Interaction, game: str): url = f'https://secondrobotics.org/api/ranked/player/{interaction.user.id}' - x = requests.get(url, headers=header) - thing = x.json() + x = requests.get(url, headers=HEADER) + res = x.json() - if not thing["exists"]: + if not res["exists"]: await interaction.response.send_message( "You must register for an account at before you can queue.", ephemeral=True) @@ -436,26 +443,24 @@ async def q(self, interaction: discord.Interaction, game: str): qdata = game_queues[game] if (isinstance(interaction.channel, discord.TextChannel) and - interaction.channel.id in approved_channels and + interaction.channel.id == QUEUE_CHANNEL and isinstance(interaction.user, discord.Member)): player = interaction.user channel = interaction.channel if player in qdata.queue: await interaction.response.send_message("You are already in this queue.", ephemeral=True) return - if channel.id == 824691989366046750: - roles = [y.id for y in interaction.user.roles] - if qdata.red_role is None or qdata.blue_role is None: - pass - else: - ranked_roles = [qdata.red_role.id, qdata.blue_role.id] - # Returns false if not in a game currently. Looks for duplicates between roles and ranked_roles - queue_check = bool(set(roles).intersection(ranked_roles)) - if queue_check: - await interaction.response.send_message("You are already playing in a game!", ephemeral=True) - return + + roles = [y.id for y in interaction.user.roles] + if qdata.red_role is None or qdata.blue_role is None: + pass else: - await interaction.response.send_message("You can't queue in this channel.", ephemeral=True) + ranked_roles = [qdata.red_role.id, qdata.blue_role.id] + # Returns false if not in a game currently. Looks for duplicates between roles and ranked_roles + queue_check = bool(set(roles).intersection(ranked_roles)) + if queue_check: + await interaction.response.send_message("You are already playing in a game!", ephemeral=True) + return qdata.queue.put(player) await self.update_ranked_display() @@ -468,6 +473,8 @@ async def q(self, interaction: discord.Interaction, game: str): else: await interaction.channel.send( f"Queue for {qdata.full_game_name} is now full! You can start as soon as the current match concludes.") + else: + await interaction.response.send_message(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) # @app_commands.choices(game=games_choices) @@ -504,7 +511,7 @@ async def leave(self, interaction: discord.Interaction, game: str): if (isinstance(interaction.channel, discord.TextChannel) and isinstance(interaction.user, discord.Member) and - interaction.channel.id in approved_channels): + interaction.channel.id == QUEUE_CHANNEL): player = interaction.user if player in qdata.queue: qdata.queue.remove(player) @@ -516,6 +523,8 @@ async def leave(self, interaction: discord.Interaction, game: str): else: await interaction.response.send_message("You aren't in this queue.", ephemeral=True) return + else: + await interaction.response.send_message(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) @app_commands.choices(game=games_choices) @app_commands.command(description="Remove someone else from the queue") @@ -523,7 +532,7 @@ async def leave(self, interaction: discord.Interaction, game: str): async def kick(self, interaction: discord.Interaction, player: discord.Member, game: str): logger.info(f"{interaction.user.name} called /kick") qdata = game_queues[game] - if isinstance(interaction.channel, discord.TextChannel) and interaction.channel.id in approved_channels: + if isinstance(interaction.channel, discord.TextChannel) and interaction.channel.id == QUEUE_CHANNEL: if player in qdata.queue: qdata.queue.remove(player) await self.update_ranked_display() @@ -583,13 +592,11 @@ async def startmatch(self, interaction: discord.Interaction, game: str): await interaction.followup.send("Current match incomplete.", ephemeral=True) return - await interaction.response.defer() + if interaction.channel is None or interaction.channel.id == QUEUE_CHANNEL: + await interaction.followup.send(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) + return - if interaction.channel is not None and ( - interaction.channel.id == 712297302857089025 or - interaction.channel.id == 754569222260129832 or - interaction.channel.id == 754569102873460776): - return await self.random(interaction, game) + await interaction.response.defer() password = str(random.randint(100, 999)) min_players = games_players[game] @@ -600,13 +607,8 @@ async def startmatch(self, interaction: discord.Interaction, game: str): else: qdata.server_port = port qdata.server_password = password - chooser = random.randint(1, 10) - if chooser < 0: # 6 - logger.info("Captains") - # await self.captains(interaction) - else: - logger.info("Randoms") - await self.random(interaction, game) + await self.update_ranked_display() + await self.random(interaction, game) # async def captains(self, ctx): # qdata = self.get_queue(ctx) @@ -731,7 +733,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i logger.info(game) qdata = game_queues[game] if (isinstance(interaction.channel, discord.TextChannel) and - interaction.channel.id == 824691989366046750 and + interaction.channel.id == QUEUE_CHANNEL and isinstance(interaction.user, discord.Member)): roles = [y.id for y in interaction.user.roles] @@ -758,6 +760,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i await interaction.followup.send("Series is complete already!", ephemeral=True) return else: + await interaction.followup.send(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) return logger.info("Checking ") # Red wins @@ -829,7 +832,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i "red_score": red_score, "blue_score": blue_score } - x = requests.post(url, json=json, headers=header) + x = requests.post(url, json=json, headers=HEADER) logger.info(x.json()) response = x.json() # Getting match Number From 0993b3d157d1ee38165f7a45f76e65a9d62feea3 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Thu, 13 Apr 2023 23:33:50 -0400 Subject: [PATCH 04/18] Add a button to the game over embed that allows you to instantly re-join queue Fixes #15 --- cogs/ranked.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index f538106..f9b303c 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -426,6 +426,9 @@ async def queueall(self, interaction: discord.Interaction, @app_commands.choices(game=games_choices) @app_commands.command(name="queue", description="Add yourself to the queue") async def q(self, interaction: discord.Interaction, game: str): + await self.queue_player(interaction, game) + + async def queue_player(self, interaction: discord.Interaction, game: str): """Enter's player into queue for upcoming matches""" logger.info(f"{interaction.user.name} called /q") @@ -862,7 +865,18 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i embed.add_field(name=f'🟦 BLUE 🟦 *({blue_score})*', value=f"{blue_out}", inline=True) - await interaction.channel.send(embed=embed) + + class RejoinQueueView(discord.ui.View): + def __init__(self, qdata: XrcGame, cog: Ranked): + super().__init__() + self.qdata = qdata + self.cog = cog + + @discord.ui.button(label="Rejoin Queue", style=discord.ButtonStyle.blurple) + async def rejoin_queue(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.cog.queue_player(interaction, self.qdata.api_short) + + await interaction.channel.send(embed=embed, view=RejoinQueueView(qdata, self)) async def random(self, interaction, game_type): logger.info("randomizing") @@ -1080,8 +1094,8 @@ async def display_teams(self, ctx, qdata): # df = gspread_dataframe.get_as_dataframe(wks) # logger.info(df["Match Number"].iloc[0]) - @app_commands.choices(game=games_choices) - @app_commands.command(name="clearmatch", description="Clears current running match") + @ app_commands.choices(game=games_choices) + @ app_commands.command(name="clearmatch", description="Clears current running match") async def clearmatch(self, interaction: discord.Interaction, game: str): logger.info(f"{interaction.user.name} called /clearmatch") qdata = game_queues[game] From 3879719e91630664770b87b9189227e311278871 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 09:22:00 -0400 Subject: [PATCH 05/18] Allow admins to queueall --- cogs/ranked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index f9b303c..7eafe6f 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -410,7 +410,7 @@ async def queueall(self, interaction: discord.Interaction, members = [member1, member2, member3, member4, member5, member6] members_clean = [i for i in members if i] added_players = "" - if interaction.user.id == 118000175816900615: + if isinstance(interaction.user, discord.Member) and interaction.user.guild_permissions.administrator: for member in members_clean: qdata.queue.put(member) added_players += f"{member.display_name}\n" From 106bd50c59dd06a60369432b4843722f47022565 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 09:31:48 -0400 Subject: [PATCH 06/18] Fix == to != in channel guard --- cogs/ranked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 7eafe6f..2b23203 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -595,7 +595,7 @@ async def startmatch(self, interaction: discord.Interaction, game: str): await interaction.followup.send("Current match incomplete.", ephemeral=True) return - if interaction.channel is None or interaction.channel.id == QUEUE_CHANNEL: + if interaction.channel is None or interaction.channel.id != QUEUE_CHANNEL: await interaction.followup.send(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) return From fd39a26c86c64b97977b7012c4fa2e2cec4c91f1 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:00:57 -0400 Subject: [PATCH 07/18] Temp make cron faster --- cogs/ranked.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 2b23203..23d2ae5 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1310,14 +1310,14 @@ def warn_server_inactivity(server: int): return -@repeat(every(1).hour) +@repeat(every(1).minute) def check_queue_joins(): """every hour, check if any queue_joins are older than 2 hours if they are, remove them from the queue if they are not, do nothing""" to_remove: list[tuple[PlayerQueue, discord.Member]] = [] for (queue, player), timestamp in queue_joins.items(): - if (datetime.now() - timestamp).total_seconds() > 60 * 60 * 2: + if (datetime.now() - timestamp).total_seconds() > 60: if player in queue: queue.remove(player) # send a message to the player From c7fe297cd5e9eabae0fc3b012bca979edcfc201f Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:10:02 -0400 Subject: [PATCH 08/18] Temp add logs --- cogs/ranked.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/ranked.py b/cogs/ranked.py index 23d2ae5..44257f9 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1315,10 +1315,13 @@ def check_queue_joins(): """every hour, check if any queue_joins are older than 2 hours if they are, remove them from the queue if they are not, do nothing""" + logger.info("Checking queue joins...") to_remove: list[tuple[PlayerQueue, discord.Member]] = [] for (queue, player), timestamp in queue_joins.items(): if (datetime.now() - timestamp).total_seconds() > 60: + logger.info("Found old queue join!") if player in queue: + logger.info("Removing player from queue...") queue.remove(player) # send a message to the player asyncio.create_task(player.send( From 59fe29caee9b6415f0f756d7903edf39e73e0ca6 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:24:02 -0400 Subject: [PATCH 09/18] Convert to using discord tasks instead of scheduler --- cogs/ranked.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 44257f9..f05a6cc 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -16,9 +16,7 @@ from discord.app_commands import Choice import zipfile import shutil -from schedule import repeat, every, run_pending -from threading import Thread -from time import sleep +from discord.ext import tasks logger = logging.getLogger('discord') load_dotenv() @@ -1132,6 +1130,26 @@ async def rules(self, interaction: discord.Interaction): logger.info(f"{interaction.user.name} called /rules") await interaction.response.send_message("The rules can be found here: <#700411727430418464>") + @tasks.loop(minutes=1) + async def check_queue_joins(): + """every hour, check if any queue_joins are older than 2 hours + if they are, remove them from the queue + if they are not, do nothing""" + logger.info("Checking queue joins...") + to_remove: list[tuple[PlayerQueue, discord.Member]] = [] + for (queue, player), timestamp in queue_joins.items(): + if (datetime.now() - timestamp).total_seconds() > 60: + logger.info("Found old queue join!") + if player in queue: + logger.info("Removing player from queue...") + queue.remove(player) + # send a message to the player + await player.send( + "You have been removed from a queue because you have been in the queue for more than 2 hours.") + to_remove.append((queue, player)) + for item in to_remove: + queue_joins.pop(item, None) + class Game: def __init__(self, players: list[discord.Member]): From 86a301fcefa48fa27a93039a3e4e476d03ab0823 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:26:31 -0400 Subject: [PATCH 10/18] Start check queue task --- cogs/ranked.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/ranked.py b/cogs/ranked.py index f05a6cc..b9c5b3c 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -246,6 +246,7 @@ class Ranked(commands.Cog): def __init__(self, bot): self.bot = bot self.ranked_display = None + self.check_queue_joins.start() async def update_ranked_display(self): if self.ranked_display is None: From ee27f28274abce7bf951cf980ce09c0ab569fbef Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:27:31 -0400 Subject: [PATCH 11/18] Fix method instead of function --- cogs/ranked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index b9c5b3c..564a05d 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1132,7 +1132,7 @@ async def rules(self, interaction: discord.Interaction): await interaction.response.send_message("The rules can be found here: <#700411727430418464>") @tasks.loop(minutes=1) - async def check_queue_joins(): + async def check_queue_joins(self): """every hour, check if any queue_joins are older than 2 hours if they are, remove them from the queue if they are not, do nothing""" From ba29e10fb825a3a16bc50e3b5a600479e6fc2508 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:33:58 -0400 Subject: [PATCH 12/18] Fix for loop dictionary removal --- cogs/ranked.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 564a05d..e248b93 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1131,25 +1131,20 @@ async def rules(self, interaction: discord.Interaction): logger.info(f"{interaction.user.name} called /rules") await interaction.response.send_message("The rules can be found here: <#700411727430418464>") - @tasks.loop(minutes=1) + @tasks.loop(seconds=20) async def check_queue_joins(self): """every hour, check if any queue_joins are older than 2 hours if they are, remove them from the queue if they are not, do nothing""" - logger.info("Checking queue joins...") - to_remove: list[tuple[PlayerQueue, discord.Member]] = [] - for (queue, player), timestamp in queue_joins.items(): - if (datetime.now() - timestamp).total_seconds() > 60: - logger.info("Found old queue join!") + for (queue, player), timestamp in queue_joins.copy().items(): + if (datetime.now() - timestamp).total_seconds() > 10: if player in queue: - logger.info("Removing player from queue...") queue.remove(player) # send a message to the player await player.send( "You have been removed from a queue because you have been in the queue for more than 2 hours.") - to_remove.append((queue, player)) - for item in to_remove: - queue_joins.pop(item, None) + else: + queue_joins.pop((queue, player)) class Game: From 57c3f20753f71463041a92ed0543bed70a0d0b96 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 10:38:36 -0400 Subject: [PATCH 13/18] Revert "Temp make cron faster" This reverts commit 5af20f76e8f963fbacf496f3e98d7e2e094d4ca3. --- cogs/ranked.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index e248b93..a980b4c 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1131,13 +1131,13 @@ async def rules(self, interaction: discord.Interaction): logger.info(f"{interaction.user.name} called /rules") await interaction.response.send_message("The rules can be found here: <#700411727430418464>") - @tasks.loop(seconds=20) + @tasks.loop(minutes=10) async def check_queue_joins(self): """every hour, check if any queue_joins are older than 2 hours if they are, remove them from the queue if they are not, do nothing""" for (queue, player), timestamp in queue_joins.copy().items(): - if (datetime.now() - timestamp).total_seconds() > 10: + if (datetime.now() - timestamp).total_seconds() > 60 * 60 * 2: if player in queue: queue.remove(player) # send a message to the player @@ -1145,6 +1145,7 @@ async def check_queue_joins(self): "You have been removed from a queue because you have been in the queue for more than 2 hours.") else: queue_joins.pop((queue, player)) + await self.update_ranked_display() class Game: From 88768c87b59cbf951d1d8ff98952359735a5f659 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 11:08:32 -0400 Subject: [PATCH 14/18] Make rejoin queue button only appear on gg --- cogs/ranked.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index a980b4c..7af2ecf 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -775,6 +775,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i logger.info(f"Red {qdata.red_series}") logger.info(f"Blue {qdata.blue_series}") + gg = True if qdata.red_series == 2: # await self.queue_auto(interaction) await interaction.followup.send("🟥 Red Wins! 🟥") @@ -816,6 +817,7 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i logger.info(interaction) await interaction.followup.send("Score Submitted") logger.info("got here") + gg = False logger.info("Blah") # Finding player ids @@ -875,7 +877,10 @@ def __init__(self, qdata: XrcGame, cog: Ranked): async def rejoin_queue(self, interaction: discord.Interaction, button: discord.ui.Button): await self.cog.queue_player(interaction, self.qdata.api_short) - await interaction.channel.send(embed=embed, view=RejoinQueueView(qdata, self)) + if gg: + await interaction.channel.send(embed=embed, view=RejoinQueueView(qdata, self)) + else: + await interaction.channel.send(embed=embed) async def random(self, interaction, game_type): logger.info("randomizing") From 0ef6163de08a830eb75771e5e1074e25602b6bcc Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 11:18:28 -0400 Subject: [PATCH 15/18] Wait until bot is ready before job --- cogs/ranked.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cogs/ranked.py b/cogs/ranked.py index 7af2ecf..396a4e3 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -1152,6 +1152,10 @@ async def check_queue_joins(self): queue_joins.pop((queue, player)) await self.update_ranked_display() + @check_queue_joins.before_loop + async def before_check_queue_joins(self): + await self.bot.wait_until_ready() + class Game: def __init__(self, players: list[discord.Member]): From a6b0d74ca74cb8d1cf2e95f6ad3ce0e3b451200a Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 11:32:18 -0400 Subject: [PATCH 16/18] Convert check_empty_servers to discord task --- cogs/ranked.py | 86 ++++++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 58 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index 396a4e3..f6e1b9b 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -247,6 +247,7 @@ def __init__(self, bot): self.bot = bot self.ranked_display = None self.check_queue_joins.start() + self.check_empty_servers.start() async def update_ranked_display(self): if self.ranked_display is None: @@ -1156,6 +1157,33 @@ async def check_queue_joins(self): async def before_check_queue_joins(self): await self.bot.wait_until_ready() + @tasks.loop(minutes=10) + async def check_empty_servers(self): + """every 10 minutes, check if any servers are empty + if it is empty, add it to the list of empty servers + if it was empty last time we checked, stop the server + if it is not empty, remove it from the list of empty servers""" + + # remove servers that have closed + for server in empty_servers.copy(): + if server not in servers_active: + empty_servers.remove(server) + + for server in servers_active: + if server not in empty_servers: + if not server_has_players(server): + empty_servers.append(server) + warn_server_inactivity(server) + + else: + if not server_has_players(server): + shutdown_server_inactivity(server) + empty_servers.remove(server) + + @check_empty_servers.before_loop + async def before_check_empty_servers(self): + await self.bot.wait_until_ready() + class Game: def __init__(self, players: list[discord.Member]): @@ -1278,9 +1306,6 @@ async def setup(bot: commands.Bot) -> None: guilds=[guild] ) - # background thread for running schedule tasks - ScheduleThread().start() - def shutdown_server_inactivity(server: int): # if server is in a ranked queue, clear the match @@ -1332,58 +1357,3 @@ def warn_server_inactivity(server: int): "Your ranked match has been inactive - if all players are not present within 10 minutes, the match will be cancelled.")) pass return - - -@repeat(every(1).minute) -def check_queue_joins(): - """every hour, check if any queue_joins are older than 2 hours - if they are, remove them from the queue - if they are not, do nothing""" - logger.info("Checking queue joins...") - to_remove: list[tuple[PlayerQueue, discord.Member]] = [] - for (queue, player), timestamp in queue_joins.items(): - if (datetime.now() - timestamp).total_seconds() > 60: - logger.info("Found old queue join!") - if player in queue: - logger.info("Removing player from queue...") - queue.remove(player) - # send a message to the player - asyncio.create_task(player.send( - "You have been removed from a queue because you have been in the queue for more than 2 hours.")) - to_remove.append((queue, player)) - for item in to_remove: - queue_joins.pop(item, None) - - if cog: - asyncio.create_task(cog.update_ranked_display()) - - -@repeat(every(10).minutes) -def check_empty_servers(): - """every 10 minutes, check if any servers are empty - if it is empty, add it to the list of empty servers - if it was empty last time we checked, stop the server - if it is not empty, remove it from the list of empty servers""" - - # remove servers that have closed - for server in empty_servers.copy(): - if server not in servers_active: - empty_servers.remove(server) - - for server in servers_active: - if server not in empty_servers: - if not server_has_players(server): - empty_servers.append(server) - warn_server_inactivity(server) - - else: - if not server_has_players(server): - shutdown_server_inactivity(server) - empty_servers.remove(server) - - -class ScheduleThread(Thread): - @classmethod - def run(cls): - run_pending() - sleep(1) From 3876c6d3c23421cb11ddbd36d1fce55779d91ef3 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 13:27:58 -0400 Subject: [PATCH 17/18] Implement server_has_players --- cogs/ranked.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index f6e1b9b..d1c800d 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -222,7 +222,8 @@ def start_server_process(game: str, comment: str, password: str = "", admin: str f"GameOption={restart_mode}", f"FrameRate={frame_rate}", f"Tmode={'On' if tournament_mode else 'Off'}", f"Register={'On' if register else 'Off'}", f"Spectators={spectators}", f"UpdateTime={update_time}", f"MaxData=10000", f"StartWhenReady={'On' if start_when_ready else 'Off'}", f"Comment={comment}", - f"Password={password}", f"Admin={admin}", f"GameSettings={game_settings}", f"MinPlayers={min_players}"] + f"Password={password}", f"Admin={admin}", f"GameSettings={game_settings}", f"MinPlayers={min_players}"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=False ) logger.info(f"Server launched on port {port}: '{comment}'") @@ -1171,12 +1172,12 @@ async def check_empty_servers(self): for server in servers_active: if server not in empty_servers: - if not server_has_players(server): + if not (await server_has_players(server)): empty_servers.append(server) warn_server_inactivity(server) else: - if not server_has_players(server): + if not (await server_has_players(server)): shutdown_server_inactivity(server) empty_servers.remove(server) @@ -1329,7 +1330,7 @@ def shutdown_server_inactivity(server: int): stop_server_process(server) -def server_has_players(server: int) -> bool: +async def server_has_players(server: int) -> bool: """ Check if the server has players on it For casual matches, this is just if at least one player is present @@ -1341,9 +1342,34 @@ def server_has_players(server: int) -> bool: needed_players = queue.game_size break - # TODO: read players from xrc server stdout + # read players from xrc server stdout + process = servers_active.get(server, None) + if process is None or process.poll() is not None or process.stdout is None or process.stdin is None: + return False - return True + process.stdin.write(b"PLAYERS\n") + process.stdin.flush() + + while True: + line = await process.stdout.readline() + logger.info(f"Server {server} stdout: {line}") + if not line == b'_BEGIN_\n': + break + + players = [] + while True: + line = await process.stdout.readline() + logger.info(f"Server {server} stdout: {line}") + if line == b'_END_\n': + break + players.append(line.decode().strip()) + + logger.info(f"Server {server} players: {players}") + + if len(players) >= needed_players: + return True + + return False def warn_server_inactivity(server: int): From 21940daf788dc97a9cdbca6aee272d95ee8a3c98 Mon Sep 17 00:00:00 2001 From: Nicholas Bottone Date: Fri, 14 Apr 2023 13:47:57 -0400 Subject: [PATCH 18/18] Remove extra comments --- cogs/ranked.py | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/cogs/ranked.py b/cogs/ranked.py index d1c800d..2621327 100644 --- a/cogs/ranked.py +++ b/cogs/ranked.py @@ -585,7 +585,6 @@ async def startmatch(self, interaction: discord.Interaction, game: str): logger.info(f"{interaction.user.name} called /startmatch") qdata = game_queues[game] - logger.info(qdata.red_series) if not qdata.queue.qsize() >= qdata.game_size: await interaction.followup.send("Queue is not full.", ephemeral=True) return @@ -734,7 +733,6 @@ async def startmatch(self, interaction: discord.Interaction, game: str): async def submit(self, interaction: discord.Interaction, game: str, red_score: int, blue_score: int): logger.info(f"{interaction.user.name} called /submit") await interaction.response.defer() - logger.info(game) qdata = game_queues[game] if (isinstance(interaction.channel, discord.TextChannel) and interaction.channel.id == QUEUE_CHANNEL and @@ -754,19 +752,12 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i await interaction.followup.send("You are ineligible to submit!", ephemeral=True) return - logger.info(qdata.red_series) - logger.info(qdata.blue_series) if qdata.red_series == 2 or qdata.blue_series == 2: - logger.info("INSIDE") - logger.info(qdata.red_series) - logger.info(qdata.blue_series) - logger.info(interaction) await interaction.followup.send("Series is complete already!", ephemeral=True) return else: await interaction.followup.send(f"<#{QUEUE_CHANNEL}> >:(", ephemeral=True) return - logger.info("Checking ") # Red wins if int(red_score) > int(blue_score): qdata.red_series += 1 @@ -775,8 +766,6 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i elif int(red_score) < int(blue_score): qdata.blue_series += 1 - logger.info(f"Red {qdata.red_series}") - logger.info(f"Blue {qdata.blue_series}") gg = True if qdata.red_series == 2: # await self.queue_auto(interaction) @@ -816,12 +805,9 @@ async def submit(self, interaction: discord.Interaction, game: str, red_score: i await member.move_to(lobby) await qdata.blue_channel.delete() else: - logger.info(interaction) await interaction.followup.send("Score Submitted") - logger.info("got here") gg = False - logger.info("Blah") # Finding player ids red_ids = [] blue_ids = [] @@ -885,25 +871,18 @@ async def rejoin_queue(self, interaction: discord.Interaction, button: discord.u await interaction.channel.send(embed=embed) async def random(self, interaction, game_type): - logger.info("randomizing") qdata = create_game(game_type) if not qdata.game: await interaction.followup.send("No game found", ephemeral=True) return - logger.info(f"players: {qdata.game.players}") - logger.info(f"Team size {qdata.team_size}") red = random.sample(qdata.game.players, int(qdata.team_size)) - logger.info(red) for player in red: - logger.info(player) qdata.game.add_to_red(player) blue = list(qdata.game.players) - logger.info(blue) for player in blue: - logger.info(player) qdata.game.add_to_blue(player) await self.display_teams(interaction, qdata) @@ -1054,28 +1033,24 @@ async def display_teams(self, ctx, qdata): category=category, overwrites=overwrites_red) qdata.blue_channel = await ctx.guild.create_voice_channel(name=f"🟦{qdata.full_game_name}🟦", category=category, overwrites=overwrites_blue) - logger.info(qdata.blue_role) - logger.info(qdata.red_role) for player in qdata.game.red: await player.add_roles(discord.utils.get(ctx.guild.roles, id=qdata.red_role.id)) try: await player.move_to(qdata.red_channel) except Exception as e: - logger.info(e) + logger.error(e) pass for player in qdata.game.blue: await player.add_roles(discord.utils.get(ctx.guild.roles, id=qdata.blue_role.id)) try: await player.move_to(qdata.blue_channel) except Exception as e: - logger.info(e) + logger.error(e) pass - logger.info("Roles Created") description = f"Server started for you on port {qdata.server_port} with password {qdata.server_password}" if qdata.server_port else None - logger.info(qdata.game.red) embed = discord.Embed( color=0x34dceb, title=f"Teams have been picked for __{qdata.full_game_name}__!", description=description) embed.set_thumbnail(url=qdata.game_icon) @@ -1157,6 +1132,7 @@ async def check_queue_joins(self): @check_queue_joins.before_loop async def before_check_queue_joins(self): await self.bot.wait_until_ready() + await asyncio.sleep(5) @tasks.loop(minutes=10) async def check_empty_servers(self): @@ -1299,7 +1275,7 @@ def __contains__(self, item: discord.Member): async def setup(bot: commands.Bot) -> None: cog = Ranked(bot) - guild = bot.get_guild(GUILD_ID) + guild = await bot.fetch_guild(GUILD_ID) assert guild is not None await bot.add_cog(