From bbc7bf6c8ee324b982d19ba8348538ee87ce58c8 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 6 Mar 2024 22:48:02 -0800 Subject: [PATCH 001/292] added mkwii branch --- cogs/SquadQueue.py | 102 +-------------------------------------- mkwii_sample_config.json | 34 +++++++++++++ mmr.py | 24 ++++----- mogi_objects.py | 83 ++++++++++++++++++------------- redeploy.sh | 8 +-- 5 files changed, 103 insertions(+), 148 deletions(-) create mode 100644 mkwii_sample_config.json diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7abd1e9..bbb4c25 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone, timedelta import time import json -from mmr import mk8dx_150cc_mmr, get_mmr_from_discord_id, mk8dx_150cc_fc +from mmr import mk8dx_150cc_mmr, get_mmr_from_discord_id from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio @@ -248,48 +248,6 @@ async def sub(self, interaction: discord.Interaction): await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) await interaction.response.send_message("Sent out request for sub.") - @app_commands.command(name="l") - @app_commands.checks.cooldown(1, 120, key=lambda i: (i.channel.id)) - @app_commands.guild_only() - async def list(self, interaction: discord.Interaction): - """Display the list of confirmed players for a mogi""" - mogi = self.get_mogi(interaction) - if mogi is None: - await interaction.response.send_message("Queue has not started yet.") - return - if not await self.is_started(interaction, mogi): - return - mogi_list = mogi.confirmed_list() - if len(mogi_list) == 0: - await interaction.response.send_message(f"There are no players in the queue - type `/c` to join") - return - - sorted_mogi_list = sorted(mogi_list, reverse=True) - msg = "Current Mogi List:\n" - for i in range(len(sorted_mogi_list)): - msg += f"{i+1}) " - msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) - msg += f" ({sorted_mogi_list[i].avg_mmr:.1f} MMR)\n" - if (len(sorted_mogi_list) % (12/mogi.size) != 0): - num_next = int(len(sorted_mogi_list) % (12/mogi.size)) - teams_per_room = int(12/mogi.size) - num_rooms = int(len(sorted_mogi_list) / (12/mogi.size))+1 - msg += f"[{num_next}/{teams_per_room}] players for {num_rooms} room(s)" - message = msg.split("\n") - bulk_msg = "" - for i in range(len(message)): - if len(bulk_msg + message[i] + "\n") > 2000: - await interaction.channel.send(bulk_msg) if interaction.response.is_done() else await interaction.response.send_message(bulk_msg) - bulk_msg = "" - bulk_msg += message[i] + "\n" - if len(bulk_msg) > 0: - await interaction.channel.send(bulk_msg) if interaction.response.is_done() else await interaction.response.send_message(bulk_msg) - - @list.error # Tell the user when they've got a cooldown - async def on_list_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): - if isinstance(error, app_commands.CommandOnCooldown): - await interaction.response.send_message("Wait before using `/l` command", ephemeral=True) - @tasks.loop(minutes=1) async def list_task(self): """Continually display the list of confirmed players for a mogi in the history channel""" @@ -378,37 +336,6 @@ async def on_message(self, message: discord.Message): if player: player.score = int(message.content) - @app_commands.command(name="scoreboard") - @app_commands.guild_only() - async def scoreboard(self, interaction: discord.Interaction): - """Displays the scoreboard of the room. Only works in thread channels for SQ rooms.""" - - mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( - interaction.channel_id), self.ongoing_events.values()) - if not mogi: - mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( - interaction.channel_id), self.old_events.values()) - if not mogi: - await interaction.response.send_message(f"The Mogi object cannot be found.", ephemeral=True) - return - - room = discord.utils.find( - lambda room: room.thread.id == interaction.channel_id, mogi.rooms) - - if not room: - await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) - return - - format = round(12/len(room.teams)) - - msg = f"!submit {format} {get_tier(room.mmr_average - 500)}\n" - for team in room.teams: - for player in team.players: - msg += f"{player.lounge_name} {player.score}\n" - if format != 1: - msg += "\n" - await interaction.response.send_message(msg) - @app_commands.command(name="remove_player") @app_commands.guild_only() async def remove_player(self, interaction: discord.Interaction, member: discord.Member): @@ -533,31 +460,6 @@ async def has_roles(self, member: discord.Member, guild_id: int, config): return True return False - # command to add staff to room thread channels; users can't add new users to private threads, - # so the bot has to with this command - @commands.command() - @commands.cooldown(1, 60, commands.BucketType.channel) - async def staff(self, ctx): - """Calls staff to the current channel. Only works in thread channels for SQ rooms.""" - is_room_thread = False - for mogi in self.ongoing_events.values(): - if mogi.is_room_thread(ctx.channel.id): - is_room_thread = True - break - for mogi in self.old_events.values(): - if mogi.is_room_thread(ctx.channel.id): - is_room_thread = True - break - if not is_room_thread: - return - if str(ctx.guild.id) not in ctx.bot.config["staff_roles"].keys(): - await ctx.send("There is no Lounge Staff role configured for this server") - return - lounge_staff_roles = ctx.bot.config["staff_roles"][str(ctx.guild.id)] - mentions = " ".join( - [ctx.guild.get_role(role).mention for role in lounge_staff_roles]) - await ctx.send(mentions) - async def end_voting(self): """Ends voting in all rooms with ongoing votes.""" try: @@ -669,7 +571,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F room_msg = msg mentions += " ".join([m.mention for m in extra_members if m is not None]) room_msg += "\nVote for format FFA, 2v2, 3v3, or 4v4.\n" - room_msg += "\nIf you need staff's assistance, use the `!staff` command in this channel.\n" + # room_msg += "\nIf you need staff's assistance, use the `!staff` command in this channel.\n" room_msg += mentions try: curr_room = rooms[i] diff --git a/mkwii_sample_config.json b/mkwii_sample_config.json new file mode 100644 index 0000000..94cf7de --- /dev/null +++ b/mkwii_sample_config.json @@ -0,0 +1,34 @@ +{ + "token": "", + "guild_id": 387347467332485122, + + "admin_roles": {"387347467332485122": ["Administrator", "Lounge Staff"]}, + + "queue_join_channel": 1215174119767932979, + "queue_sub_channel": 1215174160234315806, + "queue_history_channel": 1215174057453162496, + + "members_for_channels": {"387347467332485122": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095]}, + + "queue_messages": true, + "sec_between_queue_msgs": 2, + + "username": "username", + "password": "password", + "url": "https://www.mkwlounge.gg", + + "TIME_ADJUSTMENT": 0, + "QUEUE_OPEN_TIME": 60, + "JOINING_TIME": 55, + "EXTENSION_TIME": 3, + "MOGI_LIFETIME": 180, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200 +} \ No newline at end of file diff --git a/mmr.py b/mmr.py index 3e61701..26828d2 100644 --- a/mmr.py +++ b/mmr.py @@ -6,7 +6,7 @@ async def mk8dx_150cc_mmr(url, members): - base_url = url + '/api/player?' + base_url = url + '/api/ladderplayer.php?ladder_id=9&' players = [] timeout = aiohttp.ClientTimeout(total=10) try: @@ -15,40 +15,42 @@ async def mk8dx_150cc_mmr(url, members): auth=aiohttp.BasicAuth( "username", "password")) as session: for member in members: - request_text = f"discordId={member.id}" + request_text = f"discord_user_id={member.id}" request_url = base_url + request_text async with session.get(request_url, headers=headers) as resp: if resp.status != 200: players.append(None) continue - player_data = await resp.json() - if 'mmr' not in player_data.keys(): + result = await resp.json() + player_data = result["results"][0] + if 'current_mmr' not in player_data.keys(): players.append( - Player(member, player_data['name'], None)) + Player(member, player_data['player_name'], None)) continue players.append( - Player(member, player_data['name'], player_data['mmr'])) + Player(member, player_data['player_name'], player_data['current_mmr'])) except: print(f"Fetch for player {members[0]} has failed.", flush=True) return players async def get_mmr_from_discord_id(discord_id): - base_url = "https://www.mk8dx-lounge.com" + '/api/player?' + base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_id=9&' timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession( timeout=timeout, auth=aiohttp.BasicAuth( "username", "password")) as session: - request_text = f"discordId={discord_id}" + request_text = f"discord_user_id={discord_id}" request_url = base_url + request_text async with session.get(request_url, headers=headers) as resp: if resp.status != 200: return "Player does not exist" - player_data = await resp.json() - if 'mmr' not in player_data.keys(): + result = await resp.json() + player_data = result["results"][0] + if 'current_mmr' not in player_data.keys(): return "Player has no mmr" - return player_data['mmr'] + return player_data['current_mmr'] async def get_mmr(config, members): diff --git a/mogi_objects.py b/mogi_objects.py index 7a9c1a8..d63b0fe 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -163,6 +163,7 @@ def __init__(self, players, thread, mogi): self.__setattr__("2v2", []) self.__setattr__("3v3", []) self.__setattr__("4v4", []) + self.__setattr__("6v6", []) def __getitem__(self, key): return getattr(self, key) @@ -177,11 +178,12 @@ async def make_teams(self, format): msg += f"2) 2v2 - {len(self['2v2'])}\n" msg += f"3) 3v3 - {len(self['3v3'])}\n" msg += f"4) 4v4 - {len(self['4v4'])}\n" + msg += f"5) 6v6 - {len(self['6v6'])}\n" msg += f"Winner: {format[1]}\n\n" room_mmr = round(sum([p.mmr for p in self.players]) / 12) room.mmr_average = room_mmr - self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - Tier {get_tier(room_mmr - 500)}** " + self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - {get_tier(room_mmr)}** " msg += self.header_text msg += "\n" @@ -204,11 +206,7 @@ async def make_teams(self, format): msg += team_text self.teams_text += team_text - msg += f"\nTable: `/scoreboard`\n" - - msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n\n" - - msg += "Decide a host amongst yourselves; room open at :00, penalty at :06. Good luck!" + msg += "Decide a host amongst yourselves; room open at :00, penalty at :08. Good luck!" room.teams = teams @@ -227,6 +225,8 @@ async def find_winner(self): max = len(self["3v3"]) if len(self["4v4"]) > max: max = len(self["4v4"]) + if len(self["6v6"]) > max: + max = len(self["6v6"]) winners = [] @@ -238,6 +238,8 @@ async def find_winner(self): winners.append((3, "3v3")) if len(self["4v4"]) == max: winners.append((4, "4v4")) + if len(self["6v6"]) == max: + winners.append((6, "6v6")) winner = random.choice(winners) @@ -258,6 +260,8 @@ async def one_button_callback(self, interaction, button): self["3v3"].remove(interaction.user.id) if interaction.user.id in self["4v4"]: self["4v4"].remove(interaction.user.id) + if interaction.user.id in self["6v6"]: + self["6v6"].remove(interaction.user.id) self["FFA"].append(interaction.user.id) if len(self["FFA"]) == 6: await self.make_teams((1, "FFA")) @@ -279,6 +283,8 @@ async def two_button_callback(self, interaction, button): self["3v3"].remove(interaction.user.id) if interaction.user.id in self["4v4"]: self["4v4"].remove(interaction.user.id) + if interaction.user.id in self["6v6"]: + self["6v6"].remove(interaction.user.id) self["2v2"].append(interaction.user.id) if len(self["2v2"]) == 6: await self.make_teams((2, "2v2")) @@ -300,6 +306,8 @@ async def three_button_callback(self, interaction, button): self["2v2"].remove(interaction.user.id) if interaction.user.id in self["4v4"]: self["4v4"].remove(interaction.user.id) + if interaction.user.id in self["6v6"]: + self["6v6"].remove(interaction.user.id) self["3v3"].append(interaction.user.id) if len(self["3v3"]) == 6: await self.make_teams((3, "3v3")) @@ -321,6 +329,8 @@ async def four_button_callback(self, interaction, button): self["2v2"].remove(interaction.user.id) if interaction.user.id in self["3v3"]: self["3v3"].remove(interaction.user.id) + if interaction.user.id in self["6v6"]: + self["6v6"].remove(interaction.user.id) self["4v4"].append(interaction.user.id) if len(self["4v4"]) == 6: await self.make_teams((4, "4v4")) @@ -330,6 +340,29 @@ async def four_button_callback(self, interaction, button): curr_button.disabled = True await interaction.response.edit_message(view=self) + @discord.ui.button(label="6v6: 0", custom_id="6v6") + async def six_button_callback(self, interaction, button): + if not self.found_winner: + if interaction.user.id in self["6v6"]: + self["6v6"].remove(interaction.user.id) + else: + if interaction.user.id in self["FFA"]: + self["FFA"].remove(interaction.user.id) + if interaction.user.id in self["2v2"]: + self["2v2"].remove(interaction.user.id) + if interaction.user.id in self["3v3"]: + self["3v3"].remove(interaction.user.id) + if interaction.user.id in self["4v4"]: + self["4v4"].remove(interaction.user.id) + self["6v6"].append(interaction.user.id) + if len(self["6v6"]) == 6: + await self.make_teams((6, "6v6")) + for curr_button in self.children: + curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" + if len(self["6v6"]) == 6: + curr_button.disabled = True + await interaction.response.edit_message(view=self) + class JoinView(View): def __init__(self, room, get_mmr): @@ -364,33 +397,17 @@ async def button_callback(self, interaction, button): def get_tier(mmr: int): - if mmr > 14000: - return 'X' - if mmr > 13000: - return 'S' - if mmr > 12000: - return 'A' if mmr > 11000: - return 'AB' - if mmr > 10000: - return 'B' - if mmr > 9000: - return 'BC' - if mmr > 8000: - return 'C' - if mmr > 7000: - return 'CD' - if mmr > 6000: - return 'D' + return 'T7' + if mmr > 9500: + return 'T6' + if mmr > 7500: + return 'T5' + if mmr > 6250: + return 'T4' if mmr > 5000: - return 'DE' - if mmr > 4000: - return 'E' - if mmr > 3000: - return 'EF' - if mmr > 2000: - return 'F' - if mmr > 1000: - return 'FG' + return 'T3' + if mmr > 3500: + return 'T2' else: - return 'G' + return 'T1' diff --git a/redeploy.sh b/redeploy.sh index 9db4c40..4cf9181 100644 --- a/redeploy.sh +++ b/redeploy.sh @@ -1,5 +1,5 @@ #!/bin/bash -docker build --tag mogi-queuebot . -docker stop mogi-queuebot -docker rm mogi-queuebot -docker run -d --name mogi-queuebot --restart unless-stopped mogi-queuebot \ No newline at end of file +docker build --tag mkwii-mogi-queuebot . +docker stop mkwii-mogi-queuebot +docker rm mkwii-mogi-queuebot +docker run -d --name mkwii-mogi-queuebot --restart unless-stopped mkwii-mogi-queuebot \ No newline at end of file From f3241d6591251efbbbbd100f0f895410b057dc82 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 8 Apr 2024 17:52:48 -0700 Subject: [PATCH 002/292] updated config file --- mkwii_sample_config.json | 65 +++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/mkwii_sample_config.json b/mkwii_sample_config.json index 94cf7de..a273c48 100644 --- a/mkwii_sample_config.json +++ b/mkwii_sample_config.json @@ -1,34 +1,37 @@ { - "token": "", - "guild_id": 387347467332485122, - - "admin_roles": {"387347467332485122": ["Administrator", "Lounge Staff"]}, - - "queue_join_channel": 1215174119767932979, + "token": "", + "guild_id": 387347467332485122, + "admin_roles": { + "387347467332485122": [ + "Administrator", + "Lounge Staff" + ] + }, + "queue_join_channel": 1215174119767932979, "queue_sub_channel": 1215174160234315806, - "queue_history_channel": 1215174057453162496, - - "members_for_channels": {"387347467332485122": [ - 235148962103951360, - 155149108183695360, - 815483289515196416, - 892420397570592768, - 775415990238314518, - 774812906830299136, - 745443073202323586, - 774866940950872095]}, - - "queue_messages": true, - "sec_between_queue_msgs": 2, - - "username": "username", - "password": "password", - "url": "https://www.mkwlounge.gg", - - "TIME_ADJUSTMENT": 0, - "QUEUE_OPEN_TIME": 60, - "JOINING_TIME": 55, - "EXTENSION_TIME": 3, - "MOGI_LIFETIME": 180, - "SUB_MESSAGE_LIFETIME_SECONDS": 1200 + "queue_list_channel": 1215174057453162496, + "queue_history_channel": 1225709377747685477, + "members_for_channels": { + "387347467332485122": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095 + ] + }, + "queue_messages": true, + "sec_between_queue_msgs": 2, + "username": "username", + "password": "password", + "url": "https://www.mkwlounge.gg", + "TIME_ADJUSTMENT": 0, + "QUEUE_OPEN_TIME": 60, + "JOINING_TIME": 55, + "EXTENSION_TIME": 3, + "MOGI_LIFETIME": 180, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200 } \ No newline at end of file From c0a1ac55cf5228a396a9dec0db7be260cf2c978d Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 8 Apr 2024 17:53:06 -0700 Subject: [PATCH 003/292] changed method of fetching player mmr --- mmr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mmr.py b/mmr.py index 26828d2..d5bd746 100644 --- a/mmr.py +++ b/mmr.py @@ -6,7 +6,7 @@ async def mk8dx_150cc_mmr(url, members): - base_url = url + '/api/ladderplayer.php?ladder_id=9&' + base_url = url + '/api/ladderplayer.php?ladder_type=rt&' players = [] timeout = aiohttp.ClientTimeout(total=10) try: @@ -35,7 +35,7 @@ async def mk8dx_150cc_mmr(url, members): async def get_mmr_from_discord_id(discord_id): - base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_id=9&' + base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_type=rt&' timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession( timeout=timeout, From 264cfcd8749a8a9df070c2f512bf2a57402b8a9c Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 8 Apr 2024 17:53:29 -0700 Subject: [PATCH 004/292] separated list and history channels --- cogs/SquadQueue.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index bbb4c25..6780939 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -43,6 +43,8 @@ def __init__(self, bot): self.SUB_CHANNEL = None + self.LIST_CHANNEL = None + self.HISTORY_CHANNEL = None self.LOCK = asyncio.Lock() @@ -72,11 +74,14 @@ async def on_ready(self): self.bot.config["queue_join_channel"]) self.SUB_CHANNEL = self.bot.get_channel( self.bot.config["queue_sub_channel"]) + self.LIST_CHANNEL = self.bot.get_channel( + self.bot.config["queue_list_channel"]) self.HISTORY_CHANNEL = self.bot.get_channel( self.bot.config["queue_history_channel"]) print(f"Server - {self.GUILD}", flush=True) print(f"Join Channel - {self.MOGI_CHANNEL}", flush=True) print(f"Sub Channel - {self.SUB_CHANNEL}", flush=True) + print(f"List Channel - {self.LIST_CHANNEL}", flush=True) print(f"History Channel - {self.HISTORY_CHANNEL}", flush=True) print("Ready!", flush=True) @@ -84,10 +89,13 @@ async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) # if not everyone_perms.send_messages: # return - overwrite = channel.overwrites_for(channel.guild.default_role) - overwrite.send_messages = False - await channel.set_permissions(channel.guild.default_role, overwrite=overwrite) - await channel.send("Locked down " + channel.mention) + try: + overwrite = channel.overwrites_for(channel.guild.default_role) + overwrite.send_messages = False + await channel.set_permissions(channel.guild.default_role, overwrite=overwrite) + await channel.send("Locked down " + channel.mention) + except Exception as e: + print(e, flush=True) async def unlockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -266,7 +274,7 @@ async def list_task(self): msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) msg += f" ({sorted_mogi_list[i].players[0].mmr} MMR)\n" if ((i + 1) % 12 == 0): - msg += "\n" + msg += "ㅤ\n" if (len(sorted_mogi_list) % (12/mogi.size) != 0): num_next = int(len(sorted_mogi_list) % (12/mogi.size)) teams_per_room = int(12/mogi.size) @@ -292,12 +300,12 @@ async def list_task(self): old_message = self.list_messages[i] await old_message.edit(content=message) else: - new_message = await self.HISTORY_CHANNEL.send(message) + new_message = await self.LIST_CHANNEL.send(message) self.list_messages.append(new_message) except: await self.delete_list_messages(0) for i, message in enumerate(new_messages): - new_message = await self.HISTORY_CHANNEL.send(message) + new_message = await self.LIST_CHANNEL.send(message) self.list_messages.append(new_message) else: await self.delete_list_messages(0) @@ -307,8 +315,8 @@ async def delete_list_messages(self, new_list_size: int): messages_to_delete = [] while len(self.list_messages) > new_list_size: messages_to_delete.append(self.list_messages.pop()) - if self.HISTORY_CHANNEL and len(messages_to_delete) > 0: - await self.HISTORY_CHANNEL.delete_messages(messages_to_delete) + if self.LIST_CHANNEL and len(messages_to_delete) > 0: + await self.LIST_CHANNEL.delete_messages(messages_to_delete) except Exception as e: print(e, flush=True) From 142b2f50b424ced5a9754de5c1a5e02a534c10ee Mon Sep 17 00:00:00 2001 From: parappurame Date: Thu, 11 Apr 2024 12:34:03 -0700 Subject: [PATCH 005/292] Changed Sub Command Edited the message associated with the sub command --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6780939..9ba6000 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -242,12 +242,12 @@ async def sub(self, interaction: discord.Interaction): if not is_room_thread: await interaction.response.send_message(f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) return - msg = "<@&682445864400453739> - " + # msg = "<@&682445864400453739> - " if room.room_num == 1: msg += f"Room {room.room_num} is looking for a sub with mmr >{room.mmr_low - 500}\n" else: - low = 0 if room.mmr_low < 500 else room.mmr_low - 500 - msg += f"Room {room.room_num} is looking for a sub with range {low}-{room.mmr_high + 500}\n" + # low = 0 if room.mmr_low < 500 else room.mmr_low - 500 + msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 500}-{room.mmr_high + 500}\n" message_delete_date = datetime.now( timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" From 8afe115f9af332ba7e7a979fe9530143dfce356b Mon Sep 17 00:00:00 2001 From: parappurame Date: Thu, 11 Apr 2024 12:35:04 -0700 Subject: [PATCH 006/292] Removed @here ping from new queues --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9ba6000..5978382 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -677,7 +677,7 @@ async def scheduler_mogi_start(self): mogi.gathering = True self.ongoing_events[mogi.mogi_channel] = mogi await self.unlockdown(mogi.mogi_channel) - await mogi.mogi_channel.send(f"A queue is gathering for the mogi {discord.utils.format_dt(mogi.start_time, style='R')} - @here Type `/c` to join, and `/d` to drop.") + await mogi.mogi_channel.send(f"A queue is gathering for the mogi {discord.utils.format_dt(mogi.start_time, style='R')} - Type `/c` to join, and `/d` to drop.") for ind in reversed(to_remove): del guild[ind] From 0810ab1bc5f28ba931c8e1206d96376c9ca3910f Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Thu, 25 Apr 2024 18:13:56 -0700 Subject: [PATCH 007/292] Essential Changes for MKW Lounge Changed Time for Queues and set MMR threshold for rooms. Other various changes include setting new variables and having existing variables grab data from config instead of being hard-coded. --- cogs/SquadQueue.py | 100 +++++++++++-------- mkwii_sample_config.json => prod_config.json | 11 +- sample_config.json | 31 ------ test_config.json | 38 +++++++ 4 files changed, 100 insertions(+), 80 deletions(-) rename mkwii_sample_config.json => prod_config.json (82%) delete mode 100644 sample_config.json create mode 100644 test_config.json diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5978382..30e7d89 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -25,6 +25,8 @@ def __init__(self, bot): self.sq_times = [] + self.prev_start_time = None + self._que_scheduler = self.que_scheduler.start() self._scheduler_task = self.sqscheduler.start() self._msgqueue_task = self.send_queued_messages.start() @@ -55,6 +57,8 @@ def __init__(self, bot): self.SUB_MESSAGE_LIFETIME_SECONDS = bot.config["SUB_MESSAGE_LIFETIME_SECONDS"] + self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] + # number of minutes before scheduled time that queue should open self.QUEUE_OPEN_TIME = timedelta(minutes=bot.config["QUEUE_OPEN_TIME"]) @@ -78,6 +82,14 @@ async def on_ready(self): self.bot.config["queue_list_channel"]) self.HISTORY_CHANNEL = self.bot.get_channel( self.bot.config["queue_history_channel"]) + try: + await self.LIST_CHANNEL.purge() + except: + print("Purging List channel failed", flush=True) + try: + await self.SUB_CHANNEL.purge() + except: + print("Purging Sub channel failed", flush=True) print(f"Server - {self.GUILD}", flush=True) print(f"Join Channel - {self.MOGI_CHANNEL}", flush=True) print(f"Sub Channel - {self.SUB_CHANNEL}", flush=True) @@ -242,7 +254,7 @@ async def sub(self, interaction: discord.Interaction): if not is_room_thread: await interaction.response.send_message(f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) return - # msg = "<@&682445864400453739> - " + msg = "" if room.room_num == 1: msg += f"Room {room.room_num} is looking for a sub with mmr >{room.mmr_low - 500}\n" else: @@ -373,10 +385,6 @@ async def annul_current_mogi(self, interaction: discord.Interaction): """The mogi currently gathering will be deleted. The queue resumes at the next hour. Staff use only.""" self.scheduled_events = {} self.ongoing_events = {} - curr_time = datetime.now(timezone.utc) - truncated_time = curr_time.replace( - minute=0, second=0, microsecond=0) - self.QUEUE_TIME_BLOCKER = truncated_time + timedelta(hours=1) await self.lockdown(self.MOGI_CHANNEL) await interaction.response.send_message("The current Mogi has been canceled, the queue will resume at the next hour.") @@ -394,6 +402,7 @@ async def resume_mogi_scheduling(self, interaction: discord.Interaction): """Mogis will begin to be scheduled again. Staff use only.""" curr_time = datetime.now(timezone.utc) self.QUEUE_TIME_BLOCKER = curr_time + self.prev_start_time = None await interaction.response.send_message("Mogis will resume scheduling.") @app_commands.command(name="reset_bot") @@ -512,7 +521,10 @@ async def check_room_channels(self, mogi): if num_created_rooms >= num_rooms: return for i in range(num_created_rooms, num_rooms): - room_name = f"{mogi.start_time.month}/{mogi.start_time.day}, {mogi.start_time.hour}:00:00 - Room {i+1}" + minute = mogi.start_time.minute + if len(str(minute)) == 1: + minute = '0' + str(minute) + room_name = f"{mogi.start_time.month}/{mogi.start_time.day}, {mogi.start_time.hour}:{minute}:00 - Room {i+1}" try: room_channel = await mogi.mogi_channel.create_thread(name=room_name, auto_archive_duration=60, @@ -570,8 +582,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F for j in range(teams_per_room): msg += f"`{j+1}.` " team = sorted_list[start_index+j] - player_list.append( - sorted_list[start_index+j].get_first_player()) + player_list.append(sorted_list[start_index+j].get_first_player()) msg += ", ".join([p.lounge_name for p in team.players]) msg += f" ({int(team.avg_mmr)} MMR)\n" mentions += " ".join([p.member.mention for p in team.players]) @@ -579,24 +590,27 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F room_msg = msg mentions += " ".join([m.mention for m in extra_members if m is not None]) room_msg += "\nVote for format FFA, 2v2, 3v3, or 4v4.\n" - # room_msg += "\nIf you need staff's assistance, use the `!staff` command in this channel.\n" + room_msg += "\nIf you need staff's assistance, use the `!staff` command in this channel.\n" room_msg += mentions - try: - curr_room = rooms[i] - room_channel = curr_room.thread - curr_room.teams = sorted_list[start_index:start_index+teams_per_room] - await room_channel.send(room_msg) - view = VoteView(player_list, room_channel, mogi) - curr_room.view = view - curr_room.mmr_low = player_list[11].mmr - curr_room.mmr_high = player_list[0].mmr - await room_channel.send(view=view) - except Exception as e: - print(e, flush=True) - err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" - err_msg += mentions - msg += err_msg - room_channel = None + curr_room = rooms[i] + room_channel = curr_room.thread + curr_room.teams = sorted_list[start_index:start_index+teams_per_room] + curr_room.mmr_low = player_list[11].mmr + curr_room.mmr_high = player_list[0].mmr + if curr_room.mmr_high - curr_room.mmr_low > self.room_mmr_threshold: + msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold}MMR, this room has been cancelled." + else: + try: + await room_channel.send(room_msg) + view = VoteView(player_list, room_channel, mogi) + curr_room.view = view + await room_channel.send(view=view) + except Exception as e: + print(e, flush=True) + err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" + err_msg += mentions + msg += err_msg + room_channel = None try: await mogi.mogi_channel.send(msg) except Exception as e: @@ -709,35 +723,36 @@ async def schedule_que_event(self): if self.GUILD is not None: curr_time = datetime.now(timezone.utc) - truncated_time = curr_time.replace( - minute=0, second=0, microsecond=0) - next_hour = truncated_time + timedelta(hours=1) - if len(self.sq_times) > 0 and next_hour == self.sq_times[0]: + start_time = curr_time + timedelta(minutes=65) + start_time = start_time.replace(minute=0, second=0, microsecond=0) + start_time -= self.QUEUE_OPEN_TIME + if self.prev_start_time: + start_time = self.prev_start_time + next_mogi_start = start_time + self.QUEUE_OPEN_TIME + if len(self.sq_times) > 0 and next_mogi_start == self.sq_times[0]: self.sq_times.pop(0) - self.QUEUE_TIME_BLOCKER = next_hour + self.QUEUE_TIME_BLOCKER = next_mogi_start await self.MOGI_CHANNEL.send("Squad Queue is currently going on at this hour! The queue will remain closed.") if curr_time < self.QUEUE_TIME_BLOCKER: # print(f"Mogi had been blocked from starting before the time limit {self.QUEUE_TIME_BLOCKER}", flush=True) return - if datetime.now().minute >= self.bot.config["JOINING_TIME"]: - # print("Hourly Que is too late, starting Que at next hour", flush=True) - return for mogi in self.ongoing_events.values(): - if mogi.start_time == next_hour: + if mogi.start_time == next_mogi_start: return - event_start_time = next_hour.astimezone() - self.QUEUE_OPEN_TIME - if event_start_time < discord.utils.utcnow(): - event_start_time = discord.utils.utcnow() + timedelta(minutes=1) mogi = Mogi(1, 1, self.MOGI_CHANNEL, is_automated=True, - start_time=next_hour) + start_time=next_mogi_start) if self.GUILD not in self.scheduled_events.keys(): self.scheduled_events[self.GUILD] = [] self.scheduled_events[self.GUILD].append(mogi) - print(f"Started Queue for {next_hour}", flush=True) + self.QUEUE_TIME_BLOCKER = next_mogi_start + + self.prev_start_time = next_mogi_start + + print(f"Started Queue for {next_mogi_start}", flush=True) @tasks.loop(minutes=1) async def delete_old_mogis(self): @@ -825,15 +840,12 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem @commands.command(name="debug_start_rooms") @commands.is_owner() async def debug_start_rooms(self, ctx): - truncated_time = datetime.now(timezone.utc).replace( - minute=0, second=0, microsecond=0) - next_hour = truncated_time + timedelta(hours=1) for mogi in self.ongoing_events.values(): - if mogi.start_time == next_hour: + if mogi.start_time == self.prev_start_time: await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) return for mogi in self.old_events.values(): - if mogi.start_time == next_hour: + if mogi.start_time == self.prev_start_time: await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) return diff --git a/mkwii_sample_config.json b/prod_config.json similarity index 82% rename from mkwii_sample_config.json rename to prod_config.json index a273c48..52f34fc 100644 --- a/mkwii_sample_config.json +++ b/prod_config.json @@ -2,7 +2,7 @@ "token": "", "guild_id": 387347467332485122, "admin_roles": { - "387347467332485122": [ + "1228027010836594698": [ "Administrator", "Lounge Staff" ] @@ -12,7 +12,7 @@ "queue_list_channel": 1215174057453162496, "queue_history_channel": 1225709377747685477, "members_for_channels": { - "387347467332485122": [ + "1228027010836594698": [ 235148962103951360, 155149108183695360, 815483289515196416, @@ -29,9 +29,10 @@ "password": "password", "url": "https://www.mkwlounge.gg", "TIME_ADJUSTMENT": 0, - "QUEUE_OPEN_TIME": 60, - "JOINING_TIME": 55, + "QUEUE_OPEN_TIME": 80, + "JOINING_TIME": 72, "EXTENSION_TIME": 3, "MOGI_LIFETIME": 180, - "SUB_MESSAGE_LIFETIME_SECONDS": 1200 + "SUB_MESSAGE_LIFETIME_SECONDS": 1200, + "ROOM_MMR_THRESHOLD": 8000 } \ No newline at end of file diff --git a/sample_config.json b/sample_config.json deleted file mode 100644 index 6f8aaad..0000000 --- a/sample_config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "token": "", - "application_id": 1183741625986465803, - "guild_id": 1139900310081507398, - - "admin_roles": {"1139900310081507398": ["Administrator", "Lounge Staff"]}, - "staff_roles": {"1139900310081507398": [910528103225389107]}, - - "queue_join_channel": 1139900310861660163, - "queue_sub_channel": 1186097268240162846, - "queue_history_channel": 1194143778156576880, - - "members_for_channels": {"1139900310081507398": [934740707934081035, 743532508343304242, 735910635082219570]}, - - "channel_category_position": 5, - "channels_per_category": 25, - - "queue_messages": true, - "sec_between_queue_msgs": 2, - - "username": "username", - "password": "password", - "url": "https://www.mk8dx-lounge.com", - - "TIME_ADJUSTMENT": 0, - "QUEUE_OPEN_TIME": 60, - "JOINING_TIME": 55, - "EXTENSION_TIME": 3, - "MOGI_LIFETIME": 180, - "SUB_MESSAGE_LIFETIME_SECONDS": 1200 -} \ No newline at end of file diff --git a/test_config.json b/test_config.json new file mode 100644 index 0000000..39bf0db --- /dev/null +++ b/test_config.json @@ -0,0 +1,38 @@ +{ + "token": "", + "guild_id": 1228027010836594698, + "admin_roles": { + "1228027010836594698": [ + "Administrator", + "Lounge Staff" + ] + }, + "queue_join_channel": 1228046535321849907, + "queue_sub_channel": 1228046571447386173, + "queue_list_channel": 1228046584181293066, + "queue_history_channel": 1228046598777344161, + "members_for_channels": { + "1228027010836594698": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095 + ] + }, + "queue_messages": true, + "sec_between_queue_msgs": 2, + "username": "username", + "password": "password", + "url": "https://www.mkwlounge.gg", + "TIME_ADJUSTMENT": 0, + "QUEUE_OPEN_TIME": 80, + "JOINING_TIME": 72, + "EXTENSION_TIME": 3, + "MOGI_LIFETIME": 180, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200, + "ROOM_MMR_THRESHOLD": 8000 +} \ No newline at end of file From 12948c9bd801727bec5aec20e7bc0240bec768d8 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Tue, 7 May 2024 20:37:58 -0700 Subject: [PATCH 008/292] Thread Messages Removed the message telling players to mention staff for an inconvenience. Also changed the pen message to accurately reflect the pen timing. --- cogs/SquadQueue.py | 1 - mogi_objects.py | 14 +++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 30e7d89..02cc6d4 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -590,7 +590,6 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F room_msg = msg mentions += " ".join([m.mention for m in extra_members if m is not None]) room_msg += "\nVote for format FFA, 2v2, 3v3, or 4v4.\n" - room_msg += "\nIf you need staff's assistance, use the `!staff` command in this channel.\n" room_msg += mentions curr_room = rooms[i] room_channel = curr_room.thread diff --git a/mogi_objects.py b/mogi_objects.py index d63b0fe..c3fd290 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -1,5 +1,7 @@ import random import discord +from datetime import datetime, timezone, timedelta +import time from discord.ui import View @@ -206,7 +208,17 @@ async def make_teams(self, format): msg += team_text self.teams_text += team_text - msg += "Decide a host amongst yourselves; room open at :00, penalty at :08. Good luck!" + minutes = self.mogi.start_time.minute + pen = minutes + 10 + + if pen > 60: + pen -= 60 + + if pen < 10: + pen = '0' + str(pen) + if minutes < 10: + minutes = '0' + str(minutes) + msg += "Decide a host amongst yourselves; room open at :{}, penalty at :{}. Good luck!".format(minutes, pen) room.teams = teams From 144dea35ea81d0ab717f3787a27e3a80dc2d5189 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Wed, 15 May 2024 22:30:15 -0700 Subject: [PATCH 009/292] Lounge Test 1 Fixes Added new commands for staff to edit some config values. Changed the sub message. Changed the queue joining message to clarify joining/closing time. --- cogs/SquadQueue.py | 35 +++++++++++++++++++++++++++++++---- mogi_objects.py | 2 +- prod_config.json | 8 ++++---- test_config.json | 4 ++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 02cc6d4..173b200 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -205,7 +205,7 @@ async def can(self, interaction: discord.Interaction): squad = Team(players) mogi.teams.append(squad) - msg += f"{players[0].lounge_name} joined queue for mogi {discord.utils.format_dt(mogi.start_time, style='R')}, `[{mogi.count_registered()} players]`" + msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time - timedelta(minutes=self.QUEUE_OPEN_TIME - self.JOINING_TIME))}, `[{mogi.count_registered()} players]`" await interaction.followup.send(msg) await self.check_room_channels(mogi) @@ -254,7 +254,7 @@ async def sub(self, interaction: discord.Interaction): if not is_room_thread: await interaction.response.send_message(f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) return - msg = "" + msg = "<@&1167985222533533817> - " if room.room_num == 1: msg += f"Room {room.room_num} is looking for a sub with mmr >{room.mmr_low - 500}\n" else: @@ -404,6 +404,33 @@ async def resume_mogi_scheduling(self, interaction: discord.Interaction): self.QUEUE_TIME_BLOCKER = curr_time self.prev_start_time = None await interaction.response.send_message("Mogis will resume scheduling.") + + @app_commands.command(name="change_event_time") + @app_commands.guild_only() + async def change_event_time(self, interaction: discord.Interaction, event_time: int): + """Change the amount of time for each event in the queue.""" + if event_time > 15: + self.QUEUE_OPEN_TIME = timedelta(minutes=event_time) + self.JOINING_TIME = timedelta(minutes=event_time - 5) + await interaction.response.send_message(f"The amount of time for each mogi has been changed to {event_time} minutes.") + else: + await interaction.response.send_message("Please enter a number of minutes greater than 15.") + + @app_commands.command(name="change_mmr_threshold") + @app_commands.guild_only() + async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_threshold: int): + """Change the maximum MMR gap allowed for a room.""" + self.room_mmr_threshold = mmr_threshold + await interaction.response.send_message(f"MMR Threshold for Queue Rooms has been modified to {mmr_threshold} MMR.") + + @app_commands.command(name="peek_bot_config") + @app_commands.guild_only() + async def peek_bot_config(self, interaction: discord.Interaction): + """View the configured values for the Queue System.""" + msg = "" + msg += f"Time for Each Event: {self.QUEUE_OPEN_TIME}, \n" + msg += f"Room MMR Threshold: {self.room_mmr_threshold}" + await interaction.response.send_message(msg) @app_commands.command(name="reset_bot") @app_commands.guild_only() @@ -589,7 +616,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F mentions += " " room_msg = msg mentions += " ".join([m.mention for m in extra_members if m is not None]) - room_msg += "\nVote for format FFA, 2v2, 3v3, or 4v4.\n" + room_msg += "\nVote for format FFA, 2v2, 3v3, 4v4, or 6v6.\n" room_msg += mentions curr_room = rooms[i] room_channel = curr_room.thread @@ -597,7 +624,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F curr_room.mmr_low = player_list[11].mmr curr_room.mmr_high = player_list[0].mmr if curr_room.mmr_high - curr_room.mmr_low > self.room_mmr_threshold: - msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold}MMR, this room has been cancelled." + msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: try: await room_channel.send(room_msg) diff --git a/mogi_objects.py b/mogi_objects.py index c3fd290..834f596 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -209,7 +209,7 @@ async def make_teams(self, format): self.teams_text += team_text minutes = self.mogi.start_time.minute - pen = minutes + 10 + pen = minutes + 8 if pen > 60: pen -= 60 diff --git a/prod_config.json b/prod_config.json index 52f34fc..bfec4bd 100644 --- a/prod_config.json +++ b/prod_config.json @@ -2,7 +2,7 @@ "token": "", "guild_id": 387347467332485122, "admin_roles": { - "1228027010836594698": [ + "387347467332485122": [ "Administrator", "Lounge Staff" ] @@ -12,7 +12,7 @@ "queue_list_channel": 1215174057453162496, "queue_history_channel": 1225709377747685477, "members_for_channels": { - "1228027010836594698": [ + "387347467332485122": [ 235148962103951360, 155149108183695360, 815483289515196416, @@ -29,8 +29,8 @@ "password": "password", "url": "https://www.mkwlounge.gg", "TIME_ADJUSTMENT": 0, - "QUEUE_OPEN_TIME": 80, - "JOINING_TIME": 72, + "QUEUE_OPEN_TIME": 72, + "JOINING_TIME": 67, "EXTENSION_TIME": 3, "MOGI_LIFETIME": 180, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, diff --git a/test_config.json b/test_config.json index 39bf0db..21431b8 100644 --- a/test_config.json +++ b/test_config.json @@ -29,8 +29,8 @@ "password": "password", "url": "https://www.mkwlounge.gg", "TIME_ADJUSTMENT": 0, - "QUEUE_OPEN_TIME": 80, - "JOINING_TIME": 72, + "QUEUE_OPEN_TIME": 72, + "JOINING_TIME": 67, "EXTENSION_TIME": 3, "MOGI_LIFETIME": 180, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, From 9b598a168548deb68f4d6dff3a992e91df87a713 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Wed, 15 May 2024 23:16:20 -0700 Subject: [PATCH 010/292] Add roles to queue channels Adding RT Updater and Reporter to the Queue Channels --- prod_config.json | 4 +++- test_config.json | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/prod_config.json b/prod_config.json index bfec4bd..703203c 100644 --- a/prod_config.json +++ b/prod_config.json @@ -20,7 +20,9 @@ 775415990238314518, 774812906830299136, 745443073202323586, - 774866940950872095 + 774866940950872095, + 393600567781621761, + 389252697284542465 ] }, "queue_messages": true, diff --git a/test_config.json b/test_config.json index 21431b8..9da0032 100644 --- a/test_config.json +++ b/test_config.json @@ -20,7 +20,9 @@ 775415990238314518, 774812906830299136, 745443073202323586, - 774866940950872095 + 774866940950872095, + 393600567781621761, + 389252697284542465 ] }, "queue_messages": true, From f55f3c18d4442519b7af4317c30706afb587363f Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Wed, 15 May 2024 23:35:21 -0700 Subject: [PATCH 011/292] Added Table Bot to Queue Rooms --- prod_config.json | 1 + test_config.json | 1 + 2 files changed, 2 insertions(+) diff --git a/prod_config.json b/prod_config.json index 703203c..e8dcb96 100644 --- a/prod_config.json +++ b/prod_config.json @@ -21,6 +21,7 @@ 774812906830299136, 745443073202323586, 774866940950872095, + 1019051989734273054, 393600567781621761, 389252697284542465 ] diff --git a/test_config.json b/test_config.json index 9da0032..5ad0ab4 100644 --- a/test_config.json +++ b/test_config.json @@ -21,6 +21,7 @@ 774812906830299136, 745443073202323586, 774866940950872095, + 1019051989734273054, 393600567781621761, 389252697284542465 ] From af572b73d40002240b941f662c97cd58b6c51c93 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Thu, 16 May 2024 16:16:58 -0700 Subject: [PATCH 012/292] Fixed "c" command --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 173b200..6c4098c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -205,7 +205,7 @@ async def can(self, interaction: discord.Interaction): squad = Team(players) mogi.teams.append(squad) - msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time - timedelta(minutes=self.QUEUE_OPEN_TIME - self.JOINING_TIME))}, `[{mogi.count_registered()} players]`" + msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time - (self.QUEUE_OPEN_TIME - self.JOINING_TIME))}, `[{mogi.count_registered()} players]`" await interaction.followup.send(msg) await self.check_room_channels(mogi) From cdc2231a9a973ca729edc84aef59382528579edd Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Fri, 14 Jun 2024 19:00:05 -0700 Subject: [PATCH 013/292] Added Changes since previous test Changed the display of the queue list, added restrictions and cooldown to the sub command, changed the channel that threads are made in, and others. --- cogs/SquadQueue.py | 153 ++++++++++++++++++++++++++++++++++++--------- mogi_objects.py | 41 ++++++------ prod_config.json | 7 ++- test_config.json | 10 ++- 4 files changed, 157 insertions(+), 54 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6c4098c..9a6e601 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1,3 +1,4 @@ +import aiohttp import discord from discord.ext import commands, tasks from discord import app_commands @@ -8,9 +9,13 @@ from mmr import mk8dx_150cc_mmr, get_mmr_from_discord_id from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio +from collections import defaultdict + +headers = {'Content-type': 'application/json'} # Scheduled_Event = collections.namedtuple('Scheduled_Event', 'size time started mogi_channel') +cooldowns = defaultdict(int) class SquadQueue(commands.Cog): def __init__(self, bot): @@ -49,6 +54,8 @@ def __init__(self, bot): self.HISTORY_CHANNEL = None + self.GENERAL_CHANNEL = None + self.LOCK = asyncio.Lock() self.URL = bot.config["url"] @@ -59,6 +66,8 @@ def __init__(self, bot): self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] + self.TIER_INFO = [] + # number of minutes before scheduled time that queue should open self.QUEUE_OPEN_TIME = timedelta(minutes=bot.config["QUEUE_OPEN_TIME"]) @@ -82,6 +91,9 @@ async def on_ready(self): self.bot.config["queue_list_channel"]) self.HISTORY_CHANNEL = self.bot.get_channel( self.bot.config["queue_history_channel"]) + self.GENERAL_CHANNEL = self.bot.get_channel( + self.bot.config["queue_general_channel"]) + await self.get_ladder_info() try: await self.LIST_CHANNEL.purge() except: @@ -95,7 +107,9 @@ async def on_ready(self): print(f"Sub Channel - {self.SUB_CHANNEL}", flush=True) print(f"List Channel - {self.LIST_CHANNEL}", flush=True) print(f"History Channel - {self.HISTORY_CHANNEL}", flush=True) + print(f"General Channel - {self.GENERAL_CHANNEL}", flush=True) print("Ready!", flush=True) + async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -182,24 +196,23 @@ async def can(self, interaction: discord.Interaction): player_team = mogi.check_player(member) - if player_team is not None: - await interaction.followup.send(f"{interaction.user.mention} is already signed up.") - return - - players = await mk8dx_150cc_mmr(self.URL, [member]) - - if len(players) == 0 or players[0] is None: - msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " - msg += "Please try again. If the problem continues then contact a staff member for help." - await interaction.followup.send(msg) - return - + players = [] msg = "" - if players[0].mmr is None: - starting_player_mmr = 1500 - players[0].mmr = starting_player_mmr + placement_role_id = 723753340063842345 + if member.get_role(placement_role_id): + starting_player_mmr = 750 + player = Player(member, member.display_name, starting_player_mmr) + players.append(player) msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" + else: + players = await mk8dx_150cc_mmr(self.URL, [member]) + + if len(players) == 0 or players[0] is None: + msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " + msg += "Please try again. If the problem continues then contact a staff member for help." + await interaction.followup.send(msg) + return players[0].confirmed = True squad = Team(players) @@ -237,38 +250,56 @@ async def drop(self, interaction: discord.Interaction): @app_commands.command(name="sub") @app_commands.guild_only() + # @commands.cooldown(rate=1, per=120, type=commands.BucketType.user) async def sub(self, interaction: discord.Interaction): """Sends out a request for a sub in the sub channel. Only works in thread channels for SQ rooms.""" + current_time = time.time() + lastCommandTime = cooldowns.get(interaction.user.id) + print(lastCommandTime, flush=True) + if lastCommandTime == None: + lastCommandTime = 0 + + if (current_time - lastCommandTime) < 120: #Cooldown timer in seconds + await interaction.response.send_message(f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", ephemeral=True) + return + is_room_thread = False room = None + bottom_room_num = 1 for mogi in self.ongoing_events.values(): if mogi.is_room_thread(interaction.channel_id): room = mogi.get_room_from_thread(interaction.channel_id) + bottom_room_num = len(mogi.rooms) is_room_thread = True break for mogi in self.old_events.values(): if mogi.is_room_thread(interaction.channel.id): room = mogi.get_room_from_thread(interaction.channel.id) + bottom_room_num = len(mogi.rooms) is_room_thread = True break if not is_room_thread: await interaction.response.send_message(f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) return msg = "<@&1167985222533533817> - " - if room.room_num == 1: - msg += f"Room {room.room_num} is looking for a sub with mmr >{room.mmr_low - 500}\n" + if bottom_room_num == 1: + msg += f"Room 1 is looking for a sub with any mmr\n" + elif room.room_num == 1: + msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - 500}\n" + elif room.room_num == bottom_room_num: + msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 500}\n" else: - # low = 0 if room.mmr_low < 500 else room.mmr_low - 500 msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 500}-{room.mmr_high + 500}\n" message_delete_date = datetime.now( timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = JoinView(room, get_mmr_from_discord_id) + view = JoinView(room, get_mmr_from_discord_id, bottom_room_num) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) + cooldowns[interaction.user.id] = current_time #Updates cooldown await interaction.response.send_message("Sent out request for sub.") - @tasks.loop(minutes=1) + @tasks.loop(seconds=30) async def list_task(self): """Continually display the list of confirmed players for a mogi in the history channel""" if len(self.ongoing_events) > 0: @@ -279,19 +310,30 @@ async def list_task(self): mogi_list = mogi.confirmed_list() + # Remove late players from the list to display separately + full_list_length = len(mogi_list) + num_of_rooms = full_list_length // 12 + num_confirmed_players = num_of_rooms * 12 + num_late_players = full_list_length - num_confirmed_players + late_players = [] + for i in range(num_confirmed_players, full_list_length): + player = mogi_list.pop() + late_players.append(player) + sorted_mogi_list = sorted(mogi_list, reverse=True) - msg = "Current Mogi List:\n" + msg = f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" + msg += "**Current Mogi List:**\n" for i in range(len(sorted_mogi_list)): msg += f"{i+1}) " msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) msg += f" ({sorted_mogi_list[i].players[0].mmr} MMR)\n" if ((i + 1) % 12 == 0): msg += "ㅤ\n" - if (len(sorted_mogi_list) % (12/mogi.size) != 0): - num_next = int(len(sorted_mogi_list) % (12/mogi.size)) - teams_per_room = int(12/mogi.size) - num_rooms = int(len(sorted_mogi_list) / (12/mogi.size))+1 - msg += f"[{num_next}/{teams_per_room}] players for {num_rooms} room(s)" + msg += "**Late Players:**\n" + for i in range(len(late_players)): + msg += f"{i+1}) " + msg += ", ".join([p.lounge_name for p in late_players[i].players]) + msg += f" ({late_players[i].players[0].mmr} MMR)\n" message = msg.split("\n") new_messages = [] @@ -301,7 +343,7 @@ async def list_task(self): new_messages.append(bulk_msg) bulk_msg = "" bulk_msg += message[i] + "\n" - if len(bulk_msg) > 0: + if len(bulk_msg) > 0 and bulk_msg != "\n": new_messages.append(bulk_msg) await self.delete_list_messages(len(new_messages)) @@ -494,6 +536,50 @@ async def clear_sq_times(self, interaction: discord.Interaction): self.sq_times = [] await interaction.response.send_message("Cleared list of Squad Queue Times.") + + @app_commands.command(name="update_tier_info") + @app_commands.guild_only() + async def update_tier_info(self, interaction: discord.Interaction): + """Updates the info on each tier""" + #await self.get_ladder_info() + msg = await self.get_ladder_info() + await interaction.response.send_message(msg) + + async def get_ladder_info(self): + timeout = aiohttp.ClientTimeout(total=10) + url = "https://mkwlounge.gg/api/ladderclass.php?ladder_type=rt" + msg = "" + try: + async with aiohttp.ClientSession( + timeout = timeout, + auth=aiohttp.BasicAuth( + "username", "password")) as session: + async with session.get(url, headers=headers) as resp: + if resp.status != 200: + raise Exception( + "Fetch for tier info has failed, bad status code") + result = await resp.json() + if result['status'] != "success": + raise Exception( + "Fetch for tier info has failed, Status: Failure") + self.TIER_INFO = result["results"] + msg += "Fetch for Tier Info Successful.\n" + for tier in self.TIER_INFO: + boundary = "" + if tier["minimum_mmr"]: + boundary += f"{tier['minimum_mmr']}-" + else: + boundary += "<" + if tier["maximum_mmr"]: + boundary += f"{tier['maximum_mmr']}" + else: + boundary = f">{tier['minimum_mmr']}" + msg += f"T{tier['ladder_order']} Boundary: {boundary}\n" + print(msg, flush=True) + return msg + except Exception as e: + print(e, flush=True) + # check if user has roles defined in config.json async def has_roles(self, member: discord.Member, guild_id: int, config): @@ -553,7 +639,7 @@ async def check_room_channels(self, mogi): minute = '0' + str(minute) room_name = f"{mogi.start_time.month}/{mogi.start_time.day}, {mogi.start_time.hour}:{minute}:00 - Room {i+1}" try: - room_channel = await mogi.mogi_channel.create_thread(name=room_name, + room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, invitable=False) except Exception as e: @@ -599,6 +685,11 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F mogi.mogi_channel.guild.id)] for m in extra_members_ids: extra_members.append(mogi.mogi_channel.guild.get_member(m)) + if str(mogi.mogi_channel.guild.id) in self.bot.config["roles_for_channels"].keys(): + extra_roles_ids = self.bot.config["roles_for_channels"][str( + mogi.mogi_channel.guild.id)] + for r in extra_roles_ids: + extra_members.append(mogi.mogi_channel.guild.get_role(r)) rooms = mogi.rooms for i in range(num_rooms): @@ -628,7 +719,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F else: try: await room_channel.send(room_msg) - view = VoteView(player_list, room_channel, mogi) + view = VoteView(player_list, room_channel, mogi, self.TIER_INFO) curr_room.view = view await room_channel.send(view=view) except Exception as e: @@ -878,3 +969,7 @@ async def debug_start_rooms(self, ctx): async def setup(bot): await bot.add_cog(SquadQueue(bot)) + + + + diff --git a/mogi_objects.py b/mogi_objects.py index 834f596..b2e792c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -73,6 +73,8 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.mmr_low = None self.view = None self.finished = False + def get_player_list(self): + return [player.member.id for team in self.teams for player in team.players] class Team: @@ -153,7 +155,7 @@ def __init__(self, member, lounge_name, mmr): class VoteView(View): - def __init__(self, players, thread, mogi): + def __init__(self, players, thread, mogi, tier_info): super().__init__() self.players = players self.thread = thread @@ -161,6 +163,7 @@ def __init__(self, players, thread, mogi): self.header_text = "" self.teams_text = "" self.found_winner = False + self.tier_info = tier_info self.__setattr__("FFA", []) self.__setattr__("2v2", []) self.__setattr__("3v3", []) @@ -185,7 +188,7 @@ async def make_teams(self, format): room_mmr = round(sum([p.mmr for p in self.players]) / 12) room.mmr_average = room_mmr - self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - {get_tier(room_mmr)}** " + self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " msg += self.header_text msg += "\n" @@ -377,7 +380,7 @@ async def six_button_callback(self, interaction, button): class JoinView(View): - def __init__(self, room, get_mmr): + def __init__(self, room: Room, get_mmr, bottom_room_num): super().__init__(timeout=1200) self.room = room self.get_mmr = get_mmr @@ -385,10 +388,15 @@ def __init__(self, room, get_mmr): @discord.ui.button(label="Join Room") async def button_callback(self, interaction, button): await interaction.response.defer() - muted_role_id = 600495108999086090 - if interaction.user.get_role(muted_role_id): + muted_role_id = 434887701662007296 + restricted_role_id = 797208908153618452 + if interaction.user.get_role(muted_role_id) or interaction.user.get_role(restricted_role_id): await interaction.followup.send( - "Players with the muted role cannot use the sub button.", ephemeral=True) + "Players with the muted or restricted role cannot use the sub button.", ephemeral=True) + return + if interaction.user.id in self.room.get_player_list(): + await interaction.followup.send( + "You are already in this room.", ephemeral=True) return try: user_mmr = await self.get_mmr(interaction.user.id) @@ -398,6 +406,8 @@ async def button_callback(self, interaction, button): return if self.room.room_num == 1: self.room.mmr_high = 999999 + if self.room.room_num == bottom_room_num: + self.room.mmr_low = -999999 if isinstance(user_mmr, int) and user_mmr < self.room.mmr_high + 500 and user_mmr > self.room.mmr_low - 500: button.disabled = True await interaction.followup.edit_message(interaction.message.id, view=self) @@ -408,18 +418,7 @@ async def button_callback(self, interaction, button): "You do not meet room requirements", ephemeral=True) -def get_tier(mmr: int): - if mmr > 11000: - return 'T7' - if mmr > 9500: - return 'T6' - if mmr > 7500: - return 'T5' - if mmr > 6250: - return 'T4' - if mmr > 5000: - return 'T3' - if mmr > 3500: - return 'T2' - else: - return 'T1' +def get_tier(mmr: int, tier_info): + for tier in tier_info: + if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and (tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): + return tier["ladder_order"] diff --git a/prod_config.json b/prod_config.json index e8dcb96..a385c20 100644 --- a/prod_config.json +++ b/prod_config.json @@ -11,6 +11,7 @@ "queue_sub_channel": 1215174160234315806, "queue_list_channel": 1215174057453162496, "queue_history_channel": 1225709377747685477, + "queue_general_channel": 1241842559429513361, "members_for_channels": { "387347467332485122": [ 235148962103951360, @@ -21,7 +22,11 @@ 774812906830299136, 745443073202323586, 774866940950872095, - 1019051989734273054, + 1019051989734273054 + ] + }, + "roles_for_channels": { + "387347467332485122": [ 393600567781621761, 389252697284542465 ] diff --git a/test_config.json b/test_config.json index 5ad0ab4..4ee7f5a 100644 --- a/test_config.json +++ b/test_config.json @@ -11,6 +11,7 @@ "queue_sub_channel": 1228046571447386173, "queue_list_channel": 1228046584181293066, "queue_history_channel": 1228046598777344161, + "queue_general_channel": 1250554989508825168, "members_for_channels": { "1228027010836594698": [ 235148962103951360, @@ -21,9 +22,12 @@ 774812906830299136, 745443073202323586, 774866940950872095, - 1019051989734273054, - 393600567781621761, - 389252697284542465 + 1019051989734273054 + ] + }, + "roles_for_channels": { + "1228027010836594698": [ + 1249808212547604561 ] }, "queue_messages": true, From ccc3d48366ad9cdfcef98d6a5ab1705324b9e560 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Sat, 15 Jun 2024 15:48:41 -0700 Subject: [PATCH 014/292] Canning + Sub Issues Fix Re-added the check for duplicate instances in the queue list and fixed the sub command to function properly. --- cogs/SquadQueue.py | 13 ++++++++++++- mogi_objects.py | 3 ++- prod_config.json | 8 ++++++++ test_config.json | 8 ++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9a6e601..9724a8f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -170,6 +170,12 @@ def get_mogi(self, ctx): return self.ongoing_events[ctx.channel] return None + def is_staff(self, member): + for staff_role in self.bot.config["staff_roles"]: + if member.get_role(staff_role): + return True + return False + async def is_started(self, ctx, mogi): if not mogi.started: await ctx.send("Mogi has not been started yet... type !start") @@ -196,6 +202,10 @@ async def can(self, interaction: discord.Interaction): player_team = mogi.check_player(member) + if player_team is not None: + await interaction.followup.send(f"{interaction.user.mention} is already signed up.") + return + players = [] msg = "" placement_role_id = 723753340063842345 @@ -259,7 +269,7 @@ async def sub(self, interaction: discord.Interaction): if lastCommandTime == None: lastCommandTime = 0 - if (current_time - lastCommandTime) < 120: #Cooldown timer in seconds + if (current_time - lastCommandTime) < 120 and not self.is_staff(interaction.user): #Cooldown timer in seconds await interaction.response.send_message(f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", ephemeral=True) return @@ -965,6 +975,7 @@ async def debug_start_rooms(self, ctx): if mogi.start_time == self.prev_start_time: await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) return + async def setup(bot): diff --git a/mogi_objects.py b/mogi_objects.py index b2e792c..6d3571f 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -384,6 +384,7 @@ def __init__(self, room: Room, get_mmr, bottom_room_num): super().__init__(timeout=1200) self.room = room self.get_mmr = get_mmr + self.bottom_room_num = bottom_room_num @discord.ui.button(label="Join Room") async def button_callback(self, interaction, button): @@ -406,7 +407,7 @@ async def button_callback(self, interaction, button): return if self.room.room_num == 1: self.room.mmr_high = 999999 - if self.room.room_num == bottom_room_num: + if self.room.room_num == self.bottom_room_num: self.room.mmr_low = -999999 if isinstance(user_mmr, int) and user_mmr < self.room.mmr_high + 500 and user_mmr > self.room.mmr_low - 500: button.disabled = True diff --git a/prod_config.json b/prod_config.json index a385c20..bb082c3 100644 --- a/prod_config.json +++ b/prod_config.json @@ -31,6 +31,14 @@ 389252697284542465 ] }, + "staff_roles": [ + 387347888935534593, + 399382503825211393, + 399384750923579392, + 521149807994208295, + 792891432301625364, + 877629421811540016 + ], "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", diff --git a/test_config.json b/test_config.json index 4ee7f5a..06c72cb 100644 --- a/test_config.json +++ b/test_config.json @@ -30,6 +30,14 @@ 1249808212547604561 ] }, + "staff_roles": [ + 387347888935534593, + 399382503825211393, + 399384750923579392, + 521149807994208295, + 792891432301625364, + 877629421811540016 + ], "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", From 5beafcc2229066020f647abafa4b2e899140b33a Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Wed, 19 Jun 2024 21:58:20 -0700 Subject: [PATCH 015/292] Added Track Type to config Changed endpoint url's to reflect new track type variable in the configuration file of the bot. Also added the config details for the CT side of things. --- cogs/SquadQueue.py | 12 +++++----- ct_config.json | 55 ++++++++++++++++++++++++++++++++++++++++++++++ mmr.py | 10 +++++---- prod_config.json | 5 +++-- test_config.json | 5 +++-- 5 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 ct_config.json diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9724a8f..1c645aa 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -6,7 +6,7 @@ from datetime import datetime, timezone, timedelta import time import json -from mmr import mk8dx_150cc_mmr, get_mmr_from_discord_id +from mmr import mkw_mmr, get_mmr_from_discord_id from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio from collections import defaultdict @@ -66,6 +66,8 @@ def __init__(self, bot): self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] + self.TRACK_TYPE = bot.config["track_type"] + self.TIER_INFO = [] # number of minutes before scheduled time that queue should open @@ -216,7 +218,7 @@ async def can(self, interaction: discord.Interaction): msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" else: - players = await mk8dx_150cc_mmr(self.URL, [member]) + players = await mkw_mmr(self.URL, [member], self.TRACK_TYPE) if len(players) == 0 or players[0] is None: msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " @@ -557,7 +559,7 @@ async def update_tier_info(self, interaction: discord.Interaction): async def get_ladder_info(self): timeout = aiohttp.ClientTimeout(total=10) - url = "https://mkwlounge.gg/api/ladderclass.php?ladder_type=rt" + url = "https://mkwlounge.gg/api/ladderclass.php?ladder_type=" + self.TRACK_TYPE msg = "" try: async with aiohttp.ClientSession( @@ -928,7 +930,7 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): check_players = [ctx.author] check_players.extend(members) - players = await mk8dx_150cc_mmr(self.URL, check_players) + players = await mkw_mmr(self.URL, check_players, self.TRACK_TYPE) for i in range(0, 12): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) @@ -952,7 +954,7 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem check_players = [ctx.author] check_players.extend(members) - players = await mk8dx_150cc_mmr(self.URL, check_players) + players = await mkw_mmr(self.URL, check_players, self.TRACK_TYPE) for i in range(0, 100): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) diff --git a/ct_config.json b/ct_config.json new file mode 100644 index 0000000..55cfb91 --- /dev/null +++ b/ct_config.json @@ -0,0 +1,55 @@ +{ + "token": "", + "guild_id": 387347467332485122, + "admin_roles": { + "387347467332485122": [ + "Administrator", + "Lounge Staff" + ] + }, + "queue_join_channel": 1253109700107567155, + "queue_sub_channel": 1253109752418930779, + "queue_list_channel": 1253109466027655338, + "queue_history_channel": 1253109800196243618, + "queue_general_channel": 1253109398193045534, + "members_for_channels": { + "387347467332485122": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095, + 1019051989734273054 + ] + }, + "roles_for_channels": { + "387347467332485122": [ + 520808645252874240, + 520808674411937792 + ] + }, + "staff_roles": [ + 387347888935534593, + 399382503825211393, + 399384750923579392, + 521149807994208295, + 792891432301625364, + 877629421811540016 + ], + "queue_messages": true, + "sec_between_queue_msgs": 2, + "username": "username", + "password": "password", + "url": "https://www.mkwlounge.gg", + "track_type": "ct", + "TIME_ADJUSTMENT": 0, + "QUEUE_OPEN_TIME": 90, + "JOINING_TIME": 85, + "EXTENSION_TIME": 5, + "MOGI_LIFETIME": 180, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200, + "ROOM_MMR_THRESHOLD": 9000 +} \ No newline at end of file diff --git a/mmr.py b/mmr.py index d5bd746..c938b55 100644 --- a/mmr.py +++ b/mmr.py @@ -5,8 +5,9 @@ headers = {'Content-type': 'application/json'} -async def mk8dx_150cc_mmr(url, members): - base_url = url + '/api/ladderplayer.php?ladder_type=rt&' +async def mkw_mmr(url, members, track_type): + tracks = track_type + base_url = url + '/api/ladderplayer.php?ladder_type=' + tracks + '&' players = [] timeout = aiohttp.ClientTimeout(total=10) try: @@ -34,8 +35,9 @@ async def mk8dx_150cc_mmr(url, members): return players -async def get_mmr_from_discord_id(discord_id): - base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_type=rt&' +async def get_mmr_from_discord_id(discord_id, track_type): + tracks = track_type + base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_type=' + tracks + '&' timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession( timeout=timeout, diff --git a/prod_config.json b/prod_config.json index bb082c3..3b4b9c7 100644 --- a/prod_config.json +++ b/prod_config.json @@ -44,11 +44,12 @@ "username": "username", "password": "password", "url": "https://www.mkwlounge.gg", + "track_type": "rt", "TIME_ADJUSTMENT": 0, "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, - "EXTENSION_TIME": 3, + "EXTENSION_TIME": 5, "MOGI_LIFETIME": 180, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 8000 + "ROOM_MMR_THRESHOLD": 9000 } \ No newline at end of file diff --git a/test_config.json b/test_config.json index 06c72cb..6264685 100644 --- a/test_config.json +++ b/test_config.json @@ -43,11 +43,12 @@ "username": "username", "password": "password", "url": "https://www.mkwlounge.gg", + "track_type": "rt", "TIME_ADJUSTMENT": 0, "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, - "EXTENSION_TIME": 3, + "EXTENSION_TIME": 5, "MOGI_LIFETIME": 180, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 8000 + "ROOM_MMR_THRESHOLD": 9000 } \ No newline at end of file From 4caf62bc787e77751d49e882c53307ef11a7b8f6 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:24:34 -0500 Subject: [PATCH 016/292] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8c60208..b73e9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.venv* /config.json /credentials.json +.idea/* From 513a484a6e062d43ae4235573e58e621a1e09137 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:25:59 -0500 Subject: [PATCH 017/292] Type hints for reading clarity and IDE help --- mogi_objects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 6d3571f..484d699 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone, timedelta import time from discord.ui import View +from typing import List class Mogi: @@ -14,8 +15,8 @@ def __init__(self, sq_id: int, size: int, mogi_channel: discord.TextChannel, self.sq_id = sq_id self.size = size self.mogi_channel = mogi_channel - self.teams = [] - self.rooms = [] + self.teams: List[Team] = [] + self.rooms: List[Room] = [] self.is_automated = is_automated if not is_automated: self.start_time = None From ac33a099544d6941ba6e23fc619a04959ca49522 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:26:21 -0500 Subject: [PATCH 018/292] Refactor for reading clarity --- mogi_objects.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 484d699..ead98d5 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -18,10 +18,7 @@ def __init__(self, sq_id: int, size: int, mogi_channel: discord.TextChannel, self.teams: List[Team] = [] self.rooms: List[Room] = [] self.is_automated = is_automated - if not is_automated: - self.start_time = None - else: - self.start_time = start_time + self.start_time = start_time if is_automated else None def check_player(self, member): for team in self.teams: From d751a6bfc83722892172dd35812f26f0ec5704dc Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:26:51 -0500 Subject: [PATCH 019/292] Refactor count_registered for readability/conciseness --- mogi_objects.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index ead98d5..cc152cc 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -27,11 +27,8 @@ def check_player(self, member): return None def count_registered(self): - count = 0 - for team in self.teams: - if team.is_registered(): - count += 1 - return count + """Returns the number of teams that are registered""" + return sum(1 for team in self.teams if team.is_registered()) def confirmed_list(self): confirmed = [] From d305d4a3178349133d0c21282ff2f9402ecb8252 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:27:20 -0500 Subject: [PATCH 020/292] Refactor confirmed_list for readability/conciseness --- mogi_objects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index cc152cc..a752c54 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -31,11 +31,7 @@ def count_registered(self): return sum(1 for team in self.teams if team.is_registered()) def confirmed_list(self): - confirmed = [] - for team in self.teams: - if team.is_registered(): - confirmed.append(team) - return confirmed + return [team for team in self.teams if team.is_registered()] def remove_id(self, squad_id: int): confirmed = self.confirmed_list() From f0de7d18fb3879a8ab9defeaa00d39f8bdd7ce81 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:27:44 -0500 Subject: [PATCH 021/292] Refactor is_registered for readability/conciseness --- mogi_objects.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index a752c54..32e3f35 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -77,10 +77,8 @@ def recalc_avg(self): self.avg_mmr = sum([p.mmr for p in self.players]) / len(self.players) def is_registered(self): - for player in self.players: - if player.confirmed is False: - return False - return True + """Returns if all players on the team are registered""" + return all(player.confirmed for player in self.players) def has_player(self, member): for player in self.players: From f4e60efe4cc8087ef20bba4f411d4cb42be9bfc8 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:28:10 -0500 Subject: [PATCH 022/292] Refactor has_player for readability/conciseness --- mogi_objects.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 32e3f35..2bf5cc4 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -81,10 +81,7 @@ def is_registered(self): return all(player.confirmed for player in self.players) def has_player(self, member): - for player in self.players: - if player.member.id == member.id: - return True - return False + return any(player.member.id == member.id for player in self.players) def get_player(self, member): for player in self.players: From 2ab5a075d423515f450cf8cc976f17dc1d8eb72a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:28:24 -0500 Subject: [PATCH 023/292] Refactor num_confirmed for readability/conciseness --- mogi_objects.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 2bf5cc4..0f5f1ee 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -100,11 +100,8 @@ def sub_player(self, sub_out, sub_in): return def num_confirmed(self): - count = 0 - for player in self.players: - if player.confirmed: - count += 1 - return count + """Returns the number of confirmed players in the team""" + return sum(1 for player in self.players if player.confirmed) def get_unconfirmed(self): unconfirmed = [] From 784d4637ae2a82098ce861cbbf989b4940c66344 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:28:43 -0500 Subject: [PATCH 024/292] Refactor get_unconfirmed for readability/conciseness --- mogi_objects.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 0f5f1ee..472084b 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -104,11 +104,8 @@ def num_confirmed(self): return sum(1 for player in self.players if player.confirmed) def get_unconfirmed(self): - unconfirmed = [] - for player in self.players: - if not player.confirmed: - unconfirmed.append(player) - return unconfirmed + """Returns a list of players on the team who have not confirmed yet.""" + return [player for player in self.players if not player.confirmed] def __lt__(self, other): if self.avg_mmr < other.avg_mmr: From aac7c2a8799727eb7388cc9d519c14dfa7cb732a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:28:59 -0500 Subject: [PATCH 025/292] Refactor __lt__ for readability/conciseness --- mogi_objects.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 472084b..ab3ce39 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -108,10 +108,7 @@ def get_unconfirmed(self): return [player for player in self.players if not player.confirmed] def __lt__(self, other): - if self.avg_mmr < other.avg_mmr: - return True - if self.avg_mmr > other.avg_mmr: - return False + return self.avg_mmr < other.avg_mmr def __gt__(self, other): return other.__lt__(self) From 5adeec5930aa36cf6f3a11941efaade73b7e5e4b Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Fri, 21 Jun 2024 20:29:10 -0500 Subject: [PATCH 026/292] Refactor __gt__ for readability/conciseness --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index ab3ce39..1acca60 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -111,7 +111,7 @@ def __lt__(self, other): return self.avg_mmr < other.avg_mmr def __gt__(self, other): - return other.__lt__(self) + return other < self # def __eq__(self, other): # if self.avg_mmr == other.avg_mmr: From c64066d4746b4ccf84f191ba4aee575b5f00d725 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 00:29:35 -0500 Subject: [PATCH 027/292] Minor code formatting --- mogi_objects.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 1acca60..eccf5c3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -37,7 +37,7 @@ def remove_id(self, squad_id: int): confirmed = self.confirmed_list() if squad_id < 1 or squad_id > len(confirmed): return None - squad = confirmed[squad_id-1] + squad = confirmed[squad_id - 1] self.teams.remove(squad) return squad @@ -64,6 +64,7 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.mmr_low = None self.view = None self.finished = False + def get_player_list(self): return [player.member.id for team in self.teams for player in team.players] @@ -180,7 +181,7 @@ async def make_teams(self, format): scoreboard_text = [] for j in range(teams_per_room): - team_text = f"`{j+1}.` " + team_text = f"`{j + 1}.` " team_names = ", ".join([p.lounge_name for p in teams[j].players]) scoreboard_text.append(team_names) team_text += team_names @@ -398,5 +399,6 @@ async def button_callback(self, interaction, button): def get_tier(mmr: int, tier_info): for tier in tier_info: - if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and (tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): + if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( + tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): return tier["ladder_order"] From ecf4ab49bddac9788796c4ae6cc173d266513301 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 00:30:10 -0500 Subject: [PATCH 028/292] Bad practice to shadow global functions --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index eccf5c3..21d0cac 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -171,9 +171,9 @@ async def make_teams(self, format): msg += "\n" teams = [] - teams_per_room = int(12 / format[0]) + teams_per_room = int(12 / format_[0]) for j in range(teams_per_room): - team = Team(self.players[j*format[0]:(j+1)*format[0]]) + team = Team(self.players[j * format_[0]:(j + 1) * format_[0]]) teams.append(team) teams.sort(key=lambda team: team.avg_mmr, reverse=True) From 8f5a5ef185e36645c4075fa8ffb46be3f43b1130 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 00:30:32 -0500 Subject: [PATCH 029/292] Combine 2 lines of text into 1 --- mogi_objects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 21d0cac..5d885ea 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -167,8 +167,7 @@ async def make_teams(self, format): room_mmr = round(sum([p.mmr for p in self.players]) / 12) room.mmr_average = room_mmr self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " - msg += self.header_text - msg += "\n" + msg += self.header_text + "\n" teams = [] teams_per_room = int(12 / format_[0]) From 86ed77614b4be21e6fd8d8edcb237526c80f6d0a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 00:31:19 -0500 Subject: [PATCH 030/292] Clean up messy code, make more concise --- mogi_objects.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 5d885ea..5e0bd51 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -188,17 +188,9 @@ async def make_teams(self, format): msg += team_text self.teams_text += team_text - minutes = self.mogi.start_time.minute - pen = minutes + 8 - - if pen > 60: - pen -= 60 - - if pen < 10: - pen = '0' + str(pen) - if minutes < 10: - minutes = '0' + str(minutes) - msg += "Decide a host amongst yourselves; room open at :{}, penalty at :{}. Good luck!".format(minutes, pen) + penalty_time = self.mogi.start_time + timedelta(minutes=8) + room_open_time = self.mogi.start_time + msg += f"Decide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" room.teams = teams From 3121bce4e8689553e72cf340c99afd0c8485e845 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 00:33:33 -0500 Subject: [PATCH 031/292] Overhaul of voting code In addition to addressing code duplication, this commit addresses poor practices of using variables that start with a number. It also addresses poor practices of shadowing global functions (max) which is why original developer couldn't determine why max wasn't working. --- mogi_objects.py | 175 ++++++++++++------------------------------------ 1 file changed, 44 insertions(+), 131 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 5e0bd51..cd093bc 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -142,27 +142,28 @@ def __init__(self, players, thread, mogi, tier_info): self.teams_text = "" self.found_winner = False self.tier_info = tier_info - self.__setattr__("FFA", []) - self.__setattr__("2v2", []) - self.__setattr__("3v3", []) - self.__setattr__("4v4", []) - self.__setattr__("6v6", []) - - def __getitem__(self, key): - return getattr(self, key) - - async def make_teams(self, format): + self.votes = {"FFA": [], + "2v2": [], + "3v3": [], + "4v4": [], + "6v6": [] + } + + async def make_teams(self, format_): random.shuffle(self.players) room = self.mogi.get_room_from_thread(self.thread.id) - msg = "**Poll Ended!** \n\n" - msg += f"1) FFA - {len(self['FFA'])}\n" - msg += f"2) 2v2 - {len(self['2v2'])}\n" - msg += f"3) 3v3 - {len(self['3v3'])}\n" - msg += f"4) 4v4 - {len(self['4v4'])}\n" - msg += f"5) 6v6 - {len(self['6v6'])}\n" - msg += f"Winner: {format[1]}\n\n" + msg = f"""**Poll Ended!** + +1) FFA - {len(self.votes['FFA'])} +2) 2v2 - {len(self.votes['2v2'])} +3) 3v3 - {len(self.votes['3v3'])} +4) 4v4 - {len(self.votes['4v4'])} +5) 6v6 - {len(self.votes['6v6'])} +Winner: {format_[1]} + +""" room_mmr = round(sum([p.mmr for p in self.players]) / 12) room.mmr_average = room_mmr @@ -199,30 +200,19 @@ async def make_teams(self, format): async def find_winner(self): if not self.found_winner: - # for some reason max function wasnt working... - max = 0 - if len(self["FFA"]) > max: - max = len(self["FFA"]) - if len(self["2v2"]) > max: - max = len(self["2v2"]) - if len(self["3v3"]) > max: - max = len(self["3v3"]) - if len(self["4v4"]) > max: - max = len(self["4v4"]) - if len(self["6v6"]) > max: - max = len(self["6v6"]) + # for some reason max function wasnt working... # It's because you were replacing it. + most_votes = len(max(self.votes.values(), key=len)) winners = [] - - if len(self["FFA"]) == max: + if len(self.votes["FFA"]) == most_votes: winners.append((1, "FFA")) - if len(self["2v2"]) == max: + if len(self.votes["2v2"]) == most_votes: winners.append((2, "2v2")) - if len(self["3v3"]) == max: + if len(self.votes["3v3"]) == most_votes: winners.append((3, "3v3")) - if len(self["4v4"]) == max: + if len(self.votes["4v4"]) == most_votes: winners.append((4, "4v4")) - if len(self["6v6"]) == max: + if len(self.votes["6v6"]) == most_votes: winners.append((6, "6v6")) winner = random.choice(winners) @@ -234,116 +224,39 @@ async def find_winner(self): @discord.ui.button(label="FFA: 0", custom_id="FFA") async def one_button_callback(self, interaction, button): - if not self.found_winner: - if interaction.user.id in self["FFA"]: - self["FFA"].remove(interaction.user.id) - else: - if interaction.user.id in self["2v2"]: - self["2v2"].remove(interaction.user.id) - if interaction.user.id in self["3v3"]: - self["3v3"].remove(interaction.user.id) - if interaction.user.id in self["4v4"]: - self["4v4"].remove(interaction.user.id) - if interaction.user.id in self["6v6"]: - self["6v6"].remove(interaction.user.id) - self["FFA"].append(interaction.user.id) - if len(self["FFA"]) == 6: - await self.make_teams((1, "FFA")) - for curr_button in self.children: - curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" - if len(self["FFA"]) == 6: - curr_button.disabled = True - await interaction.response.edit_message(view=self) + await self.general_vote_callback(interaction, 1, "FFA") @discord.ui.button(label="2v2: 0", custom_id="2v2") async def two_button_callback(self, interaction, button): - if not self.found_winner: - if interaction.user.id in self["2v2"]: - self["2v2"].remove(interaction.user.id) - else: - if interaction.user.id in self["FFA"]: - self["FFA"].remove(interaction.user.id) - if interaction.user.id in self["3v3"]: - self["3v3"].remove(interaction.user.id) - if interaction.user.id in self["4v4"]: - self["4v4"].remove(interaction.user.id) - if interaction.user.id in self["6v6"]: - self["6v6"].remove(interaction.user.id) - self["2v2"].append(interaction.user.id) - if len(self["2v2"]) == 6: - await self.make_teams((2, "2v2")) - for curr_button in self.children: - curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" - if len(self["2v2"]) == 6: - curr_button.disabled = True - await interaction.response.edit_message(view=self) + await self.general_vote_callback(interaction, 2, "2v2") @discord.ui.button(label="3v3: 0", custom_id="3v3") async def three_button_callback(self, interaction, button): - if not self.found_winner: - if interaction.user.id in self["3v3"]: - self["3v3"].remove(interaction.user.id) - else: - if interaction.user.id in self["FFA"]: - self["FFA"].remove(interaction.user.id) - if interaction.user.id in self["2v2"]: - self["2v2"].remove(interaction.user.id) - if interaction.user.id in self["4v4"]: - self["4v4"].remove(interaction.user.id) - if interaction.user.id in self["6v6"]: - self["6v6"].remove(interaction.user.id) - self["3v3"].append(interaction.user.id) - if len(self["3v3"]) == 6: - await self.make_teams((3, "3v3")) - for curr_button in self.children: - curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" - if len(self["3v3"]) == 6: - curr_button.disabled = True - await interaction.response.edit_message(view=self) + await self.general_vote_callback(interaction, 3, "3v3") @discord.ui.button(label="4v4: 0", custom_id="4v4") async def four_button_callback(self, interaction, button): - if not self.found_winner: - if interaction.user.id in self["4v4"]: - self["4v4"].remove(interaction.user.id) - else: - if interaction.user.id in self["FFA"]: - self["FFA"].remove(interaction.user.id) - if interaction.user.id in self["2v2"]: - self["2v2"].remove(interaction.user.id) - if interaction.user.id in self["3v3"]: - self["3v3"].remove(interaction.user.id) - if interaction.user.id in self["6v6"]: - self["6v6"].remove(interaction.user.id) - self["4v4"].append(interaction.user.id) - if len(self["4v4"]) == 6: - await self.make_teams((4, "4v4")) - for curr_button in self.children: - curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" - if len(self["4v4"]) == 6: - curr_button.disabled = True - await interaction.response.edit_message(view=self) + await self.general_vote_callback(interaction, 4, "4v4") @discord.ui.button(label="6v6: 0", custom_id="6v6") async def six_button_callback(self, interaction, button): + await self.general_vote_callback(interaction, 6, "6v6") + + async def general_vote_callback(self, interaction: discord.Interaction, players_per_team: int, vote: str): if not self.found_winner: - if interaction.user.id in self["6v6"]: - self["6v6"].remove(interaction.user.id) - else: - if interaction.user.id in self["FFA"]: - self["FFA"].remove(interaction.user.id) - if interaction.user.id in self["2v2"]: - self["2v2"].remove(interaction.user.id) - if interaction.user.id in self["3v3"]: - self["3v3"].remove(interaction.user.id) - if interaction.user.id in self["4v4"]: - self["4v4"].remove(interaction.user.id) - self["6v6"].append(interaction.user.id) - if len(self["6v6"]) == 6: - await self.make_teams((6, "6v6")) + original_vote = None + for vote_option, voter_ids in self.votes.items(): + if interaction.user.id in voter_ids: + original_vote = vote_option + voter_ids.remove(interaction.user.id) + if original_vote != vote: # They changed their vote or are a new voter + self.votes[vote].append(interaction.user.id) + if len(self.votes[vote]) == 6: + self.found_winner = True # This fixes a race condition + await self.make_teams((players_per_team, vote)) for curr_button in self.children: - curr_button.label = f"{curr_button.custom_id}: {len(self[curr_button.custom_id])}" - if len(self["6v6"]) == 6: + curr_button.label = f"{curr_button.custom_id}: {len(self.votes[curr_button.custom_id])}" + if self.found_winner: curr_button.disabled = True await interaction.response.edit_message(view=self) From 3ebb3814badc6b5a835fb6f6283d43099e6a54de Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 02:37:35 -0500 Subject: [PATCH 032/292] New penalty/open rooms time variable --- mogi_objects.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index cd093bc..3cdb3d5 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -12,6 +12,7 @@ def __init__(self, sq_id: int, size: int, mogi_channel: discord.TextChannel, self.started = False self.gathering = False self.making_rooms_run = False + self.making_rooms_run_time = None self.sq_id = sq_id self.size = size self.mogi_channel = mogi_channel @@ -133,7 +134,7 @@ def __init__(self, member, lounge_name, mmr): class VoteView(View): - def __init__(self, players, thread, mogi, tier_info): + def __init__(self, players, thread, mogi: Mogi, tier_info): super().__init__() self.players = players self.thread = thread @@ -150,6 +151,7 @@ def __init__(self, players, thread, mogi, tier_info): } async def make_teams(self, format_): + self.mogi.making_rooms_run_time = datetime.now(timezone.utc) random.shuffle(self.players) room = self.mogi.get_room_from_thread(self.thread.id) @@ -189,8 +191,8 @@ async def make_teams(self, format_): msg += team_text self.teams_text += team_text - penalty_time = self.mogi.start_time + timedelta(minutes=8) - room_open_time = self.mogi.start_time + penalty_time = self.mogi.making_rooms_run_time + timedelta(minutes=8) + room_open_time = self.mogi.making_rooms_run_time msg += f"Decide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" room.teams = teams From 5aadbee0e6ac87408ff0bb140a6e84a6ca39e3ee Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:17:52 -0500 Subject: [PATCH 033/292] New configuration --- ct_config.json | 3 ++- prod_config.json | 3 ++- test_config.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ct_config.json b/ct_config.json index 55cfb91..3a777d7 100644 --- a/ct_config.json +++ b/ct_config.json @@ -45,7 +45,8 @@ "password": "password", "url": "https://www.mkwlounge.gg", "track_type": "ct", - "TIME_ADJUSTMENT": 0, + "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 90, "JOINING_TIME": 85, "EXTENSION_TIME": 5, diff --git a/prod_config.json b/prod_config.json index 3b4b9c7..bf37127 100644 --- a/prod_config.json +++ b/prod_config.json @@ -45,7 +45,8 @@ "password": "password", "url": "https://www.mkwlounge.gg", "track_type": "rt", - "TIME_ADJUSTMENT": 0, + "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, "EXTENSION_TIME": 5, diff --git a/test_config.json b/test_config.json index 6264685..588af53 100644 --- a/test_config.json +++ b/test_config.json @@ -44,7 +44,8 @@ "password": "password", "url": "https://www.mkwlounge.gg", "track_type": "rt", - "TIME_ADJUSTMENT": 0, + "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, "EXTENSION_TIME": 5, From eb329e66a35c5f7508389108b22ee4701fe54aa8 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:20:46 -0500 Subject: [PATCH 034/292] Don't print exception, print traceback --- cogs/SquadQueue.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1c645aa..37ea9cf 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -100,10 +100,12 @@ async def on_ready(self): await self.LIST_CHANNEL.purge() except: print("Purging List channel failed", flush=True) + print(traceback.format_exc()) try: await self.SUB_CHANNEL.purge() except: print("Purging Sub channel failed", flush=True) + print(traceback.format_exc()) print(f"Server - {self.GUILD}", flush=True) print(f"Join Channel - {self.MOGI_CHANNEL}", flush=True) print(f"Sub Channel - {self.SUB_CHANNEL}", flush=True) @@ -123,7 +125,7 @@ async def lockdown(self, channel: discord.TextChannel): await channel.set_permissions(channel.guild.default_role, overwrite=overwrite) await channel.send("Locked down " + channel.mention) except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) async def unlockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -165,7 +167,7 @@ async def send_queued_messages(self): for i in range(len(sentmsgs)-1, -1, -1): await channel.send(sentmsgs[i]) except Exception as e: - print(e) + print(traceback.format_exc()) def get_mogi(self, ctx): if ctx.channel in self.ongoing_events.keys(): @@ -384,7 +386,7 @@ async def delete_list_messages(self, new_list_size: int): if self.LIST_CHANNEL and len(messages_to_delete) > 0: await self.LIST_CHANNEL.delete_messages(messages_to_delete) except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) @commands.Cog.listener() async def on_message(self, message: discord.Message): @@ -590,8 +592,7 @@ async def get_ladder_info(self): print(msg, flush=True) return msg except Exception as e: - print(e, flush=True) - + print(traceback.format_exc()) # check if user has roles defined in config.json async def has_roles(self, member: discord.Member, guild_id: int, config): @@ -615,7 +616,7 @@ async def end_voting(self): continue await room.view.find_winner() except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) async def write_history(self): """Writes the teams, tier and average of each room per hour.""" @@ -635,7 +636,7 @@ async def write_history(self): msg += "ㅤ" await self.HISTORY_CHANNEL.send(msg) except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) # make thread channels while the event is gathering instead of at the end, # since discord only allows 50 thread channels to be created per 5 minutes. @@ -655,7 +656,7 @@ async def check_room_channels(self, mogi): auto_archive_duration=60, invitable=False) except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) err_msg = f"\nAn error has occurred while creating a room channel:\n{e}" await mogi.mogi_channel.send(err_msg) return @@ -735,11 +736,11 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F curr_room.view = view await room_channel.send(view=view) except Exception as e: - print(e, flush=True) - err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" - err_msg += mentions - msg += err_msg - room_channel = None + print(traceback.format_exc()) + err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" + err_msg += mentions + msg += err_msg + room_channel = None try: await mogi.mogi_channel.send(msg) except Exception as e: @@ -833,11 +834,11 @@ async def sqscheduler(self): try: await self.scheduler_mogi_start() except Exception as e: - print(e) + print(traceback.format_exc()) try: await self.ongoing_mogi_checks() except Exception as e: - print(e) + print(traceback.format_exc()) @tasks.loop(minutes=1) async def que_scheduler(self): @@ -898,7 +899,7 @@ async def delete_old_mogis(self): f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) del self.old_events[mogi.mogi_channel] except Exception as e: - print(e, flush=True) + print(traceback.format_exc()) def get_event_str(self, mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") From 68038eaea18c89b8956aec53a615b03c5057b83a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:21:18 -0500 Subject: [PATCH 035/292] Add imports --- cogs/SquadQueue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 37ea9cf..83e55e3 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import aiohttp import discord from discord.ext import commands, tasks @@ -10,6 +12,8 @@ from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio from collections import defaultdict +from typing import Dict, List +import traceback headers = {'Content-type': 'application/json'} From 5635b4b61cfc04eb9bc31e0ab6d803a5104025da Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:22:46 -0500 Subject: [PATCH 036/292] Minor refactor of is_staff --- cogs/SquadQueue.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 83e55e3..c11619b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -178,11 +178,8 @@ def get_mogi(self, ctx): return self.ongoing_events[ctx.channel] return None - def is_staff(self, member): - for staff_role in self.bot.config["staff_roles"]: - if member.get_role(staff_role): - return True - return False + def is_staff(self, member: discord.Member): + return any(member.get_role(staff_role) for staff_role in self.bot.config["staff_roles"]) async def is_started(self, ctx, mogi): if not mogi.started: From 602e2440fc01087bdb7873976e247afa72d34670 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:24:32 -0500 Subject: [PATCH 037/292] Adjustable amount for debug_many_players --- cogs/SquadQueue.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c11619b..647a653 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -947,6 +947,7 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): @commands.command(name="debug_add_many_players") @commands.is_owner() async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Member]): + num_times = 25 mogi = self.get_mogi(ctx) if mogi is None: return @@ -957,13 +958,13 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem check_players = [ctx.author] check_players.extend(members) players = await mkw_mmr(self.URL, check_players, self.TRACK_TYPE) - for i in range(0, 100): + for i in range(0, num_times): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) player.confirmed = True squad = Team([player]) mogi.teams.append(squad) - msg = f"{players[0].lounge_name} added 100 times." + msg = f"{players[0].lounge_name} added {num_times} times." await self.queue_or_send(ctx, msg) await self.check_room_channels(mogi) await self.check_num_teams(mogi) From a912711f3c8751cfd47530ef8859c2ba5cd255fc Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:26:01 -0500 Subject: [PATCH 038/292] Move queue scheduler into sqscheduler This has numerous benefits, but most importantly, it will help with race conditions --- cogs/SquadQueue.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 647a653..7a325a2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -36,7 +36,6 @@ def __init__(self, bot): self.prev_start_time = None - self._que_scheduler = self.que_scheduler.start() self._scheduler_task = self.sqscheduler.start() self._msgqueue_task = self.send_queued_messages.start() self._list_task = self.list_task.start() @@ -832,6 +831,12 @@ async def sqscheduler(self): # It may seem silly to do try/except Exception, but this coroutine **cannot** fail # This coroutine *silently* fails and stops if exceptions aren't caught - an annoying abtraction of asyncio # This is unacceptable considering people are relying on these mogis to run, so we will not allow this routine to stop + try: + print(f"{datetime.now()}: sqscheduler ran") + if self.ongoing_event is None: + await self.schedule_que_event() + except Exception as e: + print(traceback.format_exc()) try: await self.scheduler_mogi_start() except Exception as e: @@ -841,13 +846,6 @@ async def sqscheduler(self): except Exception as e: print(traceback.format_exc()) - @tasks.loop(minutes=1) - async def que_scheduler(self): - try: - if not self.scheduled_events or len(self.scheduled_events[self.GUILD]) == 0: - await self.schedule_que_event() - except Exception as e: - print(e) async def schedule_que_event(self): """Schedules queue for the next hour in the given channel.""" From 99361182baa694d7e6b8ec06e7d88a98ea724ef7 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:40:33 -0500 Subject: [PATCH 039/292] Minor items --- cogs/SquadQueue.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7a325a2..17d481a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -180,13 +180,13 @@ def get_mogi(self, ctx): def is_staff(self, member: discord.Member): return any(member.get_role(staff_role) for staff_role in self.bot.config["staff_roles"]) - async def is_started(self, ctx, mogi): + async def is_started(self, ctx, mogi: Mogi): if not mogi.started: await ctx.send("Mogi has not been started yet... type !start") return False return True - async def is_gathering(self, ctx, mogi): + async def is_gathering(self, ctx, mogi: Mogi): if not mogi.gathering: await ctx.send("Mogi is closed; players cannot join or drop from the event") return False @@ -261,6 +261,7 @@ async def drop(self, interaction: discord.Interaction): msg += f", `[{mogi.count_registered()} players]`" await interaction.followup.send(msg) + await self.check_num_teams(mogi) @app_commands.command(name="sub") @app_commands.guild_only() @@ -746,7 +747,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F except Exception as e: print( f"Mogi Channel message for room {i+1} has failed to send.", flush=True) - print(e, flush=True) + print(traceback.format_exc()) if num_teams < mogi.count_registered(): missed_teams = mogi.confirmed_list( )[num_teams:mogi.count_registered()] @@ -759,10 +760,10 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F await mogi.mogi_channel.send(msg) except Exception as e: print("Late Player message has failed to send.", flush=True) - print(e, flush=True) await asyncio.sleep(120) await self.end_voting() await self.write_history() + print(traceback.format_exc()) async def check_num_teams(self, mogi): if not mogi.gathering or not mogi.is_automated: @@ -832,7 +833,6 @@ async def sqscheduler(self): # This coroutine *silently* fails and stops if exceptions aren't caught - an annoying abtraction of asyncio # This is unacceptable considering people are relying on these mogis to run, so we will not allow this routine to stop try: - print(f"{datetime.now()}: sqscheduler ran") if self.ongoing_event is None: await self.schedule_que_event() except Exception as e: @@ -900,7 +900,7 @@ async def delete_old_mogis(self): except Exception as e: print(traceback.format_exc()) - def get_event_str(self, mogi): + def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( mogi.start_time, style="R") From f70151e44ca8e20a4b0d90f2f277e96238bf782f Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:42:24 -0500 Subject: [PATCH 040/292] Main overhaul This commit overhauls complex code surrounding: SquadQueue.scheduled_events SquadQueue.ongoing_events SquadQueue.old_events And scheduling events --- cogs/SquadQueue.py | 431 ++++++++++++++++++++++----------------------- 1 file changed, 213 insertions(+), 218 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 17d481a..b00508a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -25,16 +25,13 @@ class SquadQueue(commands.Cog): def __init__(self, bot): self.bot = bot - # keys are discord.Guild objects, values are list of Mogi instances - self.scheduled_events = {} - # keys are discord.TextChannel objects, values are instances of Mogi - self.ongoing_events = {} + self.next_event: Mogi = None - self.old_events = {} + self.ongoing_event: Mogi = None - self.sq_times = [] + self.old_events: List[Mogi] = [] - self.prev_start_time = None + self.sq_times = [] self._scheduler_task = self.sqscheduler.start() self._msgqueue_task = self.send_queued_messages.start() @@ -45,7 +42,7 @@ def __init__(self, bot): self.list_messages = [] - self.QUEUE_TIME_BLOCKER = datetime.now(timezone.utc) + self.LAUNCH_NEW_EVENTS = True self.GUILD = None @@ -73,7 +70,7 @@ def __init__(self, bot): self.TIER_INFO = [] - # number of minutes before scheduled time that queue should open + # Time between each event queue opening self.QUEUE_OPEN_TIME = timedelta(minutes=bot.config["QUEUE_OPEN_TIME"]) # number of minutes after QUEUE_OPEN_TIME that teams can join the mogi @@ -82,6 +79,13 @@ def __init__(self, bot): # number of minutes after JOINING_TIME for any potential extra teams to join self.EXTENSION_TIME = timedelta(minutes=bot.config["EXTENSION_TIME"]) + assert (self.QUEUE_OPEN_TIME >= self.JOINING_TIME + self.EXTENSION_TIME) + + # This is the first event of the day's time. However, this isn't the first event the bot will run. This is literally the time of the first event in a daily schedule. + self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), + tzinfo=timezone.utc) + timedelta( + minutes=bot.config["FIRST_EVENT_TIME"]) + with open('./timezones.json', 'r') as cjson: self.timezones = json.load(cjson) @@ -172,10 +176,11 @@ async def send_queued_messages(self): except Exception as e: print(traceback.format_exc()) - def get_mogi(self, ctx): - if ctx.channel in self.ongoing_events.keys(): - return self.ongoing_events[ctx.channel] - return None + def get_mogi(self, ctx: discord.Interaction | discord.ext.commands.context.Context) -> Mogi | None: + if self.ongoing_event is None: + return None + if self.ongoing_event.mogi_channel.id == ctx.channel.id: + return self.ongoing_event def is_staff(self, member: discord.Member): return any(member.get_role(staff_role) for staff_role in self.bot.config["staff_roles"]) @@ -232,7 +237,7 @@ async def can(self, interaction: discord.Interaction): squad = Team(players) mogi.teams.append(squad) - msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time - (self.QUEUE_OPEN_TIME - self.JOINING_TIME))}, `[{mogi.count_registered()} players]`" + msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" await interaction.followup.send(msg) await self.check_room_channels(mogi) @@ -273,28 +278,32 @@ async def sub(self, interaction: discord.Interaction): print(lastCommandTime, flush=True) if lastCommandTime == None: lastCommandTime = 0 - - if (current_time - lastCommandTime) < 120 and not self.is_staff(interaction.user): #Cooldown timer in seconds - await interaction.response.send_message(f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", ephemeral=True) + + if (current_time - lastCommandTime) < 120 and not self.is_staff(interaction.user): # Cooldown timer in seconds + await interaction.response.send_message( + f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", + ephemeral=True) return is_room_thread = False room = None bottom_room_num = 1 - for mogi in self.ongoing_events.values(): + mogi = self.ongoing_event + if mogi is not None: if mogi.is_room_thread(interaction.channel_id): room = mogi.get_room_from_thread(interaction.channel_id) bottom_room_num = len(mogi.rooms) is_room_thread = True - break - for mogi in self.old_events.values(): - if mogi.is_room_thread(interaction.channel.id): - room = mogi.get_room_from_thread(interaction.channel.id) - bottom_room_num = len(mogi.rooms) + for old_mogi in self.old_events: + if old_mogi.is_room_thread(interaction.channel.id): + room = old_mogi.get_room_from_thread(interaction.channel.id) + bottom_room_num = len(old_mogi.rooms) is_room_thread = True break if not is_room_thread: - await interaction.response.send_message(f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) + await interaction.response.send_message( + f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", + ephemeral=True) return msg = "<@&1167985222533533817> - " if bottom_room_num == 1: @@ -302,7 +311,7 @@ async def sub(self, interaction: discord.Interaction): elif room.room_num == 1: msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - 500}\n" elif room.room_num == bottom_room_num: - msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 500}\n" + msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 500}\n" else: msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 500}-{room.mmr_high + 500}\n" message_delete_date = datetime.now( @@ -311,71 +320,72 @@ async def sub(self, interaction: discord.Interaction): await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) view = JoinView(room, get_mmr_from_discord_id, bottom_room_num) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - cooldowns[interaction.user.id] = current_time #Updates cooldown + cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") @tasks.loop(seconds=30) async def list_task(self): """Continually display the list of confirmed players for a mogi in the history channel""" - if len(self.ongoing_events) > 0: - for mogi in self.ongoing_events.values(): - if not mogi.gathering: - await self.delete_list_messages(0) - return + mogi = self.ongoing_event + if mogi is not None: + if not mogi.gathering: + await self.delete_list_messages(0) + return - mogi_list = mogi.confirmed_list() - - # Remove late players from the list to display separately - full_list_length = len(mogi_list) - num_of_rooms = full_list_length // 12 - num_confirmed_players = num_of_rooms * 12 - num_late_players = full_list_length - num_confirmed_players - late_players = [] - for i in range(num_confirmed_players, full_list_length): - player = mogi_list.pop() - late_players.append(player) - - sorted_mogi_list = sorted(mogi_list, reverse=True) - msg = f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" - msg += "**Current Mogi List:**\n" - for i in range(len(sorted_mogi_list)): - msg += f"{i+1}) " - msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) - msg += f" ({sorted_mogi_list[i].players[0].mmr} MMR)\n" - if ((i + 1) % 12 == 0): - msg += "ㅤ\n" - msg += "**Late Players:**\n" - for i in range(len(late_players)): - msg += f"{i+1}) " - msg += ", ".join([p.lounge_name for p in late_players[i].players]) - msg += f" ({late_players[i].players[0].mmr} MMR)\n" - message = msg.split("\n") - - new_messages = [] - bulk_msg = "" - for i in range(len(message)): - if len(bulk_msg + message[i] + "\n") > 2000: - new_messages.append(bulk_msg) - bulk_msg = "" - bulk_msg += message[i] + "\n" - if len(bulk_msg) > 0 and bulk_msg != "\n": + mogi_list = mogi.confirmed_list() + + # Remove late players from the list to display separately + full_list_length = len(mogi_list) + num_of_rooms = full_list_length // 12 + num_confirmed_players = num_of_rooms * 12 + num_late_players = full_list_length - num_confirmed_players + late_players = [] + for i in range(num_confirmed_players, full_list_length): + player = mogi_list.pop() + late_players.append(player) + + sorted_mogi_list = sorted(mogi_list, reverse=True) + msg = f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" + msg += "**Current Mogi List:**\n\n" + msg += f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n" + for i in range(len(sorted_mogi_list)): + msg += f"{i + 1}) " + msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) + msg += f" ({sorted_mogi_list[i].players[0].mmr} MMR)\n" + if ((i + 1) % 12 == 0): + msg += "ㅤ\n" + msg += "**Late Players:**\n" + for i in range(len(late_players)): + msg += f"{i + 1}) " + msg += ", ".join([p.lounge_name for p in late_players[i].players]) + msg += f" ({late_players[i].players[0].mmr} MMR)\n" + message = msg.split("\n") + + new_messages = [] + bulk_msg = "" + for i in range(len(message)): + if len(bulk_msg + message[i] + "\n") > 2000: new_messages.append(bulk_msg) + bulk_msg = "" + bulk_msg += message[i] + "\n" + if len(bulk_msg) > 0 and bulk_msg != "\n": + new_messages.append(bulk_msg) - await self.delete_list_messages(len(new_messages)) + await self.delete_list_messages(len(new_messages)) - try: - for i, message in enumerate(new_messages): - if i < len(self.list_messages): - old_message = self.list_messages[i] - await old_message.edit(content=message) - else: - new_message = await self.LIST_CHANNEL.send(message) - self.list_messages.append(new_message) - except: - await self.delete_list_messages(0) - for i, message in enumerate(new_messages): + try: + for i, message in enumerate(new_messages): + if i < len(self.list_messages): + old_message = self.list_messages[i] + await old_message.edit(content=message) + else: new_message = await self.LIST_CHANNEL.send(message) self.list_messages.append(new_message) + except: + await self.delete_list_messages(0) + for i, message in enumerate(new_messages): + new_message = await self.LIST_CHANNEL.send(message) + self.list_messages.append(new_message) else: await self.delete_list_messages(0) @@ -393,24 +403,21 @@ async def delete_list_messages(self, new_list_size: int): async def on_message(self, message: discord.Message): if message.author.bot or not (message.content.isdecimal() and 12 <= int(message.content) <= 180): return - mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( - message.channel.id), self.ongoing_events.values()) - if not mogi: - mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( - message.channel.id), self.old_events.values()) - if not mogi: - return - room = discord.utils.find( - lambda room: room.thread.id == message.channel.id, mogi.rooms) - if not room or not room.teams: + mogi = None + if self.ongoing_event is not None: + mogi = self.ongoing_event if self.ongoing_event.is_room_thread(message.channel.id) else None + if mogi is None: + mogi = discord.utils.find(lambda m: m.is_room_thread(message.channel.id), self.old_events) + if mogi is None: + return + room = discord.utils.find(lambda r: r.thread.id == message.channel.id, mogi.rooms) + if room is None or not room.teams: return - team = discord.utils.find( - lambda team: team.has_player(message.author), room.teams) - if not team: + team = discord.utils.find(lambda t: t.has_player(message.author), room.teams) + if team is None: return - player = discord.utils.find( - lambda player: player.member.id == message.author.id, team.players) - if player: + player = discord.utils.find(lambda p: p.member.id == message.author.id, team.players) + if player is not None: player.score = int(message.content) @app_commands.command(name="remove_player") @@ -440,8 +447,8 @@ async def remove_player(self, interaction: discord.Interaction, member: discord. @app_commands.guild_only() async def annul_current_mogi(self, interaction: discord.Interaction): """The mogi currently gathering will be deleted. The queue resumes at the next hour. Staff use only.""" - self.scheduled_events = {} - self.ongoing_events = {} + self.next_event = None + self.ongoing_event = None await self.lockdown(self.MOGI_CHANNEL) await interaction.response.send_message("The current Mogi has been canceled, the queue will resume at the next hour.") @@ -449,17 +456,14 @@ async def annul_current_mogi(self, interaction: discord.Interaction): @app_commands.guild_only() async def pause_mogi_scheduling(self, interaction: discord.Interaction): """The mogi that is currently gathering will continue to work. Future mogis cannot be scheduled. Staff use only.""" - curr_time = datetime.now(timezone.utc) - self.QUEUE_TIME_BLOCKER = curr_time + timedelta(weeks=52) + self.LAUNCH_NEW_EVENTS = False await interaction.response.send_message("Future Mogis will not be started.") @app_commands.command(name="resume_mogi_scheduling") @app_commands.guild_only() async def resume_mogi_scheduling(self, interaction: discord.Interaction): """Mogis will begin to be scheduled again. Staff use only.""" - curr_time = datetime.now(timezone.utc) - self.QUEUE_TIME_BLOCKER = curr_time - self.prev_start_time = None + self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("Mogis will resume scheduling.") @app_commands.command(name="change_event_time") @@ -468,8 +472,9 @@ async def change_event_time(self, interaction: discord.Interaction, event_time: """Change the amount of time for each event in the queue.""" if event_time > 15: self.QUEUE_OPEN_TIME = timedelta(minutes=event_time) - self.JOINING_TIME = timedelta(minutes=event_time - 5) - await interaction.response.send_message(f"The amount of time for each mogi has been changed to {event_time} minutes.") + self.JOINING_TIME = timedelta(minutes=event_time) - self.EXTENSION_TIME + await interaction.response.send_message( + f"The amount of time for each mogi has been changed to {event_time} minutes.") else: await interaction.response.send_message("Please enter a number of minutes greater than 15.") @@ -485,7 +490,7 @@ async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_thres async def peek_bot_config(self, interaction: discord.Interaction): """View the configured values for the Queue System.""" msg = "" - msg += f"Time for Each Event: {self.QUEUE_OPEN_TIME}, \n" + msg += f"Time between each event: {self.QUEUE_OPEN_TIME}, \n" msg += f"Room MMR Threshold: {self.room_mmr_threshold}" await interaction.response.send_message(msg) @@ -493,11 +498,10 @@ async def peek_bot_config(self, interaction: discord.Interaction): @app_commands.guild_only() async def reset_bot(self, interaction: discord.Interaction): """Resets the bot. Staff use only.""" - self.scheduled_events = {} - self.ongoing_events = {} - self.old_events = {} - curr_time = datetime.now(timezone.utc) - self.QUEUE_TIME_BLOCKER = curr_time + self.next_event = None + self.ongoing_event = None + self.old_events = [] + self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("All events have been deleted. Queue will restart shortly.") @commands.command(name="schedule_sq_times") @@ -506,6 +510,9 @@ async def schedule_sq_times(self, ctx, timestamps: commands.Greedy[int]): """Saves a list of sq times to skip over. Input a list of unix utc timestamps. Staff use only.""" if not await self.has_roles(ctx.author, ctx.guild.id, ctx.bot.config): return + await self.queue_or_send(ctx, + "This command has been disabled as it's unclear how the new changes might affect this system.") + return msg = "List of new Dates:\n" curr_time = datetime.now(timezone.utc) @@ -566,9 +573,9 @@ async def get_ladder_info(self): msg = "" try: async with aiohttp.ClientSession( - timeout = timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: + timeout=timeout, + auth=aiohttp.BasicAuth( + "username", "password")) as session: async with session.get(url, headers=headers) as resp: if resp.status != 200: raise Exception( @@ -604,38 +611,34 @@ async def has_roles(self, member: discord.Member, guild_id: int, config): return True return False - async def end_voting(self): + @staticmethod + async def end_voting(mogi: Mogi): """Ends voting in all rooms with ongoing votes.""" try: - index = 0 - for mogi in self.ongoing_events.values(): - index += 1 - for room in mogi.rooms: + if mogi is not None: + for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: - print( - f"Skipping room {index} in function end_voting.", flush=True) + print(f"Skipping room {index} in function end_voting.", flush=True) continue await room.view.find_winner() except Exception as e: print(traceback.format_exc()) - async def write_history(self): + @staticmethod + async def write_history(mogi: Mogi, history_channel: discord.TextChannel): """Writes the teams, tier and average of each room per hour.""" try: - index = 0 - for mogi in self.ongoing_events.values(): - index += 1 - await self.HISTORY_CHANNEL.send(f"{discord.utils.format_dt(mogi.start_time)} Rooms") - for room in mogi.rooms: + if mogi is not None: + await history_channel.send(f"{discord.utils.format_dt(mogi.start_time)} Rooms") + for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: - print( - f"Skipping room {index} in function write_history.", flush=True) + print(f"Skipping room {index} in function write_history.", flush=True) continue msg = room.view.header_text msg += f"{room.thread.jump_url}\n" msg += room.view.teams_text msg += "ㅤ" - await self.HISTORY_CHANNEL.send(msg) + await history_channel.send(msg) except Exception as e: print(traceback.format_exc()) @@ -654,8 +657,8 @@ async def check_room_channels(self, mogi): room_name = f"{mogi.start_time.month}/{mogi.start_time.day}, {mogi.start_time.hour}:{minute}:00 - Room {i+1}" try: room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, - auto_archive_duration=60, - invitable=False) + auto_archive_duration=60, + invitable=False) except Exception as e: print(traceback.format_exc()) err_msg = f"\nAn error has occurred while creating a room channel:\n{e}" @@ -663,6 +666,12 @@ async def check_room_channels(self, mogi): return mogi.rooms.append(Room(None, i+1, room_channel)) + @staticmethod + async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): + await asyncio.sleep(120) + await SquadQueue.end_voting(mogi) + await SquadQueue.write_history(mogi, history_channel) + # add teams to the room threads that we have already created async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=False): if open_time >= 60 or open_time < 0: @@ -672,9 +681,8 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F return num_rooms = int(mogi.count_registered() / (12/mogi.size)) if num_rooms == 0: + self.ongoing_event = None await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") - self.scheduled_events = {} - self.ongoing_events = {} return await self.lockdown(mogi.mogi_channel) mogi.making_rooms_run = True @@ -760,44 +768,46 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F await mogi.mogi_channel.send(msg) except Exception as e: print("Late Player message has failed to send.", flush=True) - await asyncio.sleep(120) - await self.end_voting() - await self.write_history() print(traceback.format_exc()) - async def check_num_teams(self, mogi): + asyncio.create_task(SquadQueue.handle_voting_and_history(self.ongoing_event, self.HISTORY_CHANNEL)) + self.old_events.append(self.ongoing_event) + self.ongoing_event = None + + async def check_num_teams(self, mogi: Mogi): if not mogi.gathering or not mogi.is_automated: return cur_time = datetime.now(timezone.utc) - if mogi.start_time - self.QUEUE_OPEN_TIME + self.JOINING_TIME <= cur_time: - numLeftoverTeams = mogi.count_registered() % int((12/mogi.size)) - if numLeftoverTeams == 0: + if mogi.start_time <= cur_time: + num_leftover_teams = mogi.count_registered() % int((12 / mogi.size)) + if num_leftover_teams == 0: mogi.gathering = False await self.lockdown(mogi.mogi_channel) - await mogi.mogi_channel.send("A sufficient amount of players has been reached, so the mogi has been closed to extra players. Rooms will be made within the next minute.") + await mogi.mogi_channel.send( + "A sufficient amount of players has been reached, so the mogi has been closed to extra players. Rooms will be made within the next minute.") async def ongoing_mogi_checks(self): - for mogi in self.ongoing_events.values(): + mogi = self.ongoing_event + if mogi is not None: # If it's not automated, not started, we've already started making the rooms, don't run this async with self.LOCK: if not mogi.is_automated or not mogi.started or mogi.making_rooms_run: return cur_time = datetime.now(timezone.utc) - if (mogi.start_time - self.QUEUE_OPEN_TIME + self.JOINING_TIME + self.EXTENSION_TIME) <= cur_time: + if (mogi.start_time + self.EXTENSION_TIME) <= cur_time: mogi.gathering = False - if mogi.start_time - self.QUEUE_OPEN_TIME + self.JOINING_TIME <= cur_time and mogi.gathering: + elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - numLeftoverTeams = mogi.count_registered() % int((12/mogi.size)) - if numLeftoverTeams == 0: + num_leftover_teams = mogi.count_registered() % int((12 / mogi.size)) + if num_leftover_teams == 0: mogi.gathering = False else: if int(cur_time.second / 20) == 0: - force_time = mogi.start_time - self.QUEUE_OPEN_TIME + \ - self.JOINING_TIME + self.EXTENSION_TIME - minutes_left = int( - (force_time - cur_time).seconds/60) - x_teams = int(int(12/mogi.size) - numLeftoverTeams) - await mogi.mogi_channel.send(f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") + force_time = mogi.start_time + self.EXTENSION_TIME + minutes_left = (force_time - cur_time).seconds // 60 + x_teams = int(int(12 / mogi.size) - num_leftover_teams) + await mogi.mogi_channel.send( + f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") if not mogi.gathering: await self.delete_list_messages(0) await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") @@ -805,26 +815,25 @@ async def ongoing_mogi_checks(self): async def scheduler_mogi_start(self): cur_time = datetime.now(timezone.utc) - for guild in self.scheduled_events.values(): - to_remove = [] # Keep a list of indexes to remove - can't remove while iterating - for i, mogi in enumerate(guild): - if (mogi.start_time - self.QUEUE_OPEN_TIME) < cur_time: - if mogi.mogi_channel in self.ongoing_events.keys() and self.ongoing_events[mogi.mogi_channel].gathering: - to_remove.append(i) - await mogi.mogi_channel.send(f"Because there is an ongoing event right now, the following event has been removed:\n{self.get_event_str(mogi)}\n") - else: - if mogi.mogi_channel in self.ongoing_events.keys(): - if self.ongoing_events[mogi.mogi_channel].started: - self.old_events[mogi.mogi_channel] = self.ongoing_events[mogi.mogi_channel] - del self.ongoing_events[mogi.mogi_channel] - to_remove.append(i) - mogi.started = True - mogi.gathering = True - self.ongoing_events[mogi.mogi_channel] = mogi - await self.unlockdown(mogi.mogi_channel) - await mogi.mogi_channel.send(f"A queue is gathering for the mogi {discord.utils.format_dt(mogi.start_time, style='R')} - Type `/c` to join, and `/d` to drop.") - for ind in reversed(to_remove): - del guild[ind] + next_mogi = self.next_event + if next_mogi is not None and (next_mogi.start_time - self.JOINING_TIME) < cur_time: + self.next_event = None # We are trying to launch the event - fail or not, we set next event to None + # There is an ongoing event, but people are still queueing, so remove it and fail + if self.ongoing_event is not None and self.ongoing_event.gathering: + await next_mogi.mogi_channel.send( + f"Because there is an ongoing event right now, the following event has been removed:\n{self.get_event_str(next_mogi)}\n") + return + # There is an ongoing event, but it has already started, so add it to the old events + if self.ongoing_event is not None and self.ongoing_event.started: # This is potentially an issue - we should be checking if make rooms has been run, not if it's started...? + self.old_events.append(self.ongoing_event) + + # Put the next mogi as the current event and launch it + self.ongoing_event = next_mogi + self.ongoing_event.started = True + self.ongoing_event.gathering = True + await self.unlockdown(self.ongoing_event.mogi_channel) + await self.ongoing_event.mogi_channel.send( + f"A queue is gathering for the mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, and `/d` to drop.") @tasks.loop(seconds=20.0) async def sqscheduler(self): @@ -846,42 +855,34 @@ async def sqscheduler(self): except Exception as e: print(traceback.format_exc()) + def compute_next_event_open_time(self): + cur_time = datetime.now(timezone.utc) + time_elapsed = cur_time - self.FIRST_EVENT_TIME + num_launched_events = time_elapsed // self.QUEUE_OPEN_TIME + next_event_open_time = self.FIRST_EVENT_TIME + (self.QUEUE_OPEN_TIME * num_launched_events) + return next_event_open_time async def schedule_que_event(self): - """Schedules queue for the next hour in the given channel.""" - + """Schedules next queue in the SquadQueue mogi queueing channel.""" if self.GUILD is not None: - curr_time = datetime.now(timezone.utc) - start_time = curr_time + timedelta(minutes=65) - start_time = start_time.replace(minute=0, second=0, microsecond=0) - start_time -= self.QUEUE_OPEN_TIME - if self.prev_start_time: - start_time = self.prev_start_time - next_mogi_start = start_time + self.QUEUE_OPEN_TIME - if len(self.sq_times) > 0 and next_mogi_start == self.sq_times[0]: - self.sq_times.pop(0) - self.QUEUE_TIME_BLOCKER = next_mogi_start - await self.MOGI_CHANNEL.send("Squad Queue is currently going on at this hour! The queue will remain closed.") - if curr_time < self.QUEUE_TIME_BLOCKER: - # print(f"Mogi had been blocked from starting before the time limit {self.QUEUE_TIME_BLOCKER}", flush=True) + if not self.LAUNCH_NEW_EVENTS: + # (f"Not allowed to launch new events.", flush=True) return - for mogi in self.ongoing_events.values(): - if mogi.start_time == next_mogi_start: - return - - mogi = Mogi(1, 1, self.MOGI_CHANNEL, is_automated=True, - start_time=next_mogi_start) - - if self.GUILD not in self.scheduled_events.keys(): - self.scheduled_events[self.GUILD] = [] - - self.scheduled_events[self.GUILD].append(mogi) - - self.QUEUE_TIME_BLOCKER = next_mogi_start + next_event_open_time = self.compute_next_event_open_time() + next_event_start_time = next_event_open_time + self.JOINING_TIME + print(f"Next event open time: {next_event_open_time}", flush=True) + # We don't want to schedule the next event if it would open after it's joining period and during its extension period + if next_event_start_time < datetime.now(timezone.utc): + return + if len(self.sq_times) > 0 and next_event_open_time == self.sq_times[0]: + self.sq_times.pop(0) + await self.MOGI_CHANNEL.send( + "Squad Queue is currently going on at this hour! The queue will remain closed.") - self.prev_start_time = next_mogi_start + self.next_event = Mogi(1, 1, self.MOGI_CHANNEL, is_automated=True, + start_time=next_event_start_time) - print(f"Started Queue for {next_mogi_start}", flush=True) + print(f"Started Queue for {next_event_start_time}", flush=True) @tasks.loop(minutes=1) async def delete_old_mogis(self): @@ -889,14 +890,10 @@ async def delete_old_mogis(self): try: curr_time = datetime.now(timezone.utc) mogi_lifetime = timedelta(minutes=self.MOGI_LIFETIME) - delete_queue = [] - for mogi in self.old_events.values(): - if curr_time - mogi_lifetime > mogi.start_time: - delete_queue.append(mogi) + delete_queue = [m for m in self.old_events if (curr_time - mogi_lifetime) > m.start_time] for mogi in delete_queue: - print( - f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) - del self.old_events[mogi.mogi_channel] + print(f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) + self.old_events.remove(mogi) except Exception as e: print(traceback.format_exc()) @@ -970,15 +967,13 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem @commands.command(name="debug_start_rooms") @commands.is_owner() async def debug_start_rooms(self, ctx): - for mogi in self.ongoing_events.values(): - if mogi.start_time == self.prev_start_time: - await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) - return - for mogi in self.old_events.values(): - if mogi.start_time == self.prev_start_time: - await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) - return - + cur_mogi = self.ongoing_event + if cur_mogi is not None: + await self.add_teams_to_rooms(cur_mogi, (cur_mogi.start_time.minute) % 60, True) + return + for old_mogi in self.old_events: + await self.add_teams_to_rooms(old_mogi, (old_mogi.start_time.minute) % 60, True) + return async def setup(bot): From d72518b6841088a5c3eb6aaaaa825e4fa0e9d3e4 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 03:42:38 -0500 Subject: [PATCH 041/292] Minor refactor --- cogs/SquadQueue.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b00508a..4f67e37 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -188,14 +188,12 @@ def is_staff(self, member: discord.Member): async def is_started(self, ctx, mogi: Mogi): if not mogi.started: await ctx.send("Mogi has not been started yet... type !start") - return False - return True + return mogi.started async def is_gathering(self, ctx, mogi: Mogi): if not mogi.gathering: await ctx.send("Mogi is closed; players cannot join or drop from the event") - return False - return True + return mogi.gathering @app_commands.command(name="c") @app_commands.guild_only() From e625337be5cb1d9405604a9e49146367ae0446bd Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 18:26:35 -0500 Subject: [PATCH 042/292] Add comments --- cogs/SquadQueue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 4f67e37..a69882a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -666,7 +666,7 @@ async def check_room_channels(self, mogi): @staticmethod async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): - await asyncio.sleep(120) + await asyncio.sleep(120) # We could have used asyncio.call_later(120, handle_voting_and_history) in the caller's code await SquadQueue.end_voting(mogi) await SquadQueue.write_history(mogi, history_channel) @@ -767,7 +767,9 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F except Exception as e: print("Late Player message has failed to send.", flush=True) print(traceback.format_exc()) - + + # We could have used asyncio.call_later(120, handle_voting_and_history) + # and removed asyncio.sleep(120) in handle_voting_and_history asyncio.create_task(SquadQueue.handle_voting_and_history(self.ongoing_event, self.HISTORY_CHANNEL)) self.old_events.append(self.ongoing_event) self.ongoing_event = None From e8c1fa5522b81adb5b89c99135485c7099c8c747 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 19:53:33 -0500 Subject: [PATCH 043/292] Remove irrelevant code Variables were not used --- cogs/SquadQueue.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 4f67e37..1ccf819 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -688,12 +688,6 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F mogi.gathering = False await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") - pen_time = open_time + 5 - start_time = open_time + 10 - while pen_time >= 60: - pen_time -= 60 - while start_time >= 60: - start_time -= 60 teams_per_room = int(12/mogi.size) num_teams = int(num_rooms * teams_per_room) final_list = mogi.confirmed_list()[0:num_teams] From 428af11c2cecba9452c7388b733a632d349930f6 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:09:53 -0500 Subject: [PATCH 044/292] Mogi.size -> Mogi.players_per_team + add parameter to Mogi that keeps track of teams per room --- cogs/SquadQueue.py | 16 ++++++++++------ mogi_objects.py | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1ccf819..7c91b70 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -644,7 +644,7 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): # since discord only allows 50 thread channels to be created per 5 minutes. async def check_room_channels(self, mogi): num_teams = mogi.count_registered() - num_rooms = int(num_teams / (12/mogi.size)) + num_rooms = int(num_teams / (12 / mogi.players_per_team)) num_created_rooms = len(mogi.rooms) if num_created_rooms >= num_rooms: return @@ -677,7 +677,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F return if mogi.making_rooms_run and started_automatically: return - num_rooms = int(mogi.count_registered() / (12/mogi.size)) + num_rooms = int(mogi.count_registered() / (12 / mogi.players_per_team)) if num_rooms == 0: self.ongoing_event = None await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") @@ -688,7 +688,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F mogi.gathering = False await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") - teams_per_room = int(12/mogi.size) + teams_per_room = int(12 / mogi.players_per_team) num_teams = int(num_rooms * teams_per_room) final_list = mogi.confirmed_list()[0:num_teams] sorted_list = sorted(final_list, reverse=True) @@ -790,14 +790,14 @@ async def ongoing_mogi_checks(self): mogi.gathering = False elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % int((12 / mogi.size)) + num_leftover_teams = mogi.count_registered() % int((12 / mogi.players_per_team)) if num_leftover_teams == 0: mogi.gathering = False else: if int(cur_time.second / 20) == 0: force_time = mogi.start_time + self.EXTENSION_TIME minutes_left = (force_time - cur_time).seconds // 60 - x_teams = int(int(12 / mogi.size) - num_leftover_teams) + x_teams = int(int(12 / mogi.players_per_team) - num_leftover_teams) await mogi.mogi_channel.send( f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") if not mogi.gathering: @@ -871,7 +871,11 @@ async def schedule_que_event(self): await self.MOGI_CHANNEL.send( "Squad Queue is currently going on at this hour! The queue will remain closed.") - self.next_event = Mogi(1, 1, self.MOGI_CHANNEL, is_automated=True, + self.next_event = Mogi(sq_id=1, + players_per_team=1, + teams_per_room=12, + mogi_channel=self.MOGI_CHANNEL, + is_automated=True, start_time=next_event_start_time) print(f"Started Queue for {next_event_start_time}", flush=True) diff --git a/mogi_objects.py b/mogi_objects.py index 3cdb3d5..b17593a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -7,14 +7,14 @@ class Mogi: - def __init__(self, sq_id: int, size: int, mogi_channel: discord.TextChannel, + def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_channel: discord.TextChannel, is_automated=False, start_time=None): self.started = False self.gathering = False self.making_rooms_run = False self.making_rooms_run_time = None self.sq_id = sq_id - self.size = size + self.players_per_team = players_per_team self.mogi_channel = mogi_channel self.teams: List[Team] = [] self.rooms: List[Room] = [] From 31ea0919f49cce59a98b1378acd3acd1699cae40 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:10:09 -0500 Subject: [PATCH 045/292] See last commit --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7c91b70..227c44f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -771,7 +771,7 @@ async def check_num_teams(self, mogi: Mogi): return cur_time = datetime.now(timezone.utc) if mogi.start_time <= cur_time: - num_leftover_teams = mogi.count_registered() % int((12 / mogi.size)) + num_leftover_teams = mogi.count_registered() % int((12 / mogi.players_per_team)) if num_leftover_teams == 0: mogi.gathering = False await self.lockdown(mogi.mogi_channel) From 919d5f18f285e2e7463fffb0b65cadd600f6b2cf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:10:23 -0500 Subject: [PATCH 046/292] See last commit --- mogi_objects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mogi_objects.py b/mogi_objects.py index b17593a..2e00158 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -15,6 +15,7 @@ def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_ self.making_rooms_run_time = None self.sq_id = sq_id self.players_per_team = players_per_team + self.teams_per_room = teams_per_room self.mogi_channel = mogi_channel self.teams: List[Team] = [] self.rooms: List[Room] = [] From 2c704d343885628c5653d501e9ef69647320c1bf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:11:53 -0500 Subject: [PATCH 047/292] Minor refactor for readability --- cogs/SquadQueue.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 227c44f..76d269b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -213,23 +213,21 @@ async def can(self, interaction: discord.Interaction): await interaction.followup.send(f"{interaction.user.mention} is already signed up.") return - players = [] msg = "" placement_role_id = 723753340063842345 if member.get_role(placement_role_id): starting_player_mmr = 750 - player = Player(member, member.display_name, starting_player_mmr) - players.append(player) + players = [Player(member, member.display_name, starting_player_mmr)] msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" else: players = await mkw_mmr(self.URL, [member], self.TRACK_TYPE) - if len(players) == 0 or players[0] is None: - msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " - msg += "Please try again. If the problem continues then contact a staff member for help." - await interaction.followup.send(msg) - return + if len(players) == 0 or players[0] is None: + msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " + msg += "Please try again. If the problem continues then contact a staff member for help." + await interaction.followup.send(msg) + return players[0].confirmed = True squad = Team(players) From 64b204bc6d0f3a17caaf8617344354a8fa54abde Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:12:05 -0500 Subject: [PATCH 048/292] Minor refactor for readability --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 76d269b..f08b30d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -895,7 +895,7 @@ def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( mogi.start_time, style="R") - return (f"`#{mogi.sq_id}` **{mogi.size}v{mogi.size}:** {mogi_time} - {mogi_time_relative}") + return f"`#{mogi.sq_id}` **{mogi.players_per_team}v{mogi.players_per_team}:** {mogi_time} - {mogi_time_relative}" @commands.command(name="sync") @commands.is_owner() From cd3499e0b444b56b678b0761e2bf22ec6556c299 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:12:22 -0500 Subject: [PATCH 049/292] Type hint minor refactor --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index f08b30d..7362a97 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -669,7 +669,7 @@ async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextCha await SquadQueue.write_history(mogi, history_channel) # add teams to the room threads that we have already created - async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=False): + async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatically=False): if open_time >= 60 or open_time < 0: await mogi.mogi_channel.send("Please specify a valid time (in minutes) for rooms to open (00-59)") return From 78551e3f7a46d9e050e4fad18b969ea56a4dbfbf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:12:47 -0500 Subject: [PATCH 050/292] Added common properties to Mogi --- mogi_objects.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 2e00158..606fa53 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -22,6 +22,21 @@ def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_ self.is_automated = is_automated self.start_time = start_time if is_automated else None + @property + def num_players(self): + """Returns the total number of players in teams where all players have confirmed""" + return self.num_teams * self.players_per_team + + @property + def num_teams(self): + """Returns the total number of teams where all players have confirmed""" + return self.count_registered() + + @property + def num_rooms(self): + """Returns the number of rooms based on teams where all players have confirmed""" + return self.num_teams // self.teams_per_room + def check_player(self, member): for team in self.teams: if team.has_player(member): From b0bce80ab6317c768263bdec7e2860beea39f085 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:13:05 -0500 Subject: [PATCH 051/292] Added avg_mmr property to Team --- mogi_objects.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 606fa53..a8bb3b0 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -89,10 +89,10 @@ def get_player_list(self): class Team: def __init__(self, players): self.players = players - self.avg_mmr = sum([p.mmr for p in self.players]) / len(self.players) - def recalc_avg(self): - self.avg_mmr = sum([p.mmr for p in self.players]) / len(self.players) + @property + def avg_mmr(self): + return sum([p.mmr for p in self.players]) / len(self.players) def is_registered(self): """Returns if all players on the team are registered""" @@ -114,7 +114,6 @@ def sub_player(self, sub_out, sub_in): for i, player in enumerate(self.players): if player == sub_out: self.players[i] = sub_in - self.recalc_avg() return def num_confirmed(self): From bdc430d77a19117dda87ff1766fb48d45c0d6028 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:13:37 -0500 Subject: [PATCH 052/292] int * int results in int - no need to cast --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7362a97..cec86d9 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -707,7 +707,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic for i in range(num_rooms): msg = f"`Room {i+1} - Player List`\n" mentions = "" - start_index = int(i*teams_per_room) + start_index = i*teams_per_room player_list = [] for j in range(teams_per_room): msg += f"`{j+1}.` " From 32d3e9dd785118464eb1ea336a7821a8b5bba15c Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:19:22 -0500 Subject: [PATCH 053/292] Addresses race condition Someone could queue or drop during execution of lockdown call, and, after it was finished, we might not actually have enough rooms, throwing the rest of code after into question. --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index cec86d9..ae95c6e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -680,11 +680,12 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic self.ongoing_event = None await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") return - await self.lockdown(mogi.mogi_channel) + mogi.making_rooms_run = True if mogi.gathering: mogi.gathering = False await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") + await self.lockdown(mogi.mogi_channel) teams_per_room = int(12 / mogi.players_per_team) num_teams = int(num_rooms * teams_per_room) From 20a60be7938ebf06f2fb1183353d3a184f376fc4 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 22 Jun 2024 23:22:29 -0500 Subject: [PATCH 054/292] Modification of last commit to make lockdown run before message --- cogs/SquadQueue.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ae95c6e..92622f1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -681,11 +681,14 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") return + was_gathering = mogi.gathering mogi.making_rooms_run = True - if mogi.gathering: - mogi.gathering = False - await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") + mogi.gathering = False + await self.lockdown(mogi.mogi_channel) + if was_gathering: + await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") + teams_per_room = int(12 / mogi.players_per_team) num_teams = int(num_rooms * teams_per_room) From 9badb9d648f609671772607a1a0033cfbcf08585 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Mon, 24 Jun 2024 17:36:26 -0700 Subject: [PATCH 055/292] Reformatted List Message Changed the position of the Queue Closing Time in the List Channel. --- cogs/SquadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a69882a..efe1164 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -343,9 +343,9 @@ async def list_task(self): late_players.append(player) sorted_mogi_list = sorted(mogi_list, reverse=True) - msg = f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" + msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" + msg += f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" msg += "**Current Mogi List:**\n\n" - msg += f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n" for i in range(len(sorted_mogi_list)): msg += f"{i + 1}) " msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) From a5e423fb415d2d0b838506c766fcbd3b21673b2c Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Tue, 25 Jun 2024 10:12:11 -0700 Subject: [PATCH 056/292] Added missing argument in sub command --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index efe1164..6dcecf5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -316,7 +316,7 @@ async def sub(self, interaction: discord.Interaction): timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = JoinView(room, get_mmr_from_discord_id, bottom_room_num) + view = JoinView(room, lambda discord_id: get_mmr_from_discord_id(discord_id, self.TRACK_TYPE), bottom_room_num) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") From 925e1ae39e14a85bf76f1117c14560010291eaf4 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Tue, 25 Jun 2024 19:56:25 -0500 Subject: [PATCH 057/292] Add saving mmr threshold --- cogs/SquadQueue.py | 13 +++++++++++++ requirements.txt | 1 + 2 files changed, 14 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6dcecf5..421ee38 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -14,12 +14,15 @@ from collections import defaultdict from typing import Dict, List import traceback +import os +import dill headers = {'Content-type': 'application/json'} # Scheduled_Event = collections.namedtuple('Scheduled_Event', 'size time started mogi_channel') cooldowns = defaultdict(int) +MMR_THRESHOLD_PKL = "mmr_threshold.pkl" class SquadQueue(commands.Cog): def __init__(self, bot): @@ -65,6 +68,11 @@ def __init__(self, bot): self.SUB_MESSAGE_LIFETIME_SECONDS = bot.config["SUB_MESSAGE_LIFETIME_SECONDS"] self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] + if os.path.isfile(MMR_THRESHOLD_PKL): + try: + self.room_mmr_threshold = dill.load('name_model.pkl') + except: + print(traceback.format_exc()) self.TRACK_TYPE = bot.config["track_type"] @@ -481,6 +489,11 @@ async def change_event_time(self, interaction: discord.Interaction, event_time: async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_threshold: int): """Change the maximum MMR gap allowed for a room.""" self.room_mmr_threshold = mmr_threshold + try: + with open(MMR_THRESHOLD_PKL, 'wb') as f: + dill.dump(self.room_mmr_threshold, f) + except: + print(traceback.format_exc()) await interaction.response.send_message(f"MMR Threshold for Queue Rooms has been modified to {mmr_threshold} MMR.") @app_commands.command(name="peek_bot_config") diff --git a/requirements.txt b/requirements.txt index 5724417..bfbed65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ aiohttp==3.8.4 discord.py==2.3.2 python-dateutil==2.8.2 +dill~=0.3.8 From 1eaafc81275c7f033163a5ed963b5081eb411568 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Tue, 25 Jun 2024 20:04:29 -0500 Subject: [PATCH 058/292] Bug fix --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 421ee38..1ecd171 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -70,7 +70,8 @@ def __init__(self, bot): self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] if os.path.isfile(MMR_THRESHOLD_PKL): try: - self.room_mmr_threshold = dill.load('name_model.pkl') + with open(MMR_THRESHOLD_PKL, 'rb') as f: + self.room_mmr_threshold = dill.load(f) except: print(traceback.format_exc()) From 65db6d8dd5c6167a9d58ff520f8fcc5803775077 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Tue, 25 Jun 2024 22:59:41 -0500 Subject: [PATCH 059/292] Update configs with pingable help roles --- ct_config.json | 5 +++++ prod_config.json | 5 +++++ test_config.json | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/ct_config.json b/ct_config.json index 3a777d7..7b2759f 100644 --- a/ct_config.json +++ b/ct_config.json @@ -39,6 +39,11 @@ 792891432301625364, 877629421811540016 ], + "helper_staff_roles": [ + 520808674411937792, + 792891432301625364, + 521149807994208295 + ], "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", diff --git a/prod_config.json b/prod_config.json index bf37127..23bc531 100644 --- a/prod_config.json +++ b/prod_config.json @@ -39,6 +39,11 @@ 792891432301625364, 877629421811540016 ], + "helper_staff_roles": [ + 389252697284542465, + 399384750923579392, + 399382503825211393 + ], "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", diff --git a/test_config.json b/test_config.json index 588af53..3239a78 100644 --- a/test_config.json +++ b/test_config.json @@ -38,6 +38,11 @@ 792891432301625364, 877629421811540016 ], + "helper_staff_roles": [ + 389252697284542465, + 399384750923579392, + 399382503825211393 + ], "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", From 64d045c95b98c92fefc618cee214399f93f8d75d Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:09:44 -0500 Subject: [PATCH 060/292] Add muted and restricted roles to configs --- ct_config.json | 2 ++ prod_config.json | 2 ++ test_config.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ct_config.json b/ct_config.json index 7b2759f..7c0c6ca 100644 --- a/ct_config.json +++ b/ct_config.json @@ -44,6 +44,8 @@ 792891432301625364, 521149807994208295 ], + "restricted_role_id": 797208908153618452, + "muted_role_id": 434887701662007296, "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", diff --git a/prod_config.json b/prod_config.json index 23bc531..3aa7fd3 100644 --- a/prod_config.json +++ b/prod_config.json @@ -44,6 +44,8 @@ 399384750923579392, 399382503825211393 ], + "restricted_role_id": 797208908153618452, + "muted_role_id": 434887701662007296, "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", diff --git a/test_config.json b/test_config.json index 3239a78..cce5ecd 100644 --- a/test_config.json +++ b/test_config.json @@ -43,6 +43,8 @@ 399384750923579392, 399382503825211393 ], + "restricted_role_id": 797208908153618452, + "muted_role_id": 434887701662007296, "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", From 084095ac28d0d26e3780e225b0f2a43286363979 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:11:05 -0500 Subject: [PATCH 061/292] Added type hints --- cogs/SquadQueue.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1ecd171..c65dee1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -26,7 +26,7 @@ class SquadQueue(commands.Cog): def __init__(self, bot): - self.bot = bot + self.bot: commands.Bot = bot self.next_event: Mogi = None @@ -47,7 +47,7 @@ def __init__(self, bot): self.LAUNCH_NEW_EVENTS = True - self.GUILD = None + self.GUILD: discord.Guild = None self.MOGI_CHANNEL = None @@ -231,7 +231,7 @@ async def can(self, interaction: discord.Interaction): players.append(player) msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" - else: + else: players = await mkw_mmr(self.URL, [member], self.TRACK_TYPE) if len(players) == 0 or players[0] is None: @@ -291,7 +291,7 @@ async def sub(self, interaction: discord.Interaction): f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", ephemeral=True) return - + is_room_thread = False room = None bottom_room_num = 1 @@ -472,7 +472,7 @@ async def resume_mogi_scheduling(self, interaction: discord.Interaction): """Mogis will begin to be scheduled again. Staff use only.""" self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("Mogis will resume scheduling.") - + @app_commands.command(name="change_event_time") @app_commands.guild_only() async def change_event_time(self, interaction: discord.Interaction, event_time: int): @@ -570,7 +570,7 @@ async def clear_sq_times(self, interaction: discord.Interaction): self.sq_times = [] await interaction.response.send_message("Cleared list of Squad Queue Times.") - + @app_commands.command(name="update_tier_info") @app_commands.guild_only() async def update_tier_info(self, interaction: discord.Interaction): @@ -781,7 +781,7 @@ async def add_teams_to_rooms(self, mogi, open_time: int, started_automatically=F except Exception as e: print("Late Player message has failed to send.", flush=True) print(traceback.format_exc()) - + # We could have used asyncio.call_later(120, handle_voting_and_history) # and removed asyncio.sleep(120) in handle_voting_and_history asyncio.create_task(SquadQueue.handle_voting_and_history(self.ongoing_event, self.HISTORY_CHANNEL)) @@ -990,7 +990,7 @@ async def debug_start_rooms(self, ctx): return -async def setup(bot): +async def setup(bot: commands.Bot): await bot.add_cog(SquadQueue(bot)) From e552a1f4f43ab0c75e045fca8356f93cc981f1f5 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:11:24 -0500 Subject: [PATCH 062/292] Added cooldown handler for slash commands --- lounge.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lounge.py b/lounge.py index 5fe2f82..b024cb4 100644 --- a/lounge.py +++ b/lounge.py @@ -50,6 +50,13 @@ async def on_command_error(ctx, error): raise error +@bot.tree.error +async def on_app_command_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError): + if isinstance(error, discord.app_commands.errors.CommandOnCooldown): + await interaction.response.send_message(f'You are on cooldown. Try again in {round(error.retry_after)}s', ephemeral=True) + else: + raise error + @bot.event async def setup_hook(): for extension in initial_extensions: From 8f8d202181b7aaac61b2b80def78e2f71d3da938 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:12:57 -0500 Subject: [PATCH 063/292] More type hints --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 3cdb3d5..786f25e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -271,7 +271,7 @@ def __init__(self, room: Room, get_mmr, bottom_room_num): self.bottom_room_num = bottom_room_num @discord.ui.button(label="Join Room") - async def button_callback(self, interaction, button): + async def button_callback(self, interaction: discord.Interaction, button): await interaction.response.defer() muted_role_id = 434887701662007296 restricted_role_id = 797208908153618452 From 4ef91a191371f5568c38c19600035c9044d0aaf9 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:13:49 -0500 Subject: [PATCH 064/292] Changed restricted/muted role check for subs --- cogs/SquadQueue.py | 13 ++++++++++++- mogi_objects.py | 11 ++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c65dee1..2e1acc1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -24,6 +24,14 @@ cooldowns = defaultdict(int) MMR_THRESHOLD_PKL = "mmr_threshold.pkl" + +def is_restricted(user: discord.User | discord.Member, config: dict) -> bool: + muted_role_id = config.get("muted_role_id") + restricted_role_id = config.get("restricted_role_id") + return (muted_role_id is not None and user.get_role(muted_role_id)) \ + or (restricted_role_id is not None and user.get_role(restricted_role_id)) + + class SquadQueue(commands.Cog): def __init__(self, bot): self.bot: commands.Bot = bot @@ -325,7 +333,10 @@ async def sub(self, interaction: discord.Interaction): timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = JoinView(room, lambda discord_id: get_mmr_from_discord_id(discord_id, self.TRACK_TYPE), bottom_room_num) + view = JoinView(room, + lambda discord_id: get_mmr_from_discord_id(discord_id, self.TRACK_TYPE), + bottom_room_num, + lambda user: is_restricted(user, self.bot.config)) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") diff --git a/mogi_objects.py b/mogi_objects.py index 786f25e..0b0fac9 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import random import discord from datetime import datetime, timezone, timedelta import time from discord.ui import View -from typing import List +from typing import List, Callable class Mogi: @@ -264,18 +266,17 @@ async def general_vote_callback(self, interaction: discord.Interaction, players_ class JoinView(View): - def __init__(self, room: Room, get_mmr, bottom_room_num): + def __init__(self, room: Room, get_mmr, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): super().__init__(timeout=1200) self.room = room self.get_mmr = get_mmr self.bottom_room_num = bottom_room_num + self.is_restricted = is_restricted @discord.ui.button(label="Join Room") async def button_callback(self, interaction: discord.Interaction, button): await interaction.response.defer() - muted_role_id = 434887701662007296 - restricted_role_id = 797208908153618452 - if interaction.user.get_role(muted_role_id) or interaction.user.get_role(restricted_role_id): + if self.is_restricted is not None and self.is_restricted(interaction.user): await interaction.followup.send( "Players with the muted or restricted role cannot use the sub button.", ephemeral=True) return From 92ee1e6018010b950b01a7414cc2d431e8247076 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:16:36 -0500 Subject: [PATCH 065/292] Added /ping_staff command and role refresher - Ping staff command added, which has a user cooldown and muted/restricted role restriction. This command pings the selected staff role, which should add them to the thread. - Updated role names are pulled every 24 hours for the role IDs in case they have changed. --- cogs/SquadQueue.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 2e1acc1..1d63ff5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -106,6 +106,11 @@ def __init__(self, bot): with open('./timezones.json', 'r') as cjson: self.timezones = json.load(cjson) + # The following dictionary will be populated using the config's staff_helper_roles list. Players can + # call these roles into their room/thread using the /staff role command + # These will be refreshed every 24 hours to ensure that the correct name displays for the options + self.helper_staff_roles: Dict[str, discord.Role] = {} + @commands.Cog.listener() async def on_ready(self): self.GUILD = self.bot.get_guild(self.bot.config["guild_id"]) @@ -138,6 +143,7 @@ async def on_ready(self): print(f"General Channel - {self.GENERAL_CHANNEL}", flush=True) print("Ready!", flush=True) + self.refresh_helper_roles.start() async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -461,6 +467,29 @@ async def remove_player(self, interaction: discord.Interaction, member: discord. await interaction.followup.send(msg) + @app_commands.command(name="ping_staff") + @app_commands.guild_only() + @app_commands.checks.cooldown(1, 300, key=lambda i: i.user.id) + async def ping_staff(self, interaction: discord.Interaction, role: str): + """Pings the specified staff role for help.""" + if not isinstance(interaction.channel, discord.Thread): + await interaction.response.send_message(f"Cannot use this command here.", ephemeral=True) + return + if is_restricted(interaction.user, self.bot.config): + await interaction.response.send_message(f"You are restricted from using this command.", ephemeral=True) + return + if role not in self.helper_staff_roles: + await interaction.response.send_message(f"You are not allowed to ping this role for help. Valid roles to ping: `{', '.join(self.helper_staff_roles)}`", ephemeral=True) + return + await interaction.response.send_message(f"{self.helper_staff_roles[role].mention}, {interaction.user.mention} is requesting help.", allowed_mentions=discord.AllowedMentions(roles=True)) + + @ping_staff.autocomplete('role') + async def ping_staff_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + return [ + app_commands.Choice(name=role_name, value=role_name) + for role_name in self.helper_staff_roles if current.lower() in role_name.lower() + ] + @app_commands.command(name="annul_current_mogi") @app_commands.guild_only() async def annul_current_mogi(self, interaction: discord.Interaction): @@ -922,6 +951,28 @@ async def delete_old_mogis(self): except Exception as e: print(traceback.format_exc()) + @tasks.loop(hours=24) + async def refresh_helper_roles(self): + """Refreshes the helper staff role names for the /ping_staff command using the role IDs in the config""" + try: + helper_staff_role_ids = self.bot.config["helper_staff_roles"] + # In my experience, large servers experience caching issues. + # This is a forced API call which guarantees the role information will be up-to-date. + all_roles = await self.GUILD.fetch_roles() + updated_roles = {} + for role_id in helper_staff_role_ids: + needle: discord.Role = discord.utils.find(lambda n: role_id == n.id, all_roles) + if needle is not None: + updated_roles[needle.name] = needle + # As a fail safe, if we didn't add any new roles, either due to a misconfiguration or an internal discord + # issue, only update if we found one or more of the roles + if len(updated_roles) > 0: + # We use clear and update to ensure any *references* to the original dictionary are updated + self.helper_staff_roles.clear() + self.helper_staff_roles.update(updated_roles) + except Exception as e: + print(traceback.format_exc()) + def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( From 362a8f6dc48721201faf80904457f2b8fb095310 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Wed, 26 Jun 2024 00:24:25 -0500 Subject: [PATCH 066/292] Removed annoying print --- cogs/SquadQueue.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1d63ff5..3470e51 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -924,7 +924,6 @@ async def schedule_que_event(self): return next_event_open_time = self.compute_next_event_open_time() next_event_start_time = next_event_open_time + self.JOINING_TIME - print(f"Next event open time: {next_event_open_time}", flush=True) # We don't want to schedule the next event if it would open after it's joining period and during its extension period if next_event_start_time < datetime.now(timezone.utc): return From 640d4943cc4f4b9a76749941fcea666d0d86b6d2 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 16:54:44 -0500 Subject: [PATCH 067/292] Added server flag to configs --- ct_config.json | 2 ++ prod_config.json | 2 ++ test_config.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ct_config.json b/ct_config.json index 7c0c6ca..0c2d970 100644 --- a/ct_config.json +++ b/ct_config.json @@ -1,5 +1,7 @@ { "token": "", + "lounge": "MKW", + "lounge_description": "Valid options are MKW or MK8DX", "guild_id": 387347467332485122, "admin_roles": { "387347467332485122": [ diff --git a/prod_config.json b/prod_config.json index 3aa7fd3..682f7fe 100644 --- a/prod_config.json +++ b/prod_config.json @@ -1,5 +1,7 @@ { "token": "", + "lounge": "MKW", + "lounge_description": "Valid options are MKW or MK8DX", "guild_id": 387347467332485122, "admin_roles": { "387347467332485122": [ diff --git a/test_config.json b/test_config.json index cce5ecd..3f78a68 100644 --- a/test_config.json +++ b/test_config.json @@ -1,5 +1,7 @@ { "token": "", + "lounge": "MKW", + "lounge_description": "Valid options are MKW or MK8DX", "guild_id": 1228027010836594698, "admin_roles": { "1228027010836594698": [ From 515997e66e8c79a32e5fbb3e23b0b69b974348bf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 16:57:34 -0500 Subject: [PATCH 068/292] Add helper functions These functions help manipulate 2d arrays (lists) and chunk 1d array (list) into a 2d array (list) --- common.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 common.py diff --git a/common.py b/common.py new file mode 100644 index 0000000..cb65358 --- /dev/null +++ b/common.py @@ -0,0 +1,15 @@ +from typing import TypeVar, Type, List + +_T = TypeVar('T') + +def divide_chunks(list_: list, chunk_size: int): + # looping till length l + for i in range(0, len(list_), chunk_size): + yield list_[i:i + chunk_size] + + +def flatten(matrix: List[List[_T]]) -> List[_T]: + flat_list = [] + for row in matrix: + flat_list.extend(row) + return flat_list From 6039bc91d4f86efaa4345b753d6963be7c14df94 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 16:58:18 -0500 Subject: [PATCH 069/292] Load config into common file --- common.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common.py b/common.py index cb65358..9e6e508 100644 --- a/common.py +++ b/common.py @@ -1,7 +1,12 @@ +import json from typing import TypeVar, Type, List _T = TypeVar('T') +with open('./config.json', 'r') as cjson: + CONFIG = json.load(cjson) + + def divide_chunks(list_: list, chunk_size: int): # looping till length l for i in range(0, len(list_), chunk_size): From 9afe16bfd3f35a9f44b588e6688459212e002eb0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 16:58:36 -0500 Subject: [PATCH 070/292] Server enumeration --- common.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common.py b/common.py index 9e6e508..1c8ee9b 100644 --- a/common.py +++ b/common.py @@ -1,4 +1,5 @@ import json +from enum import Enum, auto from typing import TypeVar, Type, List _T = TypeVar('T') @@ -7,6 +8,19 @@ CONFIG = json.load(cjson) +class Server(Enum): + MKW = auto() + MK8DX = auto() + + +if CONFIG["lounge"] == "MKW": + SERVER = Server.MKW +elif CONFIG["lounge"] == "MK8DX": + SERVER = Server.MK8DX +else: + raise ValueError(f"{CONFIG['lounge']} is not a valid option for the 'lounge' attribute in the config.") + + def divide_chunks(list_: list, chunk_size: int): # looping till length l for i in range(0, len(list_), chunk_size): From 3b7861d5109e89dc78cc66944ab356852dadd140 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 16:59:30 -0500 Subject: [PATCH 071/292] Use config from common --- lounge.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lounge.py b/lounge.py index b024cb4..ff597be 100644 --- a/lounge.py +++ b/lounge.py @@ -1,3 +1,4 @@ +import common import discord from discord.ext import commands import json @@ -10,9 +11,7 @@ case_insensitive=True, intents=intents, help_command=None) initial_extensions = ['cogs.SquadQueue'] - -with open('./config.json', 'r') as cjson: - bot.config = json.load(cjson) +bot.config = common.config @bot.event From eaeea1c1f75a521cde9d08bc790a54280e3ffa00 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:01:16 -0500 Subject: [PATCH 072/292] Avoid using bare exception --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d1cbf80..fef0209 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -793,7 +793,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic view = VoteView(player_list, room_channel, mogi, self.TIER_INFO) curr_room.view = view await room_channel.send(view=view) - except Exception as e: + except discord.DiscordException as e: print(traceback.format_exc()) err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" err_msg += mentions From 4bc8e87ebd7bbb754e83c815a7e2dfd3b0019c23 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:01:29 -0500 Subject: [PATCH 073/292] Unnecessary code --- cogs/SquadQueue.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index fef0209..ee6fd70 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -795,10 +795,6 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic await room_channel.send(view=view) except discord.DiscordException as e: print(traceback.format_exc()) - err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" - err_msg += mentions - msg += err_msg - room_channel = None try: await mogi.mogi_channel.send(msg) except Exception as e: From e2464651a036bd08d06947bd4f6c95f86d1c6ebf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:01:50 -0500 Subject: [PATCH 074/292] Avoid using bare exceptions --- cogs/SquadQueue.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ee6fd70..ff9e050 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -797,9 +797,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic print(traceback.format_exc()) try: await mogi.mogi_channel.send(msg) - except Exception as e: - print( - f"Mogi Channel message for room {i+1} has failed to send.", flush=True) + except discord.DiscordException as e: + print(f"Mogi Channel message for room {i+1} has failed to send.", flush=True) print(traceback.format_exc()) if num_teams < mogi.count_registered(): missed_teams = mogi.confirmed_list( From d9fc9ec1f477d4b37f8798d778e1762687eaeac0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:27:03 -0500 Subject: [PATCH 075/292] Avoid using bare exceptions --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ff9e050..d5af09c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -793,12 +793,12 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic view = VoteView(player_list, room_channel, mogi, self.TIER_INFO) curr_room.view = view await room_channel.send(view=view) - except discord.DiscordException as e: + except discord.DiscordException: print(traceback.format_exc()) try: await mogi.mogi_channel.send(msg) - except discord.DiscordException as e: print(f"Mogi Channel message for room {i+1} has failed to send.", flush=True) + except discord.DiscordException: print(traceback.format_exc()) if num_teams < mogi.count_registered(): missed_teams = mogi.confirmed_list( @@ -810,7 +810,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic msg += f" ({int(missed_teams[i].avg_mmr)} MMR)\n" try: await mogi.mogi_channel.send(msg) - except Exception as e: + except discord.DiscordException: print("Late Player message has failed to send.", flush=True) print(traceback.format_exc()) From 21028c711f359bad1099e4e43bc9a533278072b0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:28:32 -0500 Subject: [PATCH 076/292] Import cleanup --- mogi_objects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 7dd8e5f..21ceaf3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -1,11 +1,12 @@ from __future__ import annotations +import common +from common import flatten import random import discord from datetime import datetime, timezone, timedelta -import time from discord.ui import View -from typing import List, Callable +from typing import List, Tuple, Callable class Mogi: From 425ec55eff19e11ba48efc46c2ff33888808477c Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:28:46 -0500 Subject: [PATCH 077/292] Helper function to compute averages --- mogi_objects.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 21ceaf3..b7af1ed 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -9,6 +9,11 @@ from typing import List, Tuple, Callable + +def average(list_: List[int | float]) -> float: + return sum(list_) / len(list_) + + class Mogi: def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_channel: discord.TextChannel, is_automated=False, start_time=None): From 27d33397364682d02d8f67bb0fa24e48e07fa1d5 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:31:28 -0500 Subject: [PATCH 078/292] Remove num_rooms property I decided that we should compute things differently --- mogi_objects.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index b7af1ed..90a1322 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -41,9 +41,6 @@ def num_teams(self): return self.count_registered() @property - def num_rooms(self): - """Returns the number of rooms based on teams where all players have confirmed""" - return self.num_teams // self.teams_per_room def check_player(self, member): for team in self.teams: From fd53843137a9a4c71eb161383e3a11dc47c4a01f Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:33:28 -0500 Subject: [PATCH 079/292] Team.is_registered -> Team.all_registered --- mogi_objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 90a1322..41dbb42 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -50,10 +50,10 @@ def check_player(self, member): def count_registered(self): """Returns the number of teams that are registered""" - return sum(1 for team in self.teams if team.is_registered()) + return sum(1 for team in self.teams if team.all_registered()) def confirmed_list(self): - return [team for team in self.teams if team.is_registered()] + return [team for team in self.teams if team.all_registered()] def remove_id(self, squad_id: int): confirmed = self.confirmed_list() @@ -99,7 +99,7 @@ def __init__(self, players): def avg_mmr(self): return sum([p.mmr for p in self.players]) / len(self.players) - def is_registered(self): + def all_registered(self): """Returns if all players on the team are registered""" return all(player.confirmed for player in self.players) From 660f2249a37c7132e05744140c080dd2e2dda10b Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:39:13 -0500 Subject: [PATCH 080/292] Refactor of mmr_high, mmr_low --- mogi_objects.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 41dbb42..4c9ab1f 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -81,12 +81,27 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.teams = teams self.room_num = room_num self.thread = thread - self.mmr_average = 0 - self.mmr_high = None - self.mmr_low = None self.view = None self.finished = False + @property + def mmr_high(self) -> int: + if self.teams is None: + return None + return max(self.players, key=lambda p: p.mmr).mmr + + @property + def mmr_low(self) -> int: + if self.teams is None: + return None + return min(self.players, key=lambda p: p.mmr).mmr + + @property + def avg_mmr(self) -> float: + if self.teams is None: + return 0 + return average([p.mmr for p in self.players]) + def get_player_list(self): return [player.member.id for team in self.teams for player in team.players] From cfffc7502eecf3efaa05f51cb8725a2328c169a0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:39:30 -0500 Subject: [PATCH 081/292] Add players property --- mogi_objects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 4c9ab1f..de836f8 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -102,6 +102,12 @@ def avg_mmr(self) -> float: return 0 return average([p.mmr for p in self.players]) + @property + def players(self) -> List[Player]: + if self.teams is None: + return [] + return flatten([t.players for t in self.teams]) + def get_player_list(self): return [player.member.id for team in self.teams for player in team.players] From 90acc6bfa4e69cfff8dfdf4246fa8067cc3fbcc4 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:40:13 -0500 Subject: [PATCH 082/292] Use average function --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index de836f8..c7ccdf2 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -118,7 +118,7 @@ def __init__(self, players): @property def avg_mmr(self): - return sum([p.mmr for p in self.players]) / len(self.players) + return average([p.mmr for p in self.players]) def all_registered(self): """Returns if all players on the team are registered""" From 97ed0a459c641b16efe452fca7840e7615e0713d Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:40:47 -0500 Subject: [PATCH 083/292] Refactor: simplify comparison chain --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index c7ccdf2..289a194 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -333,7 +333,7 @@ async def button_callback(self, interaction: discord.Interaction, button): self.room.mmr_high = 999999 if self.room.room_num == self.bottom_room_num: self.room.mmr_low = -999999 - if isinstance(user_mmr, int) and user_mmr < self.room.mmr_high + 500 and user_mmr > self.room.mmr_low - 500: + if isinstance(user_mmr, int) and self.room.mmr_high + 500 > user_mmr > self.room.mmr_low - 500: button.disabled = True await interaction.followup.edit_message(interaction.message.id, view=self) mention = interaction.user.mention From 6e259696820fb16e9aaa71892032f4a9b52b2966 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:42:16 -0500 Subject: [PATCH 084/292] Refactor of Player initialization This will allow the caller making the instance to immediately specify if the Player is confirmed or not. This does not change or break any *functionality* in our code. --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 289a194..4df6a33 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -166,11 +166,11 @@ def __str__(self): class Player: - def __init__(self, member, lounge_name, mmr): + def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed=False): self.member = member self.lounge_name = lounge_name self.mmr = mmr - self.confirmed = False + self.confirmed = confirmed self.score = 0 From 00bf0b87d9ea977c6d51c408837ccb5591a972af Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:43:00 -0500 Subject: [PATCH 085/292] Add mention property for Plater --- mogi_objects.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 4df6a33..f5696c5 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -173,6 +173,13 @@ def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed self.confirmed = confirmed self.score = 0 + @property + def mention(self): + """String that, when sent in a Discord message, will mention and ping the player.""" + if self.member is None: + return "<@!1>" + return self.member.mention + class VoteView(View): def __init__(self, players, thread, mogi: Mogi, tier_info): From 445c77d2a25d2110371dff2a14c046a33e4b180a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:43:11 -0500 Subject: [PATCH 086/292] Add __repr__ for Player --- mogi_objects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index f5696c5..d098fe4 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -180,6 +180,9 @@ def mention(self): return "<@!1>" return self.member.mention + def __repr__(self): + return f"{__class__.__name__}(member={self.member}, lounge_name={self.lounge_name}, mmr={self.mmr}, confirmed={self.confirmed})" + class VoteView(View): def __init__(self, players, thread, mogi: Mogi, tier_info): From 2167799af63ab1e0799914558e253279f210488f Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:43:39 -0500 Subject: [PATCH 087/292] Remove unused function This function is not used anywhere. --- mogi_objects.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index d098fe4..642fe27 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -136,12 +136,6 @@ def get_player(self, member): def get_first_player(self): return self.players[0] - def sub_player(self, sub_out, sub_in): - for i, player in enumerate(self.players): - if player == sub_out: - self.players[i] = sub_in - return - def num_confirmed(self): """Returns the number of confirmed players in the team""" return sum(1 for player in self.players if player.confirmed) From e34c2c09c99434f624dacca686cf8c82957872a1 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:45:06 -0500 Subject: [PATCH 088/292] Refactor: Add type hinting --- mogi_objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 642fe27..ce9c814 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -78,7 +78,7 @@ def get_room_from_thread(self, channel_id: int): class Room: def __init__(self, teams, room_num: int, thread: discord.Thread): - self.teams = teams + self.teams: List["Team"] = teams self.room_num = room_num self.thread = thread self.view = None @@ -113,7 +113,7 @@ def get_player_list(self): class Team: - def __init__(self, players): + def __init__(self, players: List["Player"]): self.players = players @property @@ -127,7 +127,7 @@ def all_registered(self): def has_player(self, member): return any(player.member.id == member.id for player in self.players) - def get_player(self, member): + def get_player(self, member: discord.Member) -> Player | None: for player in self.players: if player.member.id == member.id: return player From f91df62150e8d062ef93ac43491f0242fe89e845 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:46:07 -0500 Subject: [PATCH 089/292] Remove unused function --- mogi_objects.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index ce9c814..e3121d4 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -116,6 +116,10 @@ class Team: def __init__(self, players: List["Player"]): self.players = players + def get_mentions(self): + """Return a string where all players on the team are discord @'d""" + return " ".join([p.member.mention for p in self.players]) + @property def avg_mmr(self): return average([p.mmr for p in self.players]) From bfe1f914eb84e89c6ac19dd472e62277bf11f0c0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:46:25 -0500 Subject: [PATCH 090/292] Refactor: Type hint --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index e3121d4..e3977dc 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -48,7 +48,7 @@ def check_player(self, member): return team return None - def count_registered(self): + def count_registered(self) -> int: """Returns the number of teams that are registered""" return sum(1 for team in self.teams if team.all_registered()) From 68ffca1612db0db4cc9b45d7013d0f242e36f4a3 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:47:34 -0500 Subject: [PATCH 091/292] Remove unused function --- mogi_objects.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index e3977dc..d9d6477 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -55,13 +55,6 @@ def count_registered(self) -> int: def confirmed_list(self): return [team for team in self.teams if team.all_registered()] - def remove_id(self, squad_id: int): - confirmed = self.confirmed_list() - if squad_id < 1 or squad_id > len(confirmed): - return None - squad = confirmed[squad_id - 1] - self.teams.remove(squad) - return squad def is_room_thread(self, channel_id: int): for room in self.rooms: From b7cbaa883cda8ef727b0c4fb50d88fe00242a385 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 17:52:48 -0500 Subject: [PATCH 092/292] Change how num_players is computed --- mogi_objects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index d9d6477..b90803c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -33,7 +33,7 @@ def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_ @property def num_players(self): """Returns the total number of players in teams where all players have confirmed""" - return self.num_teams * self.players_per_team + return sum(len(t.players) for t in self.teams if t.all_registered()) @property def num_teams(self): @@ -55,6 +55,8 @@ def count_registered(self) -> int: def confirmed_list(self): return [team for team in self.teams if team.all_registered()] + def players_on_confirmed_teams(self) -> List[Player]: + return flatten([team.players for team in self.confirmed_teams()]) def is_room_thread(self, channel_id: int): for room in self.rooms: From a61eb7dac1820fb7dc275dcfaa2cb7c6863482ac Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:00:41 -0500 Subject: [PATCH 093/292] Main one room algorithm Supports both MK8DX and MKW to implement differing algorithms and functionality. MK8DX is free to implement their own algorithm if they want to. --- cogs/SquadQueue.py | 135 ++++++++++++++++++++++++--------------------- mogi_objects.py | 86 ++++++++++++++++++++++++++--- 2 files changed, 152 insertions(+), 69 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d5af09c..0e09e22 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -8,6 +8,9 @@ from datetime import datetime, timezone, timedelta import time import json + +import common +from common import divide_chunks from mmr import mkw_mmr, get_mmr_from_discord_id from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio @@ -33,6 +36,22 @@ def is_restricted(user: discord.User | discord.Member, config: dict) -> bool: or (restricted_role_id is not None and user.get_role(restricted_role_id)) +def basic_threshold_players_allowed(players: List[Player], threshold: int) -> bool: + """Returns True if the highest player's rating minus the lowest player's rating is below the given threshold""" + sorted_players = sorted(players, key=lambda p: p.mmr) + return (sorted_players[-1].mmr - sorted_players[0].mmr) <= threshold + + +def mkw_players_allowed(players: List[Player], threshold: int) -> bool: + """Returns true if the given list of players would be allowed to play together""" + return basic_threshold_players_allowed(players, threshold) + + +def mk8dx_players_allowed(players: List[Player], threshold: int) -> bool: + """Returns true if the given list of players would be allowed to play together""" + return basic_threshold_players_allowed(players, threshold) + + class SquadQueue(commands.Cog): def __init__(self, bot): self.bot: commands.Bot = bot @@ -355,7 +374,7 @@ async def list_task(self): await self.delete_list_messages(0) return - mogi_list = mogi.confirmed_list() + mogi_list = mogi.confirmed_teams() # Remove late players from the list to display separately full_list_length = len(mogi_list) @@ -696,12 +715,10 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): # make thread channels while the event is gathering instead of at the end, # since discord only allows 50 thread channels to be created per 5 minutes. async def check_room_channels(self, mogi): - num_teams = mogi.count_registered() - num_rooms = int(num_teams / (12 / mogi.players_per_team)) num_created_rooms = len(mogi.rooms) - if num_created_rooms >= num_rooms: + if num_created_rooms >= mogi.max_possible_rooms: return - for i in range(num_created_rooms, num_rooms): + for i in range(num_created_rooms, mogi.max_possible_rooms): minute = mogi.start_time.minute if len(str(minute)) == 1: minute = '0' + str(minute) @@ -730,8 +747,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic return if mogi.making_rooms_run and started_automatically: return - num_rooms = int(mogi.count_registered() / (12 / mogi.players_per_team)) - if num_rooms == 0: + if mogi.max_possible_rooms == 0: self.ongoing_event = None await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") return @@ -744,70 +760,65 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic if was_gathering: await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") - - teams_per_room = int(12 / mogi.players_per_team) - num_teams = int(num_rooms * teams_per_room) - final_list = mogi.confirmed_list()[0:num_teams] - sorted_list = sorted(final_list, reverse=True) - extra_members = [] - if str(mogi.mogi_channel.guild.id) in self.bot.config["members_for_channels"].keys(): - extra_members_ids = self.bot.config["members_for_channels"][str( - mogi.mogi_channel.guild.id)] - for m in extra_members_ids: - extra_members.append(mogi.mogi_channel.guild.get_member(m)) - if str(mogi.mogi_channel.guild.id) in self.bot.config["roles_for_channels"].keys(): - extra_roles_ids = self.bot.config["roles_for_channels"][str( - mogi.mogi_channel.guild.id)] - for r in extra_roles_ids: - extra_members.append(mogi.mogi_channel.guild.get_role(r)) - - rooms = mogi.rooms - for i in range(num_rooms): - msg = f"`Room {i+1} - Player List`\n" - mentions = "" - start_index = i*teams_per_room - player_list = [] - for j in range(teams_per_room): - msg += f"`{j+1}.` " - team = sorted_list[start_index+j] - player_list.append(sorted_list[start_index+j].get_first_player()) - msg += ", ".join([p.lounge_name for p in team.players]) - msg += f" ({int(team.avg_mmr)} MMR)\n" - mentions += " ".join([p.member.mention for p in team.players]) - mentions += " " - room_msg = msg - mentions += " ".join([m.mention for m in extra_members if m is not None]) - room_msg += "\nVote for format FFA, 2v2, 3v3, 4v4, or 6v6.\n" - room_msg += mentions - curr_room = rooms[i] - room_channel = curr_room.thread - curr_room.teams = sorted_list[start_index:start_index+teams_per_room] - curr_room.mmr_low = player_list[11].mmr - curr_room.mmr_high = player_list[0].mmr - if curr_room.mmr_high - curr_room.mmr_low > self.room_mmr_threshold: + + extra_members_ids = self.bot.config["members_for_channels"].get(str(mogi.mogi_channel.guild.id), []) + for m in extra_members_ids: + extra_members.append(mogi.mogi_channel.guild.get_member(m)) + extra_roles_ids = self.bot.config["roles_for_channels"].get(str(mogi.mogi_channel.guild.id), []) + for r in extra_roles_ids: + extra_members.append(mogi.mogi_channel.guild.get_role(r)) + + # Support for both server to implement their own algorithm for a player list being allowed + # For now, both servers have a simple rating range check that a list of players must meet, + # but each server could implement more complex checks on a given list of players. + if common.SERVER is common.Server.MKW: + def allowed_players_check(players): + return mkw_players_allowed(players, self.room_mmr_threshold) + elif common.SERVER is common.Server.MK8DX: + def allowed_players_check(players): + return mk8dx_players_allowed(players, self.room_mmr_threshold) + + all_confirmed_players = mogi.players_on_confirmed_teams() + regular_player_list = all_confirmed_players[:mogi.num_players//mogi.players_per_room] + late_player_list = all_confirmed_players[mogi.num_players//mogi.players_per_room:] + proposed_list = mogi.generate_proposed_list(allowed_players_check) + for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): + msg = f"`Room {room_number} - Player List`\n" + for player_num, player in enumerate(room_players, 1): + added_str = " *(Added from late players)*" if player in late_player_list else "" + msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} MMR){added_str}\n""" + if allowed_players_check(room_players): msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: + curr_room = mogi.rooms[room_number - 1] + curr_room.teams = [Team([p]) for p in room_players] + player_mentions = " ".join([p.mention for p in room_players]) + extra_member_mentions = " ".join([m.mention for m in extra_members if m is not None]) + room_msg = f"""{msg} +Vote for format FFA, 2v2, 3v3, 4v4, or 6v6. +{player_mentions} {extra_member_mentions}""" try: - await room_channel.send(room_msg) - view = VoteView(player_list, room_channel, mogi, self.TIER_INFO) + await curr_room.thread.send(room_msg) + view = VoteView(room_players, curr_room.thread, mogi, self.TIER_INFO) curr_room.view = view - await room_channel.send(view=view) + await curr_room.thread.send(view=view) except discord.DiscordException: print(traceback.format_exc()) + try: await mogi.mogi_channel.send(msg) - print(f"Mogi Channel message for room {i+1} has failed to send.", flush=True) except discord.DiscordException: + print(f"Mogi Channel message for room {room_number} has failed to send.", flush=True) print(traceback.format_exc()) - if num_teams < mogi.count_registered(): - missed_teams = mogi.confirmed_list( - )[num_teams:mogi.count_registered()] + + # Compute the list of "late" players that didn't get into any room + not_in_proposed_list = [p for p in all_confirmed_players if p not in proposed_list] + if len(not_in_proposed_list) > 0: msg = "`Late players:`\n" - for i in range(len(missed_teams)): - msg += f"`{i+1}.` " - msg += ", ".join([p.lounge_name for p in missed_teams[i].players]) - msg += f" ({int(missed_teams[i].avg_mmr)} MMR)\n" + for i, player in enumerate(not_in_proposed_list, 1): + removed_str = " *(Removed from player list)*" if player in regular_player_list else "" + msg += f"`{i+1}.` {player.lounge_name} ({int(player.mmr)} MMR) {removed_str}\n" try: await mogi.mogi_channel.send(msg) except discord.DiscordException: @@ -924,8 +935,8 @@ async def schedule_que_event(self): await self.MOGI_CHANNEL.send( "Squad Queue is currently going on at this hour! The queue will remain closed.") self.next_event = Mogi(sq_id=1, - players_per_team=1, - teams_per_room=12, + max_players_per_team=1, + players_per_room=12, mogi_channel=self.MOGI_CHANNEL, is_automated=True, start_time=next_event_start_time) @@ -972,7 +983,7 @@ def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( mogi.start_time, style="R") - return f"`#{mogi.sq_id}` **{mogi.players_per_team}v{mogi.players_per_team}:** {mogi_time} - {mogi_time_relative}" + return f"`#{mogi.sq_id}` **{mogi.max_player_per_team}v{mogi.max_player_per_team}:** {mogi_time} - {mogi_time_relative}" @commands.command(name="sync") @commands.is_owner() diff --git a/mogi_objects.py b/mogi_objects.py index b90803c..cf0786f 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -15,15 +15,20 @@ def average(list_: List[int | float]) -> float: class Mogi: - def __init__(self, sq_id: int, players_per_team: int, teams_per_room: int, mogi_channel: discord.TextChannel, + ALGORITHM_STATUS_INSUFFICIENT_PLAYERS = 1 + ALGORITHM_STATUS_2_OR_MORE_ROOMS = 2 + ALGORITHM_STATUS_SUCCESS_FOUND = 3 + ALGORITHM_STATUS_SUCCESS_EMPTY = 4 + + def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, mogi_channel: discord.TextChannel, is_automated=False, start_time=None): self.started = False self.gathering = False self.making_rooms_run = False self.making_rooms_run_time = None self.sq_id = sq_id - self.players_per_team = players_per_team - self.teams_per_room = teams_per_room + self.max_player_per_team = max_players_per_team + self.players_per_room = players_per_room self.mogi_channel = mogi_channel self.teams: List[Team] = [] self.rooms: List[Room] = [] @@ -41,6 +46,76 @@ def num_teams(self): return self.count_registered() @property + def max_possible_rooms(self) -> int: + """Returns the maximum possible number of rooms based on teams where all players have confirmed. Depending on range cutoffs or + the specifics of the algorithm used to make the actual rooms, this number could be higher than the final number of rooms.""" + return self.num_players // self.players_per_room + + @staticmethod + def _minimize_range(players: List[Player], num_players: int) -> List[Player] | None: + """Returns a collection of players (the number of players in the collection is the given num_players parameter) whose has the smallest + mmr spread. If the number of players in the given list is smaller than the request num_players collection size, or the num_players is less than 2 (doesn't make sense), None is returned.""" + # The number of players we were given is less than the collection size we are supposed to return, so return None + if len(players) < num_players: + return None + if num_players <= 1: + return None + # Sort the players so we easily know the player with the lowest rating and highest rating in any given collection + sorted_players = sorted(players, key=lambda p: p.mmr) + # In the beginning, the best found collection of players is the first 12 + best_collection = sorted_players[0:num_players] + cur_min = best_collection[-1].mmr - best_collection[0].mmr + # Find the collection of players with the least rating spread + for lowest_player_index, highest_player in enumerate(sorted_players[num_players:], 1): + lowest_player = sorted_players[lowest_player_index] + cur_range = highest_player.mmr - lowest_player.mmr + if cur_range < cur_min: + cur_min = cur_range + best_collection = sorted_players[lowest_player_index:lowest_player_index + num_players] + return best_collection + + def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[ + List[Player], int]: + if self.max_possible_rooms == 0: + return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS + if self.max_possible_rooms > 1: + confirmed_players = self.players_on_confirmed_teams() + return confirmed_players[0:self.players_per_room * self.max_possible_rooms], Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS + # At this point, we can only make one possible room, so our algorithm will be used + confirmed_players = self.players_on_confirmed_teams() + cur_check_list = list(confirmed_players[0:self.players_per_room]) + late_players = list(confirmed_players[self.players_per_room:]) + + while True: + best_collection = Mogi._minimize_range(cur_check_list, self.players_per_room) + if valid_players_check(best_collection): + return best_collection, Mogi.ALGORITHM_STATUS_SUCCESS_FOUND + if len(late_players) == 0: + break + cur_check_list.append(late_players.pop(0)) + # Even after checking the late players, we did not find + return [], Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY + + def _mk8dx_generate_final_list(self) -> List[Player]: + confirmed_players = self.players_on_confirmed_teams() + return confirmed_players[0:self.players_per_room * self.max_possible_rooms] + + def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: + result, _ = self._one_room_final_list_algorithm(valid_players_check) + return result + + def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], bool] = None) -> List[Player]: + """Algorithm that generates a proposed list of players that will play. This algorithm may differ between + MK8DX and MKW. The algorithm is allowed to propose any list of players it wants to. Among several possibilities, + this allows the algorithm to change the order of the players in the returned list, add or remove players, + and more. + + The algorithm may or may not enforce a hard check of the valid players. That is up to the implemented + algorithm.""" + if common.SERVER is common.Server.MK8DX: + return self._mk8dx_generate_final_list() + elif common.SERVER is common.Server.MKW: + return self._mkw_generate_final_list(valid_players_check) def check_player(self, member): for team in self.teams: @@ -52,7 +127,7 @@ def count_registered(self) -> int: """Returns the number of teams that are registered""" return sum(1 for team in self.teams if team.all_registered()) - def confirmed_list(self): + def confirmed_teams(self) -> List["Team"]: return [team for team in self.teams if team.all_registered()] def players_on_confirmed_teams(self) -> List[Player]: @@ -132,9 +207,6 @@ def get_player(self, member: discord.Member) -> Player | None: return player return None - def get_first_player(self): - return self.players[0] - def num_confirmed(self): """Returns the number of confirmed players in the team""" return sum(1 for player in self.players if player.confirmed) From a62007a11764082349274a42e69e5b74cd3f9854 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:01:17 -0500 Subject: [PATCH 094/292] Unit Testing for algorithm Unit testing to show correctness of algorithm --- unit_testing.py | 340 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 unit_testing.py diff --git a/unit_testing.py b/unit_testing.py new file mode 100644 index 0000000..5c4f935 --- /dev/null +++ b/unit_testing.py @@ -0,0 +1,340 @@ +import unittest + +import common +from cogs import SquadQueue +from mogi_objects import Player, Mogi, Team +import random + + +class AlgorithmTests(unittest.TestCase): + + def test_empty_player_list(self): + """Test for empty player list""" + players = [] + num_players = 12 + result = Mogi._minimize_range(players, num_players) + self.assertIsNone(result) + + def test_not_enough_players(self): + """Test for when too many players are requested""" + players = [Player(None, None, None), Player(None, None, None)] + random.shuffle(players) + num_players = 3 + result = Mogi._minimize_range(players, num_players) + self.assertIsNone(result) + + def test_enough_players(self): + """Test for when there are enough requested players""" + players = [Player(None, "Joe", 100), Player(None, "Bob", 200)] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertIsNotNone(result) + + def test_one_possible_collection(self): + """Test for when the number of players given is the number of players requested""" + p1 = Player(None, "Joe", 100) + p2 = Player(None, "Bob", 200) + players = [p1, p2] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p1, p2], result) + + def test_correct_collection_1(self): + """Test #1 for correctness""" + p1 = Player(None, "Joe", 100) + p2 = Player(None, "Bob", 200) + p3 = Player(None, "Jane", 250) + players = [p1, p2, p3] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p2, p3], result) + + def test_correct_collection_2(self): + """Test #2 for correctness""" + p1 = Player(None, "Joe", 100) + p2 = Player(None, "Bob", 200) + p3 = Player(None, "Jane", 350) + players = [p1, p2, p3] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p1, p2], result) + + def test_correct_collection_3(self): + """Test #3 for correctness""" + p1 = Player(None, "Joe", 100) + p2 = Player(None, "Bob", 0) + p3 = Player(None, "Jane", 350) + players = [p1, p2, p3] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p2, p1], result) + + def test_correct_collection_4(self): + """Test #4 for correctness""" + p1 = Player(None, "Joe", 0) + p2 = Player(None, "Bob", -100) + p3 = Player(None, "Jane", 1) + players = [p1, p2, p3] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p1, p3], result) + + def test_correct_collection_5(self): + """Test #5 for correctness""" + p1 = Player(None, "Joe", 0) + p2 = Player(None, "Bob", 100) + p3 = Player(None, "Jane", 1) + players = [p1, p2, p3] + random.shuffle(players) + num_players = 2 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p1, p3], result) + + def test_correct_collection_6(self): + """Test #6 for correctness, large player collection: + -300 + 0 + 100 + 200 + 500 + 501 + 502 + 600 + 750 + 800 + 1000 + 1500 : 1800 + 1501 : 1501 + 1550 : 1450 + 1800 : 1600 + 2000 : 1500""" + p1 = Player(None, "1", 1000) + p2 = Player(None, "2", 500) + p3 = Player(None, "3", 1500) + p4 = Player(None, "4", 750) + p5 = Player(None, "5", 800) + p6 = Player(None, "6", 2000) + p7 = Player(None, "7", 0) + p8 = Player(None, "8", -300) + p9 = Player(None, "9", 501) + p10 = Player(None, "10", 600) + p11 = Player(None, "11", 200) + p12 = Player(None, "12", 1550) + p13 = Player(None, "13", 1800) + p14 = Player(None, "14", 1501) + p15 = Player(None, "15", 502) + p16 = Player(None, "16", 100) + players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] + random.shuffle(players) + num_players = 12 + result = Mogi._minimize_range(players, num_players) + self.assertListEqual([p16, p11, p2, p9, p15, p10, p4, p5, p1, p3, p14, p12], result) + + def test_correct_collection_7(self): + """Test #7 for large player collection where 12 players are given and 12 are requested""" + p1 = Player(None, "1", 1000) + p2 = Player(None, "2", 500) + p3 = Player(None, "3", 1500) + p4 = Player(None, "4", 750) + p5 = Player(None, "5", 800) + p6 = Player(None, "6", 2000) + p7 = Player(None, "7", 0) + p8 = Player(None, "8", -300) + p9 = Player(None, "9", 500) + p10 = Player(None, "10", 600) + p11 = Player(None, "11", 200) + p12 = Player(None, "12", 1550) + players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12] + random.shuffle(players) + num_players = 12 + result = Mogi._minimize_range(players, num_players) + self.assertSetEqual(set(players), set(result)) + + +class OneRoomAlgorithmTests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.threshold = 65 + cls.threshold_function = None + if common.SERVER is common.Server.MKW: + cls.threshold_function = lambda players: SquadQueue.mkw_players_allowed(players, cls.threshold) + elif common.SERVER is common.Server.MK8DX: + cls.threshold_function = lambda players: SquadQueue.mk8dx_players_allowed(players, cls.threshold) + else: + raise Exception("Lounge parameter in config is invalid, unit tests will fail.") + + def _new_default_mogi(self) -> Mogi: + """Returns a default mogi with 12 players per room.""" + self # to hide annoying PyCharm IDE error - well aware it could be static, do not want it to be + return Mogi(sq_id=1, + max_players_per_team=1, + players_per_room=12, + mogi_channel=None + ) + + def test_no_players(self): + """Test when no players are queued""" + mogi = self._new_default_mogi() + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertListEqual([], results) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + + def test_one_player(self): + """Test when one player is queued""" + p1 = Player(None, "1", 1000, True) + mogi = self._new_default_mogi() + mogi.teams.append(Team([p1])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertListEqual([], results) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + + def test_two_players(self): + """Test when two players are queued""" + p1 = Player(None, "1", 1000, True) + p2 = Player(None, "2", 2000, True) + mogi = self._new_default_mogi() + mogi.teams.append(Team([p1])) + mogi.teams.append(Team([p2])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertListEqual([], results) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + + def test_large_max_players_per_team(self): + """This is to catch the Mogi class computes the number of confirmed players incorrectly""" + p1 = Player(None, "1", 1000, True) + mogi = self._new_default_mogi() + mogi.max_player_per_team = 24 + mogi.teams.append(Team([p1])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertListEqual([], results) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + + def test_2_rooms_correctness_1(self): + """Test if 24P for 12 player rooms generates 2 rooms with correct status code""" + players = [Player(None, f"{i}", i, True) for i in range(24)] + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertSetEqual(set(players), set(results)) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + + def test_2_rooms_correctness_2(self): + """Test if 25P for 12 player rooms generates 2 rooms with correct players and correct status code""" + players = [Player(None, f"{i}", i, True) for i in range(25)] + random.shuffle(players) + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertSetEqual(set(players[0:24]), set(results)) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + + def test_2_rooms_correctness_3(self): + """Test if 25P for 12 player rooms generates 2 rooms with correct players and correct status code even with extremely large range""" + players = [Player(None, f"{i*10000}", i*10000, True) for i in range(25)] + random.shuffle(players) + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertSetEqual(set(players[0:24]), set(results)) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + + def test_1_room_correctness_1(self): + """Test if 15P for 12 player rooms generates 1 rooms with correct players and correct status code""" + assert self.threshold == 65 + players = [Player(None, f"{i}", abs(i-8)*(i-8), True) for i in range(17)] # -64, -49, -36, ... 0, 1, 4, ... 64 + players.pop(15) # Set 36 rating to not confirmed so only one possibility occurs + correct_result = players[2:14].copy() # Only one possibility that matches threshold + random.shuffle(players) + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + self.assertSetEqual(set(correct_result), set(results)) + + def test_2_room_correctness_2(self): + """Test if 15P for 12 player rooms does not find any rooms when players are beyond range""" + assert self.threshold == 65 + players = [Player(None, f"{i}", (i-8)**3, True) for i in range(17)] # -512, -343, ... 0, 1, 8, ... 512 + correct_result = [] # No possible list of players + random.shuffle(players) + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) + self.assertSetEqual(set(correct_result), set(results)) + + def test_2_room_correctness_3(self): + """Test if 15P for 12 player rooms finds the single correct solution""" + assert self.threshold == 65 + p1 = Player(None, "1", 10, True) + p2 = Player(None, "2", 5, True) + p3 = Player(None, "3", 14, True) + p4 = Player(None, "4", 75, True) + p5 = Player(None, "5", 8, True) + p6 = Player(None, "6", 20, True) + p7 = Player(None, "7", 0, True) + p8 = Player(None, "8", -3, True) + p9 = Player(None, "9", 4, True) + p10 = Player(None, "10", -105, True) + p11 = Player(None, "11", 2, True) + p12 = Player(None, "12", 15, True) + p13 = Player(None, "13", 16, True) + p14 = Player(None, "14", 74, True) + p15 = Player(None, "15", 9, True) + p16 = Player(None, "16", 100, True) + # p4 should be removed, p10 should be removed, p13 should be added, p15 should be added + players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] + correct_result = [p1, p2, p3, p5, p6, p7, p8, p9, p11, p12, p13, p15] + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + self.assertSetEqual(set(correct_result), set(results)) + + def test_2_room_correctness_4(self): + """Test if 15P for 12 player rooms finds no solution""" + assert self.threshold == 65 + p1 = Player(None, "1", 10, True) + p2 = Player(None, "2", 5, True) + p3 = Player(None, "3", 14, True) + p4 = Player(None, "4", 75, True) + p5 = Player(None, "5", 8, True) + p6 = Player(None, "6", 20, True) + p7 = Player(None, "7", 0, True) + p8 = Player(None, "8", -3, True) + p9 = Player(None, "9", 4, True) + p10 = Player(None, "10", -105, True) + p11 = Player(None, "11", 2, True) + p12 = Player(None, "12", 15, True) + p13 = Player(None, "13", 16, True) + p14 = Player(None, "14", 74, True) + p15 = Player(None, "15", 9, False) + p16 = Player(None, "16", 100, True) + players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] + correct_result = [] # Same as previous test, but since p15 is not confirmed, no result is found + mogi = self._new_default_mogi() + for p in players: + mogi.teams.append(Team([p])) + results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) + self.assertSetEqual(set(correct_result), set(results)) + + + +if __name__ == "__main__": + """If PyCharm, add --verbose flag to configuration. + Other IDEs, run from shell: python -m unittest ./unit_testing.py --verbose""" + unittest.main(verbosity=2) From 11a7794587166307d49b1ae93cc7f1cc1c6b28be Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:08:43 -0500 Subject: [PATCH 095/292] Bug fix --- lounge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lounge.py b/lounge.py index ff597be..c9dd60e 100644 --- a/lounge.py +++ b/lounge.py @@ -11,7 +11,7 @@ case_insensitive=True, intents=intents, help_command=None) initial_extensions = ['cogs.SquadQueue'] -bot.config = common.config +bot.config = common.CONFIG @bot.event From 33b979372e320135c03f01b83e8222bc6c18c158 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:09:00 -0500 Subject: [PATCH 096/292] Added function for testing player ratings --- cogs/SquadQueue.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 0e09e22..eedcea2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1021,6 +1021,24 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): await self.check_room_channels(mogi) await self.check_num_teams(mogi) + @commands.command(name="debug_add_many_ratings") + @commands.is_owner() + async def debug_add_many_ratings(self, ctx, ratings: str): + mogi = self.get_mogi(ctx) + if mogi is None: + return + if (not await self.is_started(ctx, mogi) + or not await self.is_gathering(ctx, mogi)): + return + for i, rating in enumerate(ratings.split(" ")): + if rating.isdecimal(): + player = Player(ctx.author, f"{ctx.author.display_name} {i}", int(rating), confirmed=True) + mogi.teams.append(Team([player])) + msg = f"Players added!" + await self.queue_or_send(ctx, msg) + await self.check_room_channels(mogi) + await self.check_num_teams(mogi) + @commands.command(name="debug_add_many_players") @commands.is_owner() async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Member]): From 7dbd37a54c7206fe0141cd557c184190899a5e67 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:25:56 -0500 Subject: [PATCH 097/292] Bug fixed for testing function --- cogs/SquadQueue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index eedcea2..0274a5c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -15,7 +15,7 @@ from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier import asyncio from collections import defaultdict -from typing import Dict, List +from typing import Dict, List, Tuple import traceback import os import dill @@ -1023,18 +1023,18 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): @commands.command(name="debug_add_many_ratings") @commands.is_owner() - async def debug_add_many_ratings(self, ctx, ratings: str): + async def debug_add_many_ratings(self, ctx, *ratings: str): mogi = self.get_mogi(ctx) if mogi is None: return if (not await self.is_started(ctx, mogi) or not await self.is_gathering(ctx, mogi)): return - for i, rating in enumerate(ratings.split(" ")): + for i, rating in enumerate(ratings): if rating.isdecimal(): player = Player(ctx.author, f"{ctx.author.display_name} {i}", int(rating), confirmed=True) mogi.teams.append(Team([player])) - msg = f"Players added!" + msg = f"Players added with the following ratings: {' '.join(['`' + r + '`' for r in ratings])}" await self.queue_or_send(ctx, msg) await self.check_room_channels(mogi) await self.check_num_teams(mogi) From 74237ad0cf409288350d301be10c50000b311ec8 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:34:37 -0500 Subject: [PATCH 098/292] Bug fix --- cogs/SquadQueue.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 0274a5c..1bb2024 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -780,8 +780,9 @@ def allowed_players_check(players): return mk8dx_players_allowed(players, self.room_mmr_threshold) all_confirmed_players = mogi.players_on_confirmed_teams() - regular_player_list = all_confirmed_players[:mogi.num_players//mogi.players_per_room] - late_player_list = all_confirmed_players[mogi.num_players//mogi.players_per_room:] + first_late_player_index = (mogi.num_players//mogi.players_per_room) * mogi.players_per_room + regular_player_list = all_confirmed_players[:first_late_player_index] + late_player_list = all_confirmed_players[first_late_player_index:] proposed_list = mogi.generate_proposed_list(allowed_players_check) for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" From ea59dd351792af94f8e8d36c7bd2c17469771943 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:34:58 -0500 Subject: [PATCH 099/292] Bug fix on display list --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1bb2024..2f9cb27 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -819,7 +819,7 @@ def allowed_players_check(players): msg = "`Late players:`\n" for i, player in enumerate(not_in_proposed_list, 1): removed_str = " *(Removed from player list)*" if player in regular_player_list else "" - msg += f"`{i+1}.` {player.lounge_name} ({int(player.mmr)} MMR) {removed_str}\n" + msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} MMR) {removed_str}\n" try: await mogi.mogi_channel.send(msg) except discord.DiscordException: From 712c3c66c7e724fa18762931c53acdfb9271aef5 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:35:12 -0500 Subject: [PATCH 100/292] Display modification --- cogs/SquadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 2f9cb27..78e4f6b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -787,7 +787,7 @@ def allowed_players_check(players): for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): - added_str = " *(Added from late players)*" if player in late_player_list else "" + added_str = ": **Added from late players**" if player in late_player_list else "" msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} MMR){added_str}\n""" if allowed_players_check(room_players): msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." @@ -818,7 +818,7 @@ def allowed_players_check(players): if len(not_in_proposed_list) > 0: msg = "`Late players:`\n" for i, player in enumerate(not_in_proposed_list, 1): - removed_str = " *(Removed from player list)*" if player in regular_player_list else "" + removed_str = ": **Removed from player list**" if player in regular_player_list else "" msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} MMR) {removed_str}\n" try: await mogi.mogi_channel.send(msg) From a4d0cdee70138a3f4db8312c1d42c93d3d9281c0 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:46:15 -0500 Subject: [PATCH 101/292] Bug fix --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 78e4f6b..6a81d41 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -789,7 +789,7 @@ def allowed_players_check(players): for player_num, player in enumerate(room_players, 1): added_str = ": **Added from late players**" if player in late_player_list else "" msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} MMR){added_str}\n""" - if allowed_players_check(room_players): + if not allowed_players_check(room_players): msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: curr_room = mogi.rooms[room_number - 1] From a969b1650c5434ea5cf15617afc9d7c90382f388 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:57:31 -0500 Subject: [PATCH 102/292] Addresses #9 #9 --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6a81d41..f25108e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -389,7 +389,7 @@ async def list_task(self): sorted_mogi_list = sorted(mogi_list, reverse=True) msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" msg += f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" - msg += "**Current Mogi List:**\n\n" + msg += "**Current Mogi List:**\n" for i in range(len(sorted_mogi_list)): msg += f"{i + 1}) " msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) From 563bc6eaafaaf4894dbd112802101e8f05952193 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 18:57:51 -0500 Subject: [PATCH 103/292] Addresses optional part on #9 Optional part on #9 --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index f25108e..6c72d37 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -388,7 +388,6 @@ async def list_task(self): sorted_mogi_list = sorted(mogi_list, reverse=True) msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" - msg += f"**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}\n\n" msg += "**Current Mogi List:**\n" for i in range(len(sorted_mogi_list)): msg += f"{i + 1}) " @@ -401,6 +400,7 @@ async def list_task(self): msg += f"{i + 1}) " msg += ", ".join([p.lounge_name for p in late_players[i].players]) msg += f" ({late_players[i].players[0].mmr} MMR)\n" + msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" message = msg.split("\n") new_messages = [] From 3093cc499d7efb783dd3544eed85cd4c3c1bf0dd Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 19:09:16 -0500 Subject: [PATCH 104/292] Bug fix on algorithm --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index cf0786f..a82bf88 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -79,7 +79,7 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS if self.max_possible_rooms > 1: - confirmed_players = self.players_on_confirmed_teams() + confirmed_players = sorted(self.players_on_confirmed_teams()) return confirmed_players[0:self.players_per_room * self.max_possible_rooms], Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS # At this point, we can only make one possible room, so our algorithm will be used confirmed_players = self.players_on_confirmed_teams() @@ -97,7 +97,7 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla return [], Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY def _mk8dx_generate_final_list(self) -> List[Player]: - confirmed_players = self.players_on_confirmed_teams() + confirmed_players = sorted(self.players_on_confirmed_teams()) return confirmed_players[0:self.players_per_room * self.max_possible_rooms] def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: From ba44c7605d450cd91ef4cce071eaa3610f8aa275 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:14:41 -0500 Subject: [PATCH 105/292] Make players comparable based on mmr --- mogi_objects.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index a82bf88..1ac927b 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -61,7 +61,7 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N if num_players <= 1: return None # Sort the players so we easily know the player with the lowest rating and highest rating in any given collection - sorted_players = sorted(players, key=lambda p: p.mmr) + sorted_players = sorted(players, reverse=True) # In the beginning, the best found collection of players is the first 12 best_collection = sorted_players[0:num_players] cur_min = best_collection[-1].mmr - best_collection[0].mmr @@ -116,6 +116,8 @@ def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], b return self._mk8dx_generate_final_list() elif common.SERVER is common.Server.MKW: return self._mkw_generate_final_list(valid_players_check) + else: + raise ValueError(f"Unknown server in config: {common.Server}") def check_player(self, member): for team in self.teams: @@ -158,13 +160,13 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): def mmr_high(self) -> int: if self.teams is None: return None - return max(self.players, key=lambda p: p.mmr).mmr + return max(self.players).mmr @property def mmr_low(self) -> int: if self.teams is None: return None - return min(self.players, key=lambda p: p.mmr).mmr + return min(self.players).mmr @property def avg_mmr(self) -> float: @@ -248,6 +250,9 @@ def mention(self): def __repr__(self): return f"{__class__.__name__}(member={self.member}, lounge_name={self.lounge_name}, mmr={self.mmr}, confirmed={self.confirmed})" + def __lt__(self, other: Player): + return self.mmr < other.mmr + class VoteView(View): def __init__(self, players, thread, mogi: Mogi, tier_info): From 60d74a10d4fd5b7ccdc1f4c8d2d5ad20e14a2e1a Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:15:23 -0500 Subject: [PATCH 106/292] Bug fix --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6c72d37..6f42e6c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -783,7 +783,7 @@ def allowed_players_check(players): first_late_player_index = (mogi.num_players//mogi.players_per_room) * mogi.players_per_room regular_player_list = all_confirmed_players[:first_late_player_index] late_player_list = all_confirmed_players[first_late_player_index:] - proposed_list = mogi.generate_proposed_list(allowed_players_check) + proposed_list = sorted(mogi.generate_proposed_list(allowed_players_check), reverse=True) for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): From 341ec83a43b7dec86e0bd3716e0fd8c4b87bace6 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:15:36 -0500 Subject: [PATCH 107/292] Modify display of test function --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6f42e6c..64cc4c1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1031,7 +1031,7 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): if (not await self.is_started(ctx, mogi) or not await self.is_gathering(ctx, mogi)): return - for i, rating in enumerate(ratings): + for i, rating in enumerate(ratings, 1): if rating.isdecimal(): player = Player(ctx.author, f"{ctx.author.display_name} {i}", int(rating), confirmed=True) mogi.teams.append(Team([player])) From c76df70591cbe6b4fce391b5d64e668eba440238 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:21:25 -0500 Subject: [PATCH 108/292] More bug fixes --- mogi_objects.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 1ac927b..170bd59 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -74,12 +74,11 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N best_collection = sorted_players[lowest_player_index:lowest_player_index + num_players] return best_collection - def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[ - List[Player], int]: + def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[Player], int]: if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS if self.max_possible_rooms > 1: - confirmed_players = sorted(self.players_on_confirmed_teams()) + confirmed_players = self.players_on_confirmed_teams() return confirmed_players[0:self.players_per_room * self.max_possible_rooms], Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS # At this point, we can only make one possible room, so our algorithm will be used confirmed_players = self.players_on_confirmed_teams() @@ -97,7 +96,7 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla return [], Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY def _mk8dx_generate_final_list(self) -> List[Player]: - confirmed_players = sorted(self.players_on_confirmed_teams()) + confirmed_players = self.players_on_confirmed_teams() return confirmed_players[0:self.players_per_room * self.max_possible_rooms] def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: From 819f1821174ca57eec953d3e42230adc73b1ca05 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:38:50 -0500 Subject: [PATCH 109/292] Another bug fix --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 170bd59..72362a0 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -61,7 +61,7 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N if num_players <= 1: return None # Sort the players so we easily know the player with the lowest rating and highest rating in any given collection - sorted_players = sorted(players, reverse=True) + sorted_players = sorted(players) # In the beginning, the best found collection of players is the first 12 best_collection = sorted_players[0:num_players] cur_min = best_collection[-1].mmr - best_collection[0].mmr From 63d9a119ea337f2e3cb0fdb938d7cd261457103d Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 20:49:35 -0500 Subject: [PATCH 110/292] Fixes #13 + Clean refactor Fixes #13, plus a more clean refactor of this code. --- cogs/SquadQueue.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 64cc4c1..af206e2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -373,33 +373,21 @@ async def list_task(self): if not mogi.gathering: await self.delete_list_messages(0) return + all_confirmed_players = mogi.players_on_confirmed_teams() + first_late_player_index = (mogi.num_players // mogi.players_per_room) * mogi.players_per_room + on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) + late_players = all_confirmed_players[first_late_player_index:] - mogi_list = mogi.confirmed_teams() - - # Remove late players from the list to display separately - full_list_length = len(mogi_list) - num_of_rooms = full_list_length // 12 - num_confirmed_players = num_of_rooms * 12 - num_late_players = full_list_length - num_confirmed_players - late_players = [] - for i in range(num_confirmed_players, full_list_length): - player = mogi_list.pop() - late_players.append(player) - - sorted_mogi_list = sorted(mogi_list, reverse=True) msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" msg += "**Current Mogi List:**\n" - for i in range(len(sorted_mogi_list)): - msg += f"{i + 1}) " - msg += ", ".join([p.lounge_name for p in sorted_mogi_list[i].players]) - msg += f" ({sorted_mogi_list[i].players[0].mmr} MMR)\n" - if ((i + 1) % 12 == 0): + for i, player in enumerate(on_time_players, 1): + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + if i % mogi.players_per_room == 0: msg += "ㅤ\n" + msg += "**Late Players:**\n" - for i in range(len(late_players)): - msg += f"{i + 1}) " - msg += ", ".join([p.lounge_name for p in late_players[i].players]) - msg += f" ({late_players[i].players[0].mmr} MMR)\n" + for i, player in enumerate(late_players, 1): + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" message = msg.split("\n") From 42c9cbb4c60e451c101a6e2d164ae6c7f58fc477 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 21:01:21 -0500 Subject: [PATCH 111/292] Minor text adjustment --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index af206e2..53368c6 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -384,7 +384,8 @@ async def list_task(self): msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" - + if len(on_time_players) == 0: # Text looks better this way + msg += "\n" msg += "**Late Players:**\n" for i, player in enumerate(late_players, 1): msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" From 7bf68ddb9040a253ef89156e2fcafbcc6ac8c7cc Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 21:23:49 -0500 Subject: [PATCH 112/292] Forgot to upload part of refactor --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 53368c6..5b68e4c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -826,7 +826,7 @@ async def check_num_teams(self, mogi: Mogi): return cur_time = datetime.now(timezone.utc) if mogi.start_time <= cur_time: - num_leftover_teams = mogi.count_registered() % int((12 / mogi.players_per_team)) + num_leftover_teams = mogi.count_registered() % int((12 / mogi.max_player_per_team)) if num_leftover_teams == 0: mogi.gathering = False await self.lockdown(mogi.mogi_channel) @@ -845,14 +845,14 @@ async def ongoing_mogi_checks(self): mogi.gathering = False elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % int((12 / mogi.players_per_team)) + num_leftover_teams = mogi.count_registered() % int((12 / mogi.max_player_per_team)) if num_leftover_teams == 0: mogi.gathering = False else: if int(cur_time.second / 20) == 0: force_time = mogi.start_time + self.EXTENSION_TIME minutes_left = (force_time - cur_time).seconds // 60 - x_teams = int(int(12 / mogi.players_per_team) - num_leftover_teams) + x_teams = int(int(12 / mogi.max_player_per_team) - num_leftover_teams) await mogi.mogi_channel.send( f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") if not mogi.gathering: From e7f2554f228ed3d80746baa86cdbf11de9ceca1d Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sat, 6 Jul 2024 21:33:39 -0500 Subject: [PATCH 113/292] Reverse part of commit, add error message back when an exception occurs with the room channel. --- cogs/SquadQueue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5b68e4c..83b7631 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -794,6 +794,9 @@ def allowed_players_check(players): curr_room.view = view await curr_room.thread.send(view=view) except discord.DiscordException: + err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" + err_msg += player_mentions + extra_member_mentions + msg += err_msg print(traceback.format_exc()) try: From e876296819f5c82d6ece0241d4e0cd89a46be83c Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 20:48:24 -0500 Subject: [PATCH 114/292] Extend discord command Just a shell. To test the command shows properly on Discord. --- cogs/SquadQueue.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 83b7631..381ba0f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -238,6 +238,18 @@ async def is_gathering(self, ctx, mogi: Mogi): await ctx.send("Mogi is closed; players cannot join or drop from the event") return mogi.gathering + @app_commands.command(name="extend") + @app_commands.guild_only() + async def extend(self, interaction: discord.Interaction, minutes: int): + """Extend the queue + + Parameters + ----------- + minutes: int + The number of minutes to add to the extension time. Can be negative. + """ + await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") + @app_commands.command(name="c") @app_commands.guild_only() async def can(self, interaction: discord.Interaction): From 65639d22d8dee16a4aa2ae748e56fc1e70485e2d Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 21:11:30 -0500 Subject: [PATCH 115/292] Add additional extension time to Mogi object --- mogi_objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 72362a0..0f022e3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -21,7 +21,7 @@ class Mogi: ALGORITHM_STATUS_SUCCESS_EMPTY = 4 def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, mogi_channel: discord.TextChannel, - is_automated=False, start_time=None): + is_automated=False, start_time=None, additional_extension_minutes=0): self.started = False self.gathering = False self.making_rooms_run = False @@ -34,6 +34,7 @@ def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, self.rooms: List[Room] = [] self.is_automated = is_automated self.start_time = start_time if is_automated else None + self.additional_extension_minutes = additional_extension_minutes @property def num_players(self): From c8eae07ebb73ff16a9b7c127d3556ab4c5be2aa2 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 21:21:19 -0500 Subject: [PATCH 116/292] Change the extension functionality to... incorporate the additional extension minutes. --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 381ba0f..91b731c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -856,7 +856,8 @@ async def ongoing_mogi_checks(self): if not mogi.is_automated or not mogi.started or mogi.making_rooms_run: return cur_time = datetime.now(timezone.utc) - if (mogi.start_time + self.EXTENSION_TIME) <= cur_time: + force_start_time = mogi.start_time + self.EXTENSION_TIME + mogi.additional_extension_minutes + if force_start_time <= cur_time: mogi.gathering = False elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time @@ -865,8 +866,7 @@ async def ongoing_mogi_checks(self): mogi.gathering = False else: if int(cur_time.second / 20) == 0: - force_time = mogi.start_time + self.EXTENSION_TIME - minutes_left = (force_time - cur_time).seconds // 60 + minutes_left = (force_start_time - cur_time).seconds // 60 x_teams = int(int(12 / mogi.max_player_per_team) - num_leftover_teams) await mogi.mogi_channel.send( f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") From 591f8f94aa58e9ce5225132f519e03407f7367ae Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 21:23:39 -0500 Subject: [PATCH 117/292] Finish extend functionality --- cogs/SquadQueue.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 91b731c..b49d58b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -248,6 +248,11 @@ async def extend(self, interaction: discord.Interaction, minutes: int): minutes: int The number of minutes to add to the extension time. Can be negative. """ + mogi = self.get_mogi(interaction) + if mogi is None or not mogi.started or not mogi.gathering: + await interaction.followup.send("Queue has not started yet.") + return + mogi.additional_extension_minutes += minutes await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") @app_commands.command(name="c") From 1ff190873ba2c5b860b71494461c4515f6a93fdf Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 21:28:05 -0500 Subject: [PATCH 118/292] Bug fixes: extension int should be timedelta --- cogs/SquadQueue.py | 2 +- mogi_objects.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b49d58b..8986275 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -252,7 +252,7 @@ async def extend(self, interaction: discord.Interaction, minutes: int): if mogi is None or not mogi.started or not mogi.gathering: await interaction.followup.send("Queue has not started yet.") return - mogi.additional_extension_minutes += minutes + mogi.additional_extension_minutes += timedelta(minutes=minutes) await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") @app_commands.command(name="c") diff --git a/mogi_objects.py b/mogi_objects.py index 0f022e3..1a790d3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -34,7 +34,7 @@ def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, self.rooms: List[Room] = [] self.is_automated = is_automated self.start_time = start_time if is_automated else None - self.additional_extension_minutes = additional_extension_minutes + self.additional_extension = timedelta(minutes=additional_extension_minutes) @property def num_players(self): From 1f37c2c3e006ff3f5445f0c8526878b2afb7c351 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 21:30:29 -0500 Subject: [PATCH 119/292] Bug fix, did not rename class variable in all places --- cogs/SquadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8986275..ba4d0dc 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -252,7 +252,7 @@ async def extend(self, interaction: discord.Interaction, minutes: int): if mogi is None or not mogi.started or not mogi.gathering: await interaction.followup.send("Queue has not started yet.") return - mogi.additional_extension_minutes += timedelta(minutes=minutes) + mogi.additional_extension += timedelta(minutes=minutes) await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") @app_commands.command(name="c") @@ -861,7 +861,7 @@ async def ongoing_mogi_checks(self): if not mogi.is_automated or not mogi.started or mogi.making_rooms_run: return cur_time = datetime.now(timezone.utc) - force_start_time = mogi.start_time + self.EXTENSION_TIME + mogi.additional_extension_minutes + force_start_time = mogi.start_time + self.EXTENSION_TIME + mogi.additional_extension if force_start_time <= cur_time: mogi.gathering = False elif mogi.start_time <= cur_time and mogi.gathering: From 9f516ff59094b1570e19b5458d3fd0b87a18f1c3 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 23:06:46 -0500 Subject: [PATCH 120/292] Fix bug with mmr check when subbing --- mogi_objects.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 1a790d3..c4caa0d 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -414,6 +414,10 @@ async def button_callback(self, interaction: discord.Interaction, button): if self.room.room_num == self.bottom_room_num: self.room.mmr_low = -999999 if isinstance(user_mmr, int) and self.room.mmr_high + 500 > user_mmr > self.room.mmr_low - 500: + mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high + mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low + if isinstance(user_mmr, int) and mmr_high + 500 > user_mmr > mmr_low - 500: + self.room.subs.append(interaction.user.id) button.disabled = True await interaction.followup.edit_message(interaction.message.id, view=self) mention = interaction.user.mention From c634d6391218d54ee21701eaece1c26d79078e76 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 23:07:28 -0500 Subject: [PATCH 121/292] See last commit --- mogi_objects.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index c4caa0d..5f33df1 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -409,11 +409,6 @@ async def button_callback(self, interaction: discord.Interaction, button): await interaction.followup.send( "MMR lookup for player has failed, please try again.", ephemeral=True) return - if self.room.room_num == 1: - self.room.mmr_high = 999999 - if self.room.room_num == self.bottom_room_num: - self.room.mmr_low = -999999 - if isinstance(user_mmr, int) and self.room.mmr_high + 500 > user_mmr > self.room.mmr_low - 500: mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low if isinstance(user_mmr, int) and mmr_high + 500 > user_mmr > mmr_low - 500: From 1a2efda107c987da79aa0330803c7ec391942e82 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 23:08:44 -0500 Subject: [PATCH 122/292] Prevent someone from subbing into room twice --- mogi_objects.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 5f33df1..854b7a9 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -155,6 +155,7 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.thread = thread self.view = None self.finished = False + self.subs: List["Player"] = [] @property def mmr_high(self) -> int: @@ -399,7 +400,7 @@ async def button_callback(self, interaction: discord.Interaction, button): await interaction.followup.send( "Players with the muted or restricted role cannot use the sub button.", ephemeral=True) return - if interaction.user.id in self.room.get_player_list(): + if interaction.user.id in self.room.get_player_list() + self.room.subs: await interaction.followup.send( "You are already in this room.", ephemeral=True) return @@ -409,6 +410,11 @@ async def button_callback(self, interaction: discord.Interaction, button): await interaction.followup.send( "MMR lookup for player has failed, please try again.", ephemeral=True) return + # Need a 2nd check to control the race condition introduced by "await self.get_mmr" + if interaction.user.id in self.room.get_player_list() + self.room.subs: + await interaction.followup.send( + "You are already in this room.", ephemeral=True) + return mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low if isinstance(user_mmr, int) and mmr_high + 500 > user_mmr > mmr_low - 500: From b196cff63c8b774bb3ac817c98f0a7f4e29652fc Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 23:33:37 -0500 Subject: [PATCH 123/292] Structure to Player object to support hosts --- mogi_objects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 1a790d3..a1d71d5 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -233,12 +233,14 @@ def __str__(self): class Player: - def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed=False): + def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed=False, host=False): self.member = member self.lounge_name = lounge_name self.mmr = mmr self.confirmed = confirmed self.score = 0 + self.host = host + self.host_fc = None @property def mention(self): From 2201bccceadb63b766b58c647d9ff40c0ac70d55 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Sun, 7 Jul 2024 23:34:45 -0500 Subject: [PATCH 124/292] Can host command interface support --- cogs/SquadQueue.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ba4d0dc..3b948a7 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -255,10 +255,19 @@ async def extend(self, interaction: discord.Interaction, minutes: int): mogi.additional_extension += timedelta(minutes=minutes) await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") + @app_commands.command(name="ch") + @app_commands.guild_only() + async def can_host(self, interaction: discord.Interaction): + """Join a mogi as a host""" + await self.join_queue(interaction, host=True) + @app_commands.command(name="c") @app_commands.guild_only() async def can(self, interaction: discord.Interaction): """Join a mogi""" + await self.join_queue(interaction) + + async def join_queue(self, interaction: discord.Interaction, host=False): await interaction.response.defer() async with self.LOCK: member = interaction.user @@ -268,9 +277,24 @@ async def can(self, interaction: discord.Interaction): return player_team = mogi.check_player(member) - - if player_team is not None: - await interaction.followup.send(f"{interaction.user.mention} is already signed up.") + player = None if player_team is None else player_team.get_player(member) + + if player is not None: + # The player is already signed up, but they might be changing to a host or non-host. Begin checks: + # The player was queued as host, and they queued again as a host + if player.host and host: + await interaction.followup.send(f"{interaction.user.mention} is already signed up as a host.") + # The player was queued as host, and they queued again as a non-host + elif player.host and not host: + await interaction.followup.send(f"{interaction.user.mention} has changed to a non-host.") + # The player was not queued as host, but they are changing to a host + elif not player.host and host: + await interaction.followup.send(f"{interaction.user.mention} has changed to a host.") + # The player was not queued as host and did not change to a host + elif not player.host and not host: + await interaction.followup.send(f"{interaction.user.mention} is already signed up.") + + player.host = host return msg = "" @@ -290,10 +314,11 @@ async def can(self, interaction: discord.Interaction): return players[0].confirmed = True + players[0].host = host squad = Team(players) mogi.teams.append(squad) - - msg += f"{players[0].lounge_name} joined queue closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + host_str = " as a host " if host else " " + msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" await interaction.followup.send(msg) await self.check_room_channels(mogi) From 8fb53c73e371e849a9ff71d4653105de42bbba79 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:53:13 -0500 Subject: [PATCH 125/292] Create host_fcs.py --- host_fcs.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 host_fcs.py diff --git a/host_fcs.py b/host_fcs.py new file mode 100644 index 0000000..438c6f0 --- /dev/null +++ b/host_fcs.py @@ -0,0 +1,44 @@ +import common +from typing import Dict +from collections.abc import Iterable +import aiohttp + +MKW_HOST_API_URL = f"https://mkwlounge.gg/api/hostfc.php?discord_guild_id={common.CONFIG['guild_id']}&discord_user_id=" +MKW_HOST_API_URL = f"https://mkwlounge.gg/api/hostfc.php?discord_guild_id=387347467332485122&discord_user_id=" + +SESSION = None + + +def create_new_session(): + global SESSION + SESSION = aiohttp.ClientSession() + + +async def _get_mk8dx_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: + return {} + + +async def _get_mkw_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: + request_url = MKW_HOST_API_URL + ",".join(discord_ids) + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5)) as session: + async with session.get(request_url) as r: + if r.status != 200: + print(f"hostfc endpoint returned status {r.status}") + return {} + results = await r.json() + if "status" not in results or results["status"] != "success": + print(f"hostfc endpoint returned unsuccessful results {results}") + return {} + host_mapping = {} + for item in results["success"]: + host_mapping[item["discord_user_id"]] = item["fc"] + return host_mapping + + +async def get_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: + if common.SERVER is common.Server.MKW: + return await _get_mkw_hosts(discord_ids) + elif common.SERVER is common.Server.MK8DX: + return await _get_mk8dx_hosts(discord_ids) + else: + raise ValueError("Bad config settings for server.") From a0ecb52c62bc337b2b81b70027795aa4da312890 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:55:42 -0500 Subject: [PATCH 126/292] Mogi.populate_host_fcs --- host_fcs.py | 2 +- mogi_objects.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/host_fcs.py b/host_fcs.py index 438c6f0..fa21d4a 100644 --- a/host_fcs.py +++ b/host_fcs.py @@ -30,7 +30,7 @@ async def _get_mkw_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: print(f"hostfc endpoint returned unsuccessful results {results}") return {} host_mapping = {} - for item in results["success"]: + for item in results["results"]: host_mapping[item["discord_user_id"]] = item["fc"] return host_mapping diff --git a/mogi_objects.py b/mogi_objects.py index a1d71d5..25bef0b 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -7,6 +7,7 @@ from datetime import datetime, timezone, timedelta from discord.ui import View from typing import List, Tuple, Callable +import host_fcs @@ -147,6 +148,14 @@ def get_room_from_thread(self, channel_id: int): return room return None + async def populate_host_fcs(self): + all_hosts = {str(plr.member.id): plr for plr in filter(lambda p: p.host, self.players_on_confirmed_teams())} + hosts = await host_fcs.get_hosts(all_hosts) + for host_discord_id, host_fc in hosts.items(): + player: Player = all_hosts.get(host_discord_id) + if player is not None: + player.host_fc = host_fc + class Room: def __init__(self, teams, room_num: int, thread: discord.Thread): From 36a73ddd2ba27a1cf1c659976a2cee290ea7ded7 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:56:07 -0500 Subject: [PATCH 127/292] Room.host_list to keep track of the room's host order --- mogi_objects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mogi_objects.py b/mogi_objects.py index 25bef0b..f8089fe 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -164,6 +164,7 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.thread = thread self.view = None self.finished = False + self.host_list: List["Player"] = [] @property def mmr_high(self) -> int: From 1902da7e107406858e9773241a2a82e1124b079b Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:57:13 -0500 Subject: [PATCH 128/292] Room.create_host_list Function to generate the host list depending on the rooms players who queued as host --- mogi_objects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index f8089fe..8dc232e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -193,6 +193,12 @@ def players(self) -> List[Player]: def get_player_list(self): return [player.member.id for team in self.teams for player in team.players] + def create_host_list(self): + all_hosts = list(filter(lambda p: p.host, self.players)) + random.shuffle(all_hosts) + self.host_list.clear() + self.host_list.extend(all_hosts) + class Team: def __init__(self, players: List["Player"]): From a487769be16b6d48da7ca9ca397b78f54ccdd3fe Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:58:03 -0500 Subject: [PATCH 129/292] Room.get_host_str Returns a nicely formatted string with the host order --- mogi_objects.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 8dc232e..6a95da3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -199,6 +199,21 @@ def create_host_list(self): self.host_list.clear() self.host_list.extend(all_hosts) + def get_host_str(self) -> str: + if len(self.host_list) == 0: + return "" + host_strs = [] + for i, player in enumerate(self.host_list, 1): + host_strs.append(f"{i}. {player.member.display_name}") + # First player on the list should be bold + if i == 1: + host_strs[0] = f"**{host_strs[0]}**" + result = f"Host: {', '.join(host_strs)}" + if common.SERVER is common.Server.MKW and self.host_list[0].host_fc is not None: + result += f"**Host ({self.host_list[0].member.display_name}) Friend Code: {self.host_list[0].host_fc}**" + return result + + class Team: def __init__(self, players: List["Player"]): From 85d5cb385238df7a09001f5f34a7dcb47ec8d606 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 00:59:00 -0500 Subject: [PATCH 130/292] Add mogi_objects.Room to VoteView --- cogs/SquadQueue.py | 2 +- mogi_objects.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 3b948a7..9e58daf 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -832,7 +832,7 @@ def allowed_players_check(players): {player_mentions} {extra_member_mentions}""" try: await curr_room.thread.send(room_msg) - view = VoteView(room_players, curr_room.thread, mogi, self.TIER_INFO) + view = VoteView(room_players, curr_room.thread, mogi, curr_room, self.TIER_INFO) curr_room.view = view await curr_room.thread.send(view=view) except discord.DiscordException: diff --git a/mogi_objects.py b/mogi_objects.py index 6a95da3..c0f09c3 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -288,11 +288,12 @@ def __lt__(self, other: Player): class VoteView(View): - def __init__(self, players, thread, mogi: Mogi, tier_info): + def __init__(self, players, thread, mogi: Mogi, room: Room, tier_info): super().__init__() self.players = players self.thread = thread self.mogi = mogi + self.room = room self.header_text = "" self.teams_text = "" self.found_winner = False From 6ec8faf069b33c4007214c3e0f3fec5bfd56611f Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 01:00:50 -0500 Subject: [PATCH 131/292] Populate host fcs and create the host list when queueing closes --- cogs/SquadQueue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9e58daf..acc0fda 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -815,6 +815,7 @@ def allowed_players_check(players): regular_player_list = all_confirmed_players[:first_late_player_index] late_player_list = all_confirmed_players[first_late_player_index:] proposed_list = sorted(mogi.generate_proposed_list(allowed_players_check), reverse=True) + await mogi.populate_host_fcs() for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): @@ -825,6 +826,7 @@ def allowed_players_check(players): else: curr_room = mogi.rooms[room_number - 1] curr_room.teams = [Team([p]) for p in room_players] + curr_room.create_host_list() player_mentions = " ".join([p.mention for p in room_players]) extra_member_mentions = " ".join([m.mention for m in extra_members if m is not None]) room_msg = f"""{msg} From 037f7001e1bec1f6468f55365575e4383a1aa3a9 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 01:01:51 -0500 Subject: [PATCH 132/292] When room begins, display the hosts appropriately --- mogi_objects.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index c0f09c3..29530a8 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -348,7 +348,11 @@ async def make_teams(self, format_): penalty_time = self.mogi.making_rooms_run_time + timedelta(minutes=8) room_open_time = self.mogi.making_rooms_run_time - msg += f"Decide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + potential_host_str = self.room.get_host_str() + if potential_host_str == "": + msg += f"Decide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + else: + msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" room.teams = teams From 735ed34ec9fed95c7c759d593b96e6b9bc3077b7 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 01:02:37 -0500 Subject: [PATCH 133/292] Textual bug fix --- mogi_objects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 29530a8..c717629 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -210,11 +210,10 @@ def get_host_str(self) -> str: host_strs[0] = f"**{host_strs[0]}**" result = f"Host: {', '.join(host_strs)}" if common.SERVER is common.Server.MKW and self.host_list[0].host_fc is not None: - result += f"**Host ({self.host_list[0].member.display_name}) Friend Code: {self.host_list[0].host_fc}**" + result += f"\n**Host ({self.host_list[0].member.display_name}) Friend Code: {self.host_list[0].host_fc}**" return result - class Team: def __init__(self, players: List["Player"]): self.players = players From eef42473536d4baaba0f3d3823edba1f4919b976 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 01:07:04 -0500 Subject: [PATCH 134/292] Change back to guild id for production --- host_fcs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/host_fcs.py b/host_fcs.py index fa21d4a..a107ab2 100644 --- a/host_fcs.py +++ b/host_fcs.py @@ -4,7 +4,6 @@ import aiohttp MKW_HOST_API_URL = f"https://mkwlounge.gg/api/hostfc.php?discord_guild_id={common.CONFIG['guild_id']}&discord_user_id=" -MKW_HOST_API_URL = f"https://mkwlounge.gg/api/hostfc.php?discord_guild_id=387347467332485122&discord_user_id=" SESSION = None From d34837a580f7e6e97372d65101e49cca7f17290c Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 01:09:07 -0500 Subject: [PATCH 135/292] Add extra line --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index c717629..c027293 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -349,7 +349,7 @@ async def make_teams(self, format_): room_open_time = self.mogi.making_rooms_run_time potential_host_str = self.room.get_host_str() if potential_host_str == "": - msg += f"Decide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + msg += f"\nDecide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" From 07ade2d6754a6c972f0da8c243a7fa85d35a1086 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 03:30:35 -0500 Subject: [PATCH 136/292] Thread name will update with tier in title --- mogi_objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 823d772..8d9ca31 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -291,7 +291,7 @@ class VoteView(View): def __init__(self, players, thread, mogi: Mogi, room: Room, tier_info): super().__init__() self.players = players - self.thread = thread + self.thread:discord.Thread = thread self.mogi = mogi self.room = room self.header_text = "" @@ -358,6 +358,9 @@ async def make_teams(self, format_): self.found_winner = True await self.thread.send(msg) + if common.SERVER is common.Server.MKW: + new_thread_name = self.thread.name + f" - T{get_tier(room_mmr, self.tier_info)}" + await self.thread.edit(name=new_thread_name) async def find_winner(self): if not self.found_winner: From 64a75a0d88a4bd63c5e078debf499eeb20f6e20f Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 14:37:53 -0500 Subject: [PATCH 137/292] Change mmr sub range to 700 --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 8d9ca31..574e4a6 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -457,7 +457,7 @@ async def button_callback(self, interaction: discord.Interaction, button): return mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low - if isinstance(user_mmr, int) and mmr_high + 500 > user_mmr > mmr_low - 500: + if isinstance(user_mmr, int) and mmr_high + 700 > user_mmr > mmr_low - 700: self.room.subs.append(interaction.user.id) button.disabled = True await interaction.followup.edit_message(interaction.message.id, view=self) From 6e01bced9a35a05014f59c2ca295a7b988a9b364 Mon Sep 17 00:00:00 2001 From: Bad Wolf Date: Mon, 8 Jul 2024 14:40:09 -0500 Subject: [PATCH 138/292] Update sub range text from 500 to 700 --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index acc0fda..a1b93f2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -390,11 +390,11 @@ async def sub(self, interaction: discord.Interaction): if bottom_room_num == 1: msg += f"Room 1 is looking for a sub with any mmr\n" elif room.room_num == 1: - msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - 500}\n" + msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - 700}\n" elif room.room_num == bottom_room_num: - msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 500}\n" + msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 700}\n" else: - msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 500}-{room.mmr_high + 500}\n" + msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 700}-{room.mmr_high + 700}\n" message_delete_date = datetime.now( timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" From 5dc72aceee6bf7c772be20d178aee6a998b858b4 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Mon, 8 Jul 2024 22:13:02 -0700 Subject: [PATCH 139/292] Start the first event of the day 1 hour sooner Shifted the first event time from 0:00 UTC to 23:00 UTC --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a1b93f2..6b2ae1e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -120,7 +120,7 @@ def __init__(self, bot): # This is the first event of the day's time. However, this isn't the first event the bot will run. This is literally the time of the first event in a daily schedule. self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), - tzinfo=timezone.utc) + timedelta( + tzinfo=timezone.utc) - timedelta(hours=1) + timedelta( minutes=bot.config["FIRST_EVENT_TIME"]) with open('./timezones.json', 'r') as cjson: From 1caaf2727bc8e8aa8d1bcf3126edd0236912f599 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Tue, 9 Jul 2024 11:45:53 -0700 Subject: [PATCH 140/292] Revert "Start the first event of the day 1 hour sooner" This reverts commit 5dc72aceee6bf7c772be20d178aee6a998b858b4. --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6b2ae1e..a1b93f2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -120,7 +120,7 @@ def __init__(self, bot): # This is the first event of the day's time. However, this isn't the first event the bot will run. This is literally the time of the first event in a daily schedule. self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), - tzinfo=timezone.utc) - timedelta(hours=1) + timedelta( + tzinfo=timezone.utc) + timedelta( minutes=bot.config["FIRST_EVENT_TIME"]) with open('./timezones.json', 'r') as cjson: From 723e37c9c474f6838f1505d7946e4769fc3e5e0e Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 13 Jul 2024 17:35:24 -0700 Subject: [PATCH 141/292] reimplemented all old mk8 queuebot features --- .gitignore | 1 + cogs/SquadQueue.py | 434 +++++++++++++----- example_configs/prod/mk8dx_prod_config.json | 46 ++ .../prod/mkw_ct_prod_config.json | 56 +-- .../prod/mkw_rt_prod_config.json | 58 +-- mmr.py | 88 +++- mogi_objects.py | 135 ++++-- test_config.json | 64 --- 8 files changed, 588 insertions(+), 294 deletions(-) create mode 100644 example_configs/prod/mk8dx_prod_config.json rename ct_config.json => example_configs/prod/mkw_ct_prod_config.json (60%) rename prod_config.json => example_configs/prod/mkw_rt_prod_config.json (59%) delete mode 100644 test_config.json diff --git a/.gitignore b/.gitignore index b73e9cf..aaa5a92 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /config.json /credentials.json .idea/* +/example_configs/test/* diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a1b93f2..133b87b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -11,8 +11,8 @@ import common from common import divide_chunks -from mmr import mkw_mmr, get_mmr_from_discord_id -from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier +from mmr import mk8dx_mmr, mk8dx_get_mmr_from_discord_id, mkw_mmr, mkw_get_mmr_from_discord_id +from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier, get_tier_mk8dx import asyncio from collections import defaultdict from typing import Dict, List, Tuple @@ -56,6 +56,8 @@ class SquadQueue(commands.Cog): def __init__(self, bot): self.bot: commands.Bot = bot + self.is_production = bot.config["is_production"] + self.next_event: Mogi = None self.ongoing_event: Mogi = None @@ -91,8 +93,14 @@ def __init__(self, bot): self.URL = bot.config["url"] + self.PLACEMENT_PLAYER_MMR = self.bot.config["PLACEMENT_PLAYER_MMR"] + + self.ROOM_JOIN_PENALTY_TIME = self.bot.config["ROOM_JOIN_PENALTY_TIME"] + self.MOGI_LIFETIME = bot.config["MOGI_LIFETIME"] + self.SUB_RANGE_MMR_ALLOWANCE = bot.config["SUB_RANGE_MMR_ALLOWANCE"] + self.SUB_MESSAGE_LIFETIME_SECONDS = bot.config["SUB_MESSAGE_LIFETIME_SECONDS"] self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] @@ -144,7 +152,8 @@ async def on_ready(self): self.bot.config["queue_history_channel"]) self.GENERAL_CHANNEL = self.bot.get_channel( self.bot.config["queue_general_channel"]) - await self.get_ladder_info() + if common.SERVER is common.Server.MKW: + await self.get_ladder_info() try: await self.LIST_CHANNEL.purge() except: @@ -162,7 +171,7 @@ async def on_ready(self): print(f"History Channel - {self.HISTORY_CHANNEL}", flush=True) print(f"General Channel - {self.GENERAL_CHANNEL}", flush=True) print("Ready!", flush=True) - + self.refresh_helper_roles.start() async def lockdown(self, channel: discord.TextChannel): @@ -226,7 +235,7 @@ def get_mogi(self, ctx: discord.Interaction | discord.ext.commands.context.Conte return self.ongoing_event def is_staff(self, member: discord.Member): - return any(member.get_role(staff_role) for staff_role in self.bot.config["staff_roles"]) + return any(member.get_role(staff_role) for staff_role in self.bot.config["admin_roles"]) async def is_started(self, ctx, mogi: Mogi): if not mogi.started: @@ -247,6 +256,8 @@ async def extend(self, interaction: discord.Interaction, minutes: int): ----------- minutes: int The number of minutes to add to the extension time. Can be negative. + + Staff use only. """ mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: @@ -271,13 +282,23 @@ async def join_queue(self, interaction: discord.Interaction, host=False): await interaction.response.defer() async with self.LOCK: member = interaction.user + url = self.URL + if not self.is_production: + if common.SERVER is common.Server.MK8DX: + # is actually a user and not a member + member = await self.bot.fetch_user(318637887597969419) + url = "https://www.mk8dx-lounge.com" + elif common.SERVER is common.Server.MKW: + member = await self.bot.fetch_user(82862780591378432) + url = "https://www.mkwlounge.gg" mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: await interaction.followup.send("Queue has not started yet.") return player_team = mogi.check_player(member) - player = None if player_team is None else player_team.get_player(member) + player = None if player_team is None else player_team.get_player( + member) if player is not None: # The player is already signed up, but they might be changing to a host or non-host. Begin checks: @@ -298,14 +319,18 @@ async def join_queue(self, interaction: discord.Interaction, host=False): return msg = "" - placement_role_id = 723753340063842345 - if member.get_role(placement_role_id): - starting_player_mmr = 750 - players = [Player(member, member.display_name, starting_player_mmr)] + placement_role_id = self.bot.config["placement_role_id"] + if self.is_production and member.get_role(placement_role_id): + starting_player_mmr = self.PLACEMENT_PLAYER_MMR + players = [ + Player(member, member.display_name, starting_player_mmr)] msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" else: - players = await mkw_mmr(self.URL, [member], self.TRACK_TYPE) + if common.SERVER is common.Server.MKW: + players = await mkw_mmr(url, [member], self.TRACK_TYPE) + elif common.SERVER is common.Server.MK8DX: + players = await mk8dx_mmr(url, [member]) if len(players) == 0 or players[0] is None: msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " @@ -338,7 +363,7 @@ async def drop(self, interaction: discord.Interaction): member = interaction.user squad = mogi.check_player(member) if squad is None: - await interaction.followup.send(f"{member.display_name} is not currently in this event; type `/c` to join") + await interaction.followup.send(f"{member.display_name} is not currently in this event; type `/c` or `/ch` to join") return mogi.teams.remove(squad) msg = "Removed " @@ -356,11 +381,12 @@ async def sub(self, interaction: discord.Interaction): """Sends out a request for a sub in the sub channel. Only works in thread channels for SQ rooms.""" current_time = time.time() lastCommandTime = cooldowns.get(interaction.user.id) - print(lastCommandTime, flush=True) + print(f"{interaction.user.name} requests a sub, previous sub command time: {lastCommandTime}", flush=True) if lastCommandTime == None: lastCommandTime = 0 - if (current_time - lastCommandTime) < 120 and not self.is_staff(interaction.user): # Cooldown timer in seconds + # Cooldown timer in seconds + if (current_time - lastCommandTime) < 120 and not self.is_staff(interaction.user): await interaction.response.send_message( f"You are still on cooldown. Please wait for {int(2 * 60 - (current_time - lastCommandTime))} more seconds to use this command again.", ephemeral=True) @@ -386,23 +412,35 @@ async def sub(self, interaction: discord.Interaction): f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) return - msg = "<@&1167985222533533817> - " + frequently_tagged_role_id = self.bot.config["frequently_tagged_role_id"] + msg = f"<@&{frequently_tagged_role_id}> - " if bottom_room_num == 1: msg += f"Room 1 is looking for a sub with any mmr\n" elif room.room_num == 1: - msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - 700}\n" + msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}\n" elif room.room_num == bottom_room_num: - msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + 700}\n" + msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" else: - msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - 700}-{room.mmr_high + 700}\n" + msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}-{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" message_delete_date = datetime.now( timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = JoinView(room, - lambda discord_id: get_mmr_from_discord_id(discord_id, self.TRACK_TYPE), - bottom_room_num, - lambda user: is_restricted(user, self.bot.config)) + view = None + if common.SERVER is common.Server.MKW: + view = JoinView(room, + lambda discord_id: mkw_get_mmr_from_discord_id( + discord_id, self.TRACK_TYPE, self.URL), + self.SUB_RANGE_MMR_ALLOWANCE, + bottom_room_num, + lambda user: is_restricted(user, self.bot.config)) + elif common.SERVER is common.Server.MK8DX: + view = JoinView(room, + lambda discord_id: mk8dx_get_mmr_from_discord_id( + discord_id, self.URL), + self.SUB_RANGE_MMR_ALLOWANCE, + bottom_room_num, + lambda user: is_restricted(user, self.bot.config)) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") @@ -416,21 +454,40 @@ async def list_task(self): await self.delete_list_messages(0) return all_confirmed_players = mogi.players_on_confirmed_teams() - first_late_player_index = (mogi.num_players // mogi.players_per_room) * mogi.players_per_room - on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) - late_players = all_confirmed_players[first_late_player_index:] + first_late_player_index = ( + mogi.num_players // mogi.players_per_room) * mogi.players_per_room + if common.SERVER is common.Server.MK8DX: + for i, player in enumerate(all_confirmed_players, 1): + if i > first_late_player_index: + player.is_late = True + else: + player.is_late = False msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" msg += "**Current Mogi List:**\n" - for i, player in enumerate(on_time_players, 1): - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" - if i % mogi.players_per_room == 0: - msg += "ㅤ\n" - if len(on_time_players) == 0: # Text looks better this way - msg += "\n" - msg += "**Late Players:**\n" - for i, player in enumerate(late_players, 1): - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + if common.SERVER is common.Server.MKW: + on_time_players = sorted( + all_confirmed_players[:first_late_player_index], reverse=True) + late_players = all_confirmed_players[first_late_player_index:] + for i, player in enumerate(on_time_players, 1): + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + if i % mogi.players_per_room == 0: + msg += "ㅤ\n" + if len(on_time_players) == 0: # Text looks better this way + msg += "\n" + msg += "**Late Players:**\n" + for i, player in enumerate(late_players, 1): + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + elif common.SERVER is common.Server.MK8DX: + all_confirmed_players.sort(reverse=True) + for i, player in enumerate(all_confirmed_players, 1): + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)" + if player.is_late: + msg += " (late)\n" + else: + msg += "\n" + if i % mogi.players_per_room == 0: + msg += "ㅤ\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" message = msg.split("\n") @@ -478,21 +535,60 @@ async def on_message(self, message: discord.Message): return mogi = None if self.ongoing_event is not None: - mogi = self.ongoing_event if self.ongoing_event.is_room_thread(message.channel.id) else None + mogi = self.ongoing_event if self.ongoing_event.is_room_thread( + message.channel.id) else None if mogi is None: - mogi = discord.utils.find(lambda m: m.is_room_thread(message.channel.id), self.old_events) + mogi = discord.utils.find(lambda m: m.is_room_thread( + message.channel.id), self.old_events) if mogi is None: return - room = discord.utils.find(lambda r: r.thread.id == message.channel.id, mogi.rooms) + room = discord.utils.find( + lambda r: r.thread.id == message.channel.id, mogi.rooms) if room is None or not room.teams: return - team = discord.utils.find(lambda t: t.has_player(message.author), room.teams) + team = discord.utils.find( + lambda t: t.has_player(message.author), room.teams) if team is None: return - player = discord.utils.find(lambda p: p.member.id == message.author.id, team.players) + player = discord.utils.find( + lambda p: p.member.id == message.author.id, team.players) if player is not None: player.score = int(message.content) + @app_commands.command(name="scoreboard") + @app_commands.guild_only() + async def scoreboard(self, interaction: discord.Interaction): + """Displays the scoreboard of the room. Only works in thread channels for SQ rooms.""" + if common.SERVER is not common.Server.MK8DX: + await interaction.response.send_message(f"Command is only usable for MK8DX.", ephemeral=True) + return + + if not isinstance(interaction.channel, discord.Thread): + await interaction.response.send_message(f"Cannot use this command here.", ephemeral=True) + return + + mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( + interaction.channel_id), self.old_events) + if not mogi: + await interaction.response.send_message(f"The Mogi object cannot be found.", ephemeral=True) + return + + room = discord.utils.find( + lambda room: room.thread.id == interaction.channel_id, mogi.rooms) + if not room: + await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) + return + + format = round(12/len(room.teams)) + + msg = f"!submit {format} {get_tier_mk8dx(room.mmr_average - 500)}\n" + for team in room.teams: + for player in team.players: + msg += f"{player.lounge_name} {player.score}\n" + if format != 1: + msg += "\n" + await interaction.response.send_message(msg) + @app_commands.command(name="remove_player") @app_commands.guild_only() async def remove_player(self, interaction: discord.Interaction, member: discord.Member): @@ -506,7 +602,7 @@ async def remove_player(self, interaction: discord.Interaction, member: discord. squad = mogi.check_player(member) if squad is None: - await interaction.followup.send(f"{member.display_name} is not currently in this event; type `/c` to join") + await interaction.followup.send(f"{member.display_name} is not currently in this event") return mogi.teams.remove(squad) msg = "Staff has removed " @@ -539,14 +635,29 @@ async def ping_staff_autocomplete(self, interaction: discord.Interaction, curren for role_name in self.helper_staff_roles if current.lower() in role_name.lower() ] + def allow_new_mogis(self): + self.LAUNCH_NEW_EVENTS = True + @app_commands.command(name="annul_current_mogi") @app_commands.guild_only() - async def annul_current_mogi(self, interaction: discord.Interaction): - """The mogi currently gathering will be deleted. The queue resumes at the next hour. Staff use only.""" + async def annul_current_mogi(self, interaction: discord.Interaction, resume_mogis_after: bool): + """The mogi currently gathering will be deleted. resume_mogis_after determines whether future mogis will be scheduled. Staff use only.""" self.next_event = None self.ongoing_event = None + self.LAUNCH_NEW_EVENTS = False + msg = "The current mogi has been cancelled, " + if resume_mogis_after: + next_event_open_time = self.compute_next_event_open_time() + next_event_start_time = ( + next_event_open_time + self.QUEUE_OPEN_TIME) + delay = (next_event_start_time - + datetime.now(timezone.utc)).total_seconds() + asyncio.get_event_loop().call_later(delay, self.allow_new_mogis) + msg += f"the queue will resume at {next_event_start_time}." + else: + msg += "future mogis will not be started." await self.lockdown(self.MOGI_CHANNEL) - await interaction.response.send_message("The current Mogi has been canceled, the queue will resume at the next hour.") + await interaction.response.send_message(msg) @app_commands.command(name="pause_mogi_scheduling") @app_commands.guild_only() @@ -565,10 +676,11 @@ async def resume_mogi_scheduling(self, interaction: discord.Interaction): @app_commands.command(name="change_event_time") @app_commands.guild_only() async def change_event_time(self, interaction: discord.Interaction, event_time: int): - """Change the amount of time for each event in the queue.""" + """Change the amount of time for each event in the queue. Staff use only.""" if event_time > 15: self.QUEUE_OPEN_TIME = timedelta(minutes=event_time) - self.JOINING_TIME = timedelta(minutes=event_time) - self.EXTENSION_TIME + self.JOINING_TIME = timedelta( + minutes=event_time) - self.EXTENSION_TIME await interaction.response.send_message( f"The amount of time for each mogi has been changed to {event_time} minutes.") else: @@ -577,7 +689,7 @@ async def change_event_time(self, interaction: discord.Interaction, event_time: @app_commands.command(name="change_mmr_threshold") @app_commands.guild_only() async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_threshold: int): - """Change the maximum MMR gap allowed for a room.""" + """Change the maximum MMR gap allowed for a room. Staff use only.""" self.room_mmr_threshold = mmr_threshold try: with open(MMR_THRESHOLD_PKL, 'wb') as f: @@ -589,10 +701,57 @@ async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_thres @app_commands.command(name="peek_bot_config") @app_commands.guild_only() async def peek_bot_config(self, interaction: discord.Interaction): - """View the configured values for the Queue System.""" + """View the configured values for the Queue System. Staff use only.""" msg = "" - msg += f"Time between each event: {self.QUEUE_OPEN_TIME}, \n" - msg += f"Room MMR Threshold: {self.room_mmr_threshold}" + msg += f"LOUNGE TYPE: {self.bot.config['lounge']}\n" + if common.SERVER is common.Server.MKW: + msg += f"TRACK TYPE: {self.TRACK_TYPE}\n" + msg += f"CURRENT SERVER: {self.GUILD}\n" + msg += f"GENERAL CHANNEL: {self.GENERAL_CHANNEL}\n" + msg += f"LIST CHANNEL: {self.LIST_CHANNEL}\n" + msg += f"JOIN CHANNEL: {self.MOGI_CHANNEL}\n" + msg += f"SUB CHANNEL: {self.SUB_CHANNEL}\n" + msg += f"HISTORY CHANNEL: {self.HISTORY_CHANNEL}\n" + + admins = [] + for m in self.bot.config["admin_roles"]: + role = self.GUILD.get_role(m) + if role: + admins.append(role.name) + msg += f"ADMIN ROLES: {', '.join(admins)}\n" + + helper_staff = [] + for m in self.bot.config["helper_staff_roles"]: + role = self.GUILD.get_role(m) + if role: + helper_staff.append(role.name) + msg += f"HELPER STAFF ROLES: {', '.join(helper_staff)}\n" + + members_for_channels = [] + for m in self.bot.config["members_for_channels"]: + member = self.GUILD.get_member(m) + if member: + members_for_channels.append(member.name) + msg += f"MEMBERS ADDED TO EACH ROOM: {', '.join(members_for_channels)}\n" + + roles_for_channels = [] + for m in self.bot.config["roles_for_channels"]: + role = self.GUILD.get_role(m) + if role: + roles_for_channels.append(role.name) + msg += f"ROLES ADDED TO EACH ROOM: {', '.join(roles_for_channels)}\n" + + msg += f"PLACEMENT PLAYER STARTING MMR: {self.PLACEMENT_PLAYER_MMR}\n" + msg += f"SITE URL: {self.URL}\n" + msg += f"FIRST EVENT TIME: {self.FIRST_EVENT_TIME}\n" + msg += f"TIME BETWEEN EVENTS: {self.QUEUE_OPEN_TIME} minutes\n" + msg += f"EVENT JOINING TIME: {self.JOINING_TIME} minutes\n" + msg += f"EXTENSION TIME: {self.EXTENSION_TIME} minutes\n" + msg += f"ROOM JOIN PENALTY TIME: {self.ROOM_JOIN_PENALTY_TIME} minutes\n" + msg += f"EVENT LIFETIME: {self.MOGI_LIFETIME} minutes\n" + msg += f"EXTRA SUB RANGE ALLOWANCE: +- {self.SUB_RANGE_MMR_ALLOWANCE} MMR\n" + msg += f"SUB MESSAGE LIFETIME: {self.SUB_MESSAGE_LIFETIME_SECONDS} seconds\n" + msg += f"ROOM MMR THRESHOLD: {self.room_mmr_threshold}" await interaction.response.send_message(msg) @app_commands.command(name="reset_bot") @@ -609,13 +768,13 @@ async def reset_bot(self, interaction: discord.Interaction): @commands.guild_only() async def schedule_sq_times(self, ctx, timestamps: commands.Greedy[int]): """Saves a list of sq times to skip over. Input a list of unix utc timestamps. Staff use only.""" - if not await self.has_roles(ctx.author, ctx.guild.id, ctx.bot.config): + if not self.is_staff(ctx.author) and not await self.bot.is_owner(ctx.author): + self.queue_or_send( + ctx, "You do not have permission to use this command.") return - await self.queue_or_send(ctx, - "This command has been disabled as it's unclear how the new changes might affect this system.") - return - msg = "List of new Dates:\n" + msg = "The queue will skip events THAT START AT THE TIMESTAMP (not when the queue pops)\n" + msg += "List of new Dates:\n" curr_time = datetime.now(timezone.utc) new_sq_dates = [] @@ -663,8 +822,7 @@ async def clear_sq_times(self, interaction: discord.Interaction): @app_commands.command(name="update_tier_info") @app_commands.guild_only() async def update_tier_info(self, interaction: discord.Interaction): - """Updates the info on each tier""" - #await self.get_ladder_info() + """Updates the mmr divisions that denote each tier. Staff use only.""" msg = await self.get_ladder_info() await interaction.response.send_message(msg) @@ -703,15 +861,6 @@ async def get_ladder_info(self): except Exception as e: print(traceback.format_exc()) - # check if user has roles defined in config.json - async def has_roles(self, member: discord.Member, guild_id: int, config): - if str(guild_id) not in config["admin_roles"].keys(): - return True - for role in member.roles: - if role.name in config["admin_roles"][str(guild_id)]: - return True - return False - @staticmethod async def end_voting(mogi: Mogi): """Ends voting in all rooms with ongoing votes.""" @@ -719,7 +868,8 @@ async def end_voting(mogi: Mogi): if mogi is not None: for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: - print(f"Skipping room {index} in function end_voting.", flush=True) + print( + f"Skipping room {index} in function end_voting.", flush=True) continue await room.view.find_winner() except Exception as e: @@ -730,10 +880,15 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): """Writes the teams, tier and average of each room per hour.""" try: if mogi is not None: - await history_channel.send(f"{discord.utils.format_dt(mogi.start_time)} Rooms") + # I want the time displayed to be at the hour but the current way reads as :55 minutes on each queue which looks bad... + room_display_time = mogi.start_time + if common.SERVER is common.Server.MK8DX: + room_display_time = mogi.start_time + timedelta(minutes=5) + await history_channel.send(f"{discord.utils.format_dt(room_display_time)} Rooms") for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: - print(f"Skipping room {index} in function write_history.", flush=True) + print( + f"Skipping room {index} in function write_history.", flush=True) continue msg = room.view.header_text msg += f"{room.thread.jump_url}\n" @@ -750,10 +905,13 @@ async def check_room_channels(self, mogi): if num_created_rooms >= mogi.max_possible_rooms: return for i in range(num_created_rooms, mogi.max_possible_rooms): - minute = mogi.start_time.minute + start_time = mogi.start_time + if common.SERVER is common.Server.MK8DX: + start_time += timedelta(minutes=5) + minute = start_time.minute if len(str(minute)) == 1: minute = '0' + str(minute) - room_name = f"{mogi.start_time.month}/{mogi.start_time.day}, {mogi.start_time.hour}:{minute}:00 - Room {i+1}" + room_name = f"{start_time.month}/{start_time.day}, {start_time.hour}:{minute}:00 - Room {i+1}" try: room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, @@ -767,7 +925,8 @@ async def check_room_channels(self, mogi): @staticmethod async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): - await asyncio.sleep(120) # We could have used asyncio.call_later(120, handle_voting_and_history) in the caller's code + # We could have used asyncio.call_later(120, handle_voting_and_history) in the caller's code + await asyncio.sleep(120) await SquadQueue.end_voting(mogi) await SquadQueue.write_history(mogi, history_channel) @@ -793,11 +952,9 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic extra_members = [] - extra_members_ids = self.bot.config["members_for_channels"].get(str(mogi.mogi_channel.guild.id), []) - for m in extra_members_ids: + for m in self.bot.config["members_for_channels"]: extra_members.append(mogi.mogi_channel.guild.get_member(m)) - extra_roles_ids = self.bot.config["roles_for_channels"].get(str(mogi.mogi_channel.guild.id), []) - for r in extra_roles_ids: + for r in self.bot.config["roles_for_channels"]: extra_members.append(mogi.mogi_channel.guild.get_role(r)) # Support for both server to implement their own algorithm for a player list being allowed @@ -811,11 +968,14 @@ def allowed_players_check(players): return mk8dx_players_allowed(players, self.room_mmr_threshold) all_confirmed_players = mogi.players_on_confirmed_teams() - first_late_player_index = (mogi.num_players//mogi.players_per_room) * mogi.players_per_room + first_late_player_index = ( + mogi.num_players//mogi.players_per_room) * mogi.players_per_room regular_player_list = all_confirmed_players[:first_late_player_index] late_player_list = all_confirmed_players[first_late_player_index:] - proposed_list = sorted(mogi.generate_proposed_list(allowed_players_check), reverse=True) - await mogi.populate_host_fcs() + proposed_list = sorted(mogi.generate_proposed_list( + allowed_players_check), reverse=True) + if common.SERVER is common.Server.MKW: + await mogi.populate_host_fcs() for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): @@ -828,13 +988,25 @@ def allowed_players_check(players): curr_room.teams = [Team([p]) for p in room_players] curr_room.create_host_list() player_mentions = " ".join([p.mention for p in room_players]) - extra_member_mentions = " ".join([m.mention for m in extra_members if m is not None]) - room_msg = f"""{msg} + extra_member_mentions = " ".join( + [m.mention for m in extra_members if m is not None]) + room_msg = "" + if common.SERVER is common.Server.MKW: + room_msg += f"""{msg} Vote for format FFA, 2v2, 3v3, 4v4, or 6v6. -{player_mentions} {extra_member_mentions}""" +{player_mentions} {extra_member_mentions} + +If you need staff's assistance, use the `/ping_staff` command in this channel.""" + elif common.SERVER is common.Server.MK8DX: + room_msg += f"""{msg} +Vote for format FFA, 2v2, 3v3, 4v4. +{player_mentions} {extra_member_mentions} + +If you need staff's assistance, use the `/ping_staff` command in this channel.""" try: await curr_room.thread.send(room_msg) - view = VoteView(room_players, curr_room.thread, mogi, curr_room, self.TIER_INFO) + view = VoteView(room_players, curr_room.thread, + mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME, self.TIER_INFO) curr_room.view = view await curr_room.thread.send(view=view) except discord.DiscordException: @@ -846,11 +1018,13 @@ def allowed_players_check(players): try: await mogi.mogi_channel.send(msg) except discord.DiscordException: - print(f"Mogi Channel message for room {room_number} has failed to send.", flush=True) + print( + f"Mogi Channel message for room {room_number} has failed to send.", flush=True) print(traceback.format_exc()) # Compute the list of "late" players that didn't get into any room - not_in_proposed_list = [p for p in all_confirmed_players if p not in proposed_list] + not_in_proposed_list = [ + p for p in all_confirmed_players if p not in proposed_list] if len(not_in_proposed_list) > 0: msg = "`Late players:`\n" for i, player in enumerate(not_in_proposed_list, 1): @@ -864,7 +1038,8 @@ def allowed_players_check(players): # We could have used asyncio.call_later(120, handle_voting_and_history) # and removed asyncio.sleep(120) in handle_voting_and_history - asyncio.create_task(SquadQueue.handle_voting_and_history(self.ongoing_event, self.HISTORY_CHANNEL)) + asyncio.create_task(SquadQueue.handle_voting_and_history( + self.ongoing_event, self.HISTORY_CHANNEL)) self.old_events.append(self.ongoing_event) self.ongoing_event = None @@ -873,7 +1048,8 @@ async def check_num_teams(self, mogi: Mogi): return cur_time = datetime.now(timezone.utc) if mogi.start_time <= cur_time: - num_leftover_teams = mogi.count_registered() % int((12 / mogi.max_player_per_team)) + num_leftover_teams = mogi.count_registered() % int( + (12 / mogi.max_player_per_team)) if num_leftover_teams == 0: mogi.gathering = False await self.lockdown(mogi.mogi_channel) @@ -888,18 +1064,22 @@ async def ongoing_mogi_checks(self): if not mogi.is_automated or not mogi.started or mogi.making_rooms_run: return cur_time = datetime.now(timezone.utc) - force_start_time = mogi.start_time + self.EXTENSION_TIME + mogi.additional_extension + force_start_time = mogi.start_time + \ + self.EXTENSION_TIME + mogi.additional_extension if force_start_time <= cur_time: mogi.gathering = False elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % int((12 / mogi.max_player_per_team)) + num_leftover_teams = mogi.count_registered() % int( + (12 / mogi.max_player_per_team)) if num_leftover_teams == 0: mogi.gathering = False else: if int(cur_time.second / 20) == 0: - minutes_left = (force_start_time - cur_time).seconds // 60 - x_teams = int(int(12 / mogi.max_player_per_team) - num_leftover_teams) + minutes_left = (force_start_time - + cur_time).seconds // 60 + x_teams = int( + int(12 / mogi.max_player_per_team) - num_leftover_teams) await mogi.mogi_channel.send( f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") if not mogi.gathering: @@ -911,14 +1091,16 @@ async def scheduler_mogi_start(self): cur_time = datetime.now(timezone.utc) next_mogi = self.next_event if next_mogi is not None and (next_mogi.start_time - self.JOINING_TIME) < cur_time: - self.next_event = None # We are trying to launch the event - fail or not, we set next event to None + # We are trying to launch the event - fail or not, we set next event to None + self.next_event = None # There is an ongoing event, but people are still queueing, so remove it and fail if self.ongoing_event is not None and self.ongoing_event.gathering: await next_mogi.mogi_channel.send( f"Because there is an ongoing event right now, the following event has been removed:\n{self.get_event_str(next_mogi)}\n") return # There is an ongoing event, but it has already started, so add it to the old events - if self.ongoing_event is not None and self.ongoing_event.started: # This is potentially an issue - we should be checking if make rooms has been run, not if it's started...? + # This is potentially an issue - we should be checking if make rooms has been run, not if it's started...? + if self.ongoing_event is not None and self.ongoing_event.started: self.old_events.append(self.ongoing_event) # Put the next mogi as the current event and launch it @@ -927,7 +1109,7 @@ async def scheduler_mogi_start(self): self.ongoing_event.gathering = True await self.unlockdown(self.ongoing_event.mogi_channel) await self.ongoing_event.mogi_channel.send( - f"A queue is gathering for the mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, and `/d` to drop.") + f"A queue is gathering for the mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") @tasks.loop(seconds=20.0) async def sqscheduler(self): @@ -953,7 +1135,8 @@ def compute_next_event_open_time(self): cur_time = datetime.now(timezone.utc) time_elapsed = cur_time - self.FIRST_EVENT_TIME num_launched_events = time_elapsed // self.QUEUE_OPEN_TIME - next_event_open_time = self.FIRST_EVENT_TIME + (self.QUEUE_OPEN_TIME * num_launched_events) + next_event_open_time = self.FIRST_EVENT_TIME + \ + (self.QUEUE_OPEN_TIME * num_launched_events) return next_event_open_time async def schedule_que_event(self): @@ -969,8 +1152,18 @@ async def schedule_que_event(self): return if len(self.sq_times) > 0 and next_event_open_time == self.sq_times[0]: self.sq_times.pop(0) + self.next_event = None + self.ongoing_event = None + self.LAUNCH_NEW_EVENTS = False + next_event_start_time = next_event_open_time + self.QUEUE_OPEN_TIME + delay = (next_event_start_time - + datetime.now(timezone.utc)).total_seconds() + print(next_event_start_time, delay, flush=True) + asyncio.get_event_loop().call_later(delay, self.allow_new_mogis) + await self.lockdown(self.MOGI_CHANNEL) await self.MOGI_CHANNEL.send( - "Squad Queue is currently going on at this hour! The queue will remain closed.") + "Squad Queue is currently going on! The queue will remain closed.") + return self.next_event = Mogi(sq_id=1, max_players_per_team=1, players_per_room=12, @@ -986,9 +1179,11 @@ async def delete_old_mogis(self): try: curr_time = datetime.now(timezone.utc) mogi_lifetime = timedelta(minutes=self.MOGI_LIFETIME) - delete_queue = [m for m in self.old_events if (curr_time - mogi_lifetime) > m.start_time] + delete_queue = [m for m in self.old_events if ( + curr_time - mogi_lifetime) > m.start_time] for mogi in delete_queue: - print(f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) + print( + f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) self.old_events.remove(mogi) except Exception as e: print(traceback.format_exc()) @@ -1003,7 +1198,8 @@ async def refresh_helper_roles(self): all_roles = await self.GUILD.fetch_roles() updated_roles = {} for role_id in helper_staff_role_ids: - needle: discord.Role = discord.utils.find(lambda n: role_id == n.id, all_roles) + needle: discord.Role = discord.utils.find( + lambda n: role_id == n.id, all_roles) if needle is not None: updated_roles[needle.name] = needle # As a fail safe, if we didn't add any new roles, either due to a misconfiguration or an internal discord @@ -1015,7 +1211,6 @@ async def refresh_helper_roles(self): except Exception as e: print(traceback.format_exc()) - def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( @@ -1044,9 +1239,22 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): or not await self.is_gathering(ctx, mogi)): return - check_players = [ctx.author] + member = ctx.author + url = self.URL + if not self.is_production: + if common.SERVER is common.Server.MK8DX: + member = await self.bot.fetch_user(318637887597969419) + url = "https://www.mk8dx-lounge.com" + elif common.SERVER is common.Server.MKW: + member = await self.bot.fetch_user(82862780591378432) + url = "https://www.mkwlounge.gg" + check_players = [member] check_players.extend(members) - players = await mkw_mmr(self.URL, check_players, self.TRACK_TYPE) + players = [] + if common.SERVER is common.Server.MKW: + players = await mkw_mmr(url, check_players, self.TRACK_TYPE) + elif common.SERVER is common.Server.MK8DX: + players = await mk8dx_mmr(url, check_players) for i in range(0, 12): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) @@ -1069,7 +1277,8 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): return for i, rating in enumerate(ratings, 1): if rating.isdecimal(): - player = Player(ctx.author, f"{ctx.author.display_name} {i}", int(rating), confirmed=True) + player = Player(ctx.author, f"{ctx.author.display_name} {i}", int( + rating), confirmed=True) mogi.teams.append(Team([player])) msg = f"Players added with the following ratings: {' '.join(['`' + r + '`' for r in ratings])}" await self.queue_or_send(ctx, msg) @@ -1087,9 +1296,22 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem or not await self.is_gathering(ctx, mogi)): return - check_players = [ctx.author] + member = ctx.author + url = self.URL + if not self.is_production: + if common.SERVER is common.Server.MK8DX: + member = await self.bot.fetch_user(318637887597969419) + url = "https://www.mk8dx-lounge.com" + elif common.SERVER is common.Server.MKW: + member = await self.bot.fetch_user(82862780591378432) + url = "https://www.mkwlounge.gg" + check_players = [member] check_players.extend(members) - players = await mkw_mmr(self.URL, check_players, self.TRACK_TYPE) + players = [] + if common.SERVER is common.Server.MKW: + players = await mkw_mmr(url, check_players, self.TRACK_TYPE) + elif common.SERVER is common.Server.MK8DX: + players = await mk8dx_mmr(url, check_players) for i in range(0, num_times): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) @@ -1115,7 +1337,3 @@ async def debug_start_rooms(self, ctx): async def setup(bot: commands.Bot): await bot.add_cog(SquadQueue(bot)) - - - - diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json new file mode 100644 index 0000000..94ac015 --- /dev/null +++ b/example_configs/prod/mk8dx_prod_config.json @@ -0,0 +1,46 @@ +{ + "token": "", + "is_production": true, + "lounge": "MK8DX", + "lounge_description": "Valid options are MKW or MK8DX", + "guild_id": 445404006177570829, + "queue_join_channel": 1186158671525318719, + "queue_sub_channel": 1186158767029620766, + "queue_list_channel": 1221665101904478340, + "queue_history_channel": 1194440106266865684, + "queue_general_channel": 1186158671525318719, + "admin_roles": [ + 464610815299944448, + 910528103225389107 + ], + "helper_staff_roles": [ + 910528103225389107 + ], + "members_for_channels": [ + 934740707934081035, + 743532508343304242, + 735910635082219570 + ], + "roles_for_channels": [], + "PLACEMENT_PLAYER_MMR": 1500, + "placement_role_id": 730980761322389504, + "frequently_tagged_role_id": 682445864400453739, + "restricted_role_id": 600495108999086090, + "muted_role_id": 600495108999086090, + "queue_messages": true, + "sec_between_queue_msgs": 2, + "username": "username", + "password": "password", + "url": "https://www.mk8dx-lounge.com", + "track_type": "rt", + "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", + "QUEUE_OPEN_TIME": 60, + "JOINING_TIME": 55, + "EXTENSION_TIME": 3, + "ROOM_JOIN_PENALTY_TIME": 5, + "MOGI_LIFETIME": 180, + "SUB_RANGE_MMR_ALLOWANCE": 500, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200, + "ROOM_MMR_THRESHOLD": 9999999 +} \ No newline at end of file diff --git a/ct_config.json b/example_configs/prod/mkw_ct_prod_config.json similarity index 60% rename from ct_config.json rename to example_configs/prod/mkw_ct_prod_config.json index 0c2d970..91048a7 100644 --- a/ct_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -1,51 +1,41 @@ { "token": "", + "is_production": true, "lounge": "MKW", "lounge_description": "Valid options are MKW or MK8DX", "guild_id": 387347467332485122, - "admin_roles": { - "387347467332485122": [ - "Administrator", - "Lounge Staff" - ] - }, "queue_join_channel": 1253109700107567155, "queue_sub_channel": 1253109752418930779, "queue_list_channel": 1253109466027655338, "queue_history_channel": 1253109800196243618, "queue_general_channel": 1253109398193045534, - "members_for_channels": { - "387347467332485122": [ - 235148962103951360, - 155149108183695360, - 815483289515196416, - 892420397570592768, - 775415990238314518, - 774812906830299136, - 745443073202323586, - 774866940950872095, - 1019051989734273054 - ] - }, - "roles_for_channels": { - "387347467332485122": [ - 520808645252874240, - 520808674411937792 - ] - }, - "staff_roles": [ - 387347888935534593, - 399382503825211393, - 399384750923579392, - 521149807994208295, - 792891432301625364, - 877629421811540016 + "admin_roles": [ + 387347888935534593, + 877629421811540016 ], "helper_staff_roles": [ 520808674411937792, 792891432301625364, 521149807994208295 ], + "members_for_channels": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095, + 1019051989734273054 + ], + "roles_for_channels": [ + 520808645252874240, + 520808674411937792 + ], + "PLACEMENT_PLAYER_MMR": 750, + "placement_role_id": 723753340063842345, + "frequently_tagged_role_id": 1167985222533533817, "restricted_role_id": 797208908153618452, "muted_role_id": 434887701662007296, "queue_messages": true, @@ -59,7 +49,9 @@ "QUEUE_OPEN_TIME": 90, "JOINING_TIME": 85, "EXTENSION_TIME": 5, + "ROOM_JOIN_PENALTY_TIME": 8, "MOGI_LIFETIME": 180, + "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 9000 } \ No newline at end of file diff --git a/prod_config.json b/example_configs/prod/mkw_rt_prod_config.json similarity index 59% rename from prod_config.json rename to example_configs/prod/mkw_rt_prod_config.json index 682f7fe..3b65358 100644 --- a/prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -1,51 +1,41 @@ { "token": "", + "is_production": true, "lounge": "MKW", "lounge_description": "Valid options are MKW or MK8DX", "guild_id": 387347467332485122, - "admin_roles": { - "387347467332485122": [ - "Administrator", - "Lounge Staff" - ] - }, "queue_join_channel": 1215174119767932979, "queue_sub_channel": 1215174160234315806, "queue_list_channel": 1215174057453162496, "queue_history_channel": 1225709377747685477, "queue_general_channel": 1241842559429513361, - "members_for_channels": { - "387347467332485122": [ - 235148962103951360, - 155149108183695360, - 815483289515196416, - 892420397570592768, - 775415990238314518, - 774812906830299136, - 745443073202323586, - 774866940950872095, - 1019051989734273054 - ] - }, - "roles_for_channels": { - "387347467332485122": [ - 393600567781621761, - 389252697284542465 - ] - }, - "staff_roles": [ - 387347888935534593, - 399382503825211393, - 399384750923579392, - 521149807994208295, - 792891432301625364, - 877629421811540016 + "admin_roles": [ + 387347888935534593, + 877629421811540016 ], "helper_staff_roles": [ 389252697284542465, 399384750923579392, 399382503825211393 ], + "members_for_channels": [ + 235148962103951360, + 155149108183695360, + 815483289515196416, + 892420397570592768, + 775415990238314518, + 774812906830299136, + 745443073202323586, + 774866940950872095, + 1019051989734273054 + ], + "roles_for_channels": [ + 393600567781621761, + 389252697284542465 + ], + "PLACEMENT_PLAYER_MMR": 750, + "placement_role_id": 723753340063842345, + "frequently_tagged_role_id": 1167985222533533817, "restricted_role_id": 797208908153618452, "muted_role_id": 434887701662007296, "queue_messages": true, @@ -59,7 +49,9 @@ "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, "EXTENSION_TIME": 5, + "ROOM_JOIN_PENALTY_TIME": 8, "MOGI_LIFETIME": 180, + "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 9000 + "ROOM_MMR_THRESHOLD": 8000 } \ No newline at end of file diff --git a/mmr.py b/mmr.py index c938b55..87269ac 100644 --- a/mmr.py +++ b/mmr.py @@ -5,6 +5,53 @@ headers = {'Content-type': 'application/json'} +# class LoungeData: +# def __init__(self): +# self._data = None + +# async def lounge_api_full(self): +# async with aiohttp.ClientSession() as session: +# async with session.get("https://www.mk8dx-lounge.com/api/player/list") as response: +# if response.status == 200: +# _data_full = await response.json() +# self._data = [ +# player for player in _data_full['players'] if "discordId" in player] + +# def data(self): +# return self._data + + +# lounge_data = LoungeData() + + +async def mk8dx_mmr(url, members): + base_url = url + '/api/player?' + players = [] + timeout = aiohttp.ClientTimeout(total=10) + try: + async with aiohttp.ClientSession( + timeout=timeout, + auth=aiohttp.BasicAuth( + "username", "password")) as session: + for member in members: + request_text = f"discordId={member.id}" + request_url = base_url + request_text + async with session.get(request_url, headers=headers) as resp: + if resp.status != 200: + players.append(None) + continue + player_data = await resp.json() + if 'mmr' not in player_data.keys(): + players.append( + Player(member, player_data['name'], None)) + continue + players.append( + Player(member, player_data['name'], player_data['mmr'])) + except: + print(f"Fetch for player {members[0]} has failed.", flush=True) + return players + + async def mkw_mmr(url, members, track_type): tracks = track_type base_url = url + '/api/ladderplayer.php?ladder_type=' + tracks + '&' @@ -35,9 +82,28 @@ async def mkw_mmr(url, members, track_type): return players -async def get_mmr_from_discord_id(discord_id, track_type): +async def mk8dx_get_mmr_from_discord_id(discord_id, url): + base_url = url + '/api/player?' + timeout = aiohttp.ClientTimeout(total=10) + async with aiohttp.ClientSession( + timeout=timeout, + auth=aiohttp.BasicAuth( + "username", "password")) as session: + request_text = f"discordId={discord_id}" + request_url = base_url + request_text + async with session.get(request_url, headers=headers) as resp: + if resp.status != 200: + return "Player does not exist" + player_data = await resp.json() + if 'mmr' not in player_data.keys(): + return "Player has no mmr" + return player_data['mmr'] + + +async def mkw_get_mmr_from_discord_id(discord_id, track_type, url): tracks = track_type - base_url = "https://www.mkwlounge.gg" + '/api/ladderplayer.php?ladder_type=' + tracks + '&' + base_url = url + \ + '/api/ladderplayer.php?ladder_type=' + tracks + '&' timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession( timeout=timeout, @@ -53,21 +119,3 @@ async def get_mmr_from_discord_id(discord_id, track_type): if 'current_mmr' not in player_data.keys(): return "Player has no mmr" return player_data['current_mmr'] - - -async def get_mmr(config, members): - return await mk8dx_150cc_mmr(config, members) - - -async def mk8dx_150cc_fc(config, name): - base_url = config["url"] + '/api/player?' - request_url = base_url + f'name={name}' - timeout = aiohttp.ClientTimeout(total=15) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(request_url, headers=headers) as resp: - if resp.status != 200: - return None - player_data = await resp.json() - if 'switchFc' not in player_data.keys(): - return None - return player_data['switchFc'] diff --git a/mogi_objects.py b/mogi_objects.py index 574e4a6..d3b87dd 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -10,7 +10,6 @@ import host_fcs - def average(list_: List[int | float]) -> float: return sum(list_) / len(list_) @@ -35,7 +34,8 @@ def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, self.rooms: List[Room] = [] self.is_automated = is_automated self.start_time = start_time if is_automated else None - self.additional_extension = timedelta(minutes=additional_extension_minutes) + self.additional_extension = timedelta( + minutes=additional_extension_minutes) @property def num_players(self): @@ -73,7 +73,8 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N cur_range = highest_player.mmr - lowest_player.mmr if cur_range < cur_min: cur_min = cur_range - best_collection = sorted_players[lowest_player_index:lowest_player_index + num_players] + best_collection = sorted_players[lowest_player_index: + lowest_player_index + num_players] return best_collection def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[Player], int]: @@ -88,7 +89,8 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla late_players = list(confirmed_players[self.players_per_room:]) while True: - best_collection = Mogi._minimize_range(cur_check_list, self.players_per_room) + best_collection = Mogi._minimize_range( + cur_check_list, self.players_per_room) if valid_players_check(best_collection): return best_collection, Mogi.ALGORITHM_STATUS_SUCCESS_FOUND if len(late_players) == 0: @@ -149,7 +151,8 @@ def get_room_from_thread(self, channel_id: int): return None async def populate_host_fcs(self): - all_hosts = {str(plr.member.id): plr for plr in filter(lambda p: p.host, self.players_on_confirmed_teams())} + all_hosts = {str(plr.member.id): plr for plr in filter( + lambda p: p.host, self.players_on_confirmed_teams())} hosts = await host_fcs.get_hosts(all_hosts) for host_discord_id, host_fc in hosts.items(): player: Player = all_hosts.get(host_discord_id) @@ -272,6 +275,7 @@ def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed self.score = 0 self.host = host self.host_fc = None + self.is_late = False @property def mention(self): @@ -286,17 +290,24 @@ def __repr__(self): def __lt__(self, other: Player): return self.mmr < other.mmr + def set_is_late(self): + self.late = True + + def get_is_late(self): + return self.late + class VoteView(View): - def __init__(self, players, thread, mogi: Mogi, room: Room, tier_info): + def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int, tier_info): super().__init__() self.players = players - self.thread:discord.Thread = thread + self.thread: discord.Thread = thread self.mogi = mogi self.room = room self.header_text = "" self.teams_text = "" self.found_winner = False + self.penalty_time = penalty_time self.tier_info = tier_info self.votes = {"FFA": [], "2v2": [], @@ -305,8 +316,20 @@ def __init__(self, players, thread, mogi: Mogi, room: Room, tier_info): "6v6": [] } + self.add_button("FFA", self.general_vote_callback) + self.add_button("2v2", self.general_vote_callback) + self.add_button("3v3", self.general_vote_callback) + self.add_button("4v4", self.general_vote_callback) + + if common.SERVER is common.Server.MKW: + self.add_button("6v6", self.general_vote_callback) + async def make_teams(self, format_): - self.mogi.making_rooms_run_time = datetime.now(timezone.utc) + if common.SERVER is common.Server.MKW: + self.mogi.making_rooms_run_time = datetime.now(timezone.utc) + elif common.SERVER is common.Server.MK8DX: + self.mogi.making_rooms_run_time = self.mogi.start_time + \ + timedelta(minutes=5) random.shuffle(self.players) room = self.mogi.get_room_from_thread(self.thread.id) @@ -317,14 +340,18 @@ async def make_teams(self, format_): 2) 2v2 - {len(self.votes['2v2'])} 3) 3v3 - {len(self.votes['3v3'])} 4) 4v4 - {len(self.votes['4v4'])} -5) 6v6 - {len(self.votes['6v6'])} -Winner: {format_[1]} - """ + if common.SERVER is common.Server.MKW: + msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" + msg += f"Winner: {format_[1]}\n\n" room_mmr = round(sum([p.mmr for p in self.players]) / 12) room.mmr_average = room_mmr - self.header_text = f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " + self.header_text = "" + if common.SERVER is common.Server.MKW: + self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " + elif common.SERVER is common.Server.MK8DX: + self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - Tier {get_tier_mk8dx(room_mmr)}** " msg += self.header_text + "\n" teams = [] @@ -346,7 +373,13 @@ async def make_teams(self, format_): msg += team_text self.teams_text += team_text - penalty_time = self.mogi.making_rooms_run_time + timedelta(minutes=8) + if common.SERVER is common.Server.MK8DX: + msg += f"\nTable: `/scoreboard`\n" + + msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n\n" + + penalty_time = self.mogi.making_rooms_run_time + \ + timedelta(minutes=self.penalty_time) room_open_time = self.mogi.making_rooms_run_time potential_host_str = self.room.get_host_str() if potential_host_str == "": @@ -359,7 +392,12 @@ async def make_teams(self, format_): self.found_winner = True await self.thread.send(msg) if common.SERVER is common.Server.MKW: - new_thread_name = self.thread.name + f" - T{get_tier(room_mmr, self.tier_info)}" + new_thread_name = self.thread.name + \ + f" - T{get_tier(room_mmr, self.tier_info)}" + await self.thread.edit(name=new_thread_name) + elif common.SERVER is common.Server.MK8DX: + new_thread_name = self.thread.name + \ + f" - Tier {get_tier_mk8dx(room_mmr)}" await self.thread.edit(name=new_thread_name) async def find_winner(self): @@ -376,7 +414,7 @@ async def find_winner(self): winners.append((3, "3v3")) if len(self.votes["4v4"]) == most_votes: winners.append((4, "4v4")) - if len(self.votes["6v6"]) == most_votes: + if common.SERVER is common.Server.MKW and len(self.votes["6v6"]) == most_votes: winners.append((6, "6v6")) winner = random.choice(winners) @@ -386,28 +424,17 @@ async def find_winner(self): await self.make_teams(winner) - @discord.ui.button(label="FFA: 0", custom_id="FFA") - async def one_button_callback(self, interaction, button): - await self.general_vote_callback(interaction, 1, "FFA") - - @discord.ui.button(label="2v2: 0", custom_id="2v2") - async def two_button_callback(self, interaction, button): - await self.general_vote_callback(interaction, 2, "2v2") + def add_button(self, label, callback): + button = discord.ui.Button(label=f"{label}: 0", custom_id=label) + button.callback = callback + self.add_item(button) - @discord.ui.button(label="3v3: 0", custom_id="3v3") - async def three_button_callback(self, interaction, button): - await self.general_vote_callback(interaction, 3, "3v3") - - @discord.ui.button(label="4v4: 0", custom_id="4v4") - async def four_button_callback(self, interaction, button): - await self.general_vote_callback(interaction, 4, "4v4") - - @discord.ui.button(label="6v6: 0", custom_id="6v6") - async def six_button_callback(self, interaction, button): - await self.general_vote_callback(interaction, 6, "6v6") - - async def general_vote_callback(self, interaction: discord.Interaction, players_per_team: int, vote: str): + async def general_vote_callback(self, interaction: discord.Interaction): if not self.found_winner: + vote = interaction.data['custom_id'] + players_per_team = 1 + if vote != "FFA": + players_per_team = int(vote[0]) original_vote = None for vote_option, voter_ids in self.votes.items(): if interaction.user.id in voter_ids: @@ -426,10 +453,11 @@ async def general_vote_callback(self, interaction: discord.Interaction, players_ class JoinView(View): - def __init__(self, room: Room, get_mmr, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): + def __init__(self, room: Room, get_mmr, sub_range_mmr_allowance, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): super().__init__(timeout=1200) self.room = room self.get_mmr = get_mmr + self.sub_range_mmr_allowance = sub_range_mmr_allowance self.bottom_room_num = bottom_room_num self.is_restricted = is_restricted @@ -457,7 +485,7 @@ async def button_callback(self, interaction: discord.Interaction, button): return mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low - if isinstance(user_mmr, int) and mmr_high + 700 > user_mmr > mmr_low - 700: + if isinstance(user_mmr, int) and mmr_high + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: self.room.subs.append(interaction.user.id) button.disabled = True await interaction.followup.edit_message(interaction.message.id, view=self) @@ -473,3 +501,36 @@ def get_tier(mmr: int, tier_info): if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): return tier["ladder_order"] + + +def get_tier_mk8dx(mmr: int): + if mmr > 14000: + return 'X' + if mmr > 13000: + return 'S' + if mmr > 12000: + return 'A' + if mmr > 11000: + return 'AB' + if mmr > 10000: + return 'B' + if mmr > 9000: + return 'BC' + if mmr > 8000: + return 'C' + if mmr > 7000: + return 'CD' + if mmr > 6000: + return 'D' + if mmr > 5000: + return 'DE' + if mmr > 4000: + return 'E' + if mmr > 3000: + return 'EF' + if mmr > 2000: + return 'F' + if mmr > 1000: + return 'FG' + else: + return 'G' diff --git a/test_config.json b/test_config.json deleted file mode 100644 index 3f78a68..0000000 --- a/test_config.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "token": "", - "lounge": "MKW", - "lounge_description": "Valid options are MKW or MK8DX", - "guild_id": 1228027010836594698, - "admin_roles": { - "1228027010836594698": [ - "Administrator", - "Lounge Staff" - ] - }, - "queue_join_channel": 1228046535321849907, - "queue_sub_channel": 1228046571447386173, - "queue_list_channel": 1228046584181293066, - "queue_history_channel": 1228046598777344161, - "queue_general_channel": 1250554989508825168, - "members_for_channels": { - "1228027010836594698": [ - 235148962103951360, - 155149108183695360, - 815483289515196416, - 892420397570592768, - 775415990238314518, - 774812906830299136, - 745443073202323586, - 774866940950872095, - 1019051989734273054 - ] - }, - "roles_for_channels": { - "1228027010836594698": [ - 1249808212547604561 - ] - }, - "staff_roles": [ - 387347888935534593, - 399382503825211393, - 399384750923579392, - 521149807994208295, - 792891432301625364, - 877629421811540016 - ], - "helper_staff_roles": [ - 389252697284542465, - 399384750923579392, - 399382503825211393 - ], - "restricted_role_id": 797208908153618452, - "muted_role_id": 434887701662007296, - "queue_messages": true, - "sec_between_queue_msgs": 2, - "username": "username", - "password": "password", - "url": "https://www.mkwlounge.gg", - "track_type": "rt", - "FIRST_EVENT_TIME": 0, - "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", - "QUEUE_OPEN_TIME": 72, - "JOINING_TIME": 67, - "EXTENSION_TIME": 5, - "MOGI_LIFETIME": 180, - "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 9000 -} \ No newline at end of file From 05708aafc7ca3d6293ef50b696fb65a22e07f5b6 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:16:25 -0500 Subject: [PATCH 142/292] Simplify usage of is_restricted --- cogs/SquadQueue.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 133b87b..7434590 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -29,9 +29,9 @@ MMR_THRESHOLD_PKL = "mmr_threshold.pkl" -def is_restricted(user: discord.User | discord.Member, config: dict) -> bool: - muted_role_id = config.get("muted_role_id") - restricted_role_id = config.get("restricted_role_id") +def is_restricted(user: discord.User | discord.Member) -> bool: + muted_role_id = common.CONFIG.get("muted_role_id") + restricted_role_id = common.CONFIG.get("restricted_role_id") return (muted_role_id is not None and user.get_role(muted_role_id)) \ or (restricted_role_id is not None and user.get_role(restricted_role_id)) @@ -433,14 +433,14 @@ async def sub(self, interaction: discord.Interaction): discord_id, self.TRACK_TYPE, self.URL), self.SUB_RANGE_MMR_ALLOWANCE, bottom_room_num, - lambda user: is_restricted(user, self.bot.config)) + is_restricted) elif common.SERVER is common.Server.MK8DX: view = JoinView(room, lambda discord_id: mk8dx_get_mmr_from_discord_id( discord_id, self.URL), self.SUB_RANGE_MMR_ALLOWANCE, bottom_room_num, - lambda user: is_restricted(user, self.bot.config)) + is_restricted) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") @@ -620,7 +620,7 @@ async def ping_staff(self, interaction: discord.Interaction, role: str): if not isinstance(interaction.channel, discord.Thread): await interaction.response.send_message(f"Cannot use this command here.", ephemeral=True) return - if is_restricted(interaction.user, self.bot.config): + if is_restricted(interaction.user): await interaction.response.send_message(f"You are restricted from using this command.", ephemeral=True) return if role not in self.helper_staff_roles: From f39be8d1e3685e3887260103c00d6adce04a4e63 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 15 Jul 2024 18:27:00 -0700 Subject: [PATCH 143/292] Pushed check for specific game mmr fetch into mmr.py file --- cogs/SquadQueue.py | 40 ++++++++++------------------------------ mmr.py | 19 +++++++++++++++++-- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 133b87b..8f0fe9d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -11,7 +11,7 @@ import common from common import divide_chunks -from mmr import mk8dx_mmr, mk8dx_get_mmr_from_discord_id, mkw_mmr, mkw_get_mmr_from_discord_id +from mmr import get_mmr, get_mmr_from_discord_id from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier, get_tier_mk8dx import asyncio from collections import defaultdict @@ -327,10 +327,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" else: - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, [member], self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, [member]) + players = await get_mmr(url, [member], self.TRACK_TYPE) if len(players) == 0 or players[0] is None: msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " @@ -426,21 +423,12 @@ async def sub(self, interaction: discord.Interaction): timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = None - if common.SERVER is common.Server.MKW: - view = JoinView(room, - lambda discord_id: mkw_get_mmr_from_discord_id( - discord_id, self.TRACK_TYPE, self.URL), - self.SUB_RANGE_MMR_ALLOWANCE, - bottom_room_num, - lambda user: is_restricted(user, self.bot.config)) - elif common.SERVER is common.Server.MK8DX: - view = JoinView(room, - lambda discord_id: mk8dx_get_mmr_from_discord_id( - discord_id, self.URL), - self.SUB_RANGE_MMR_ALLOWANCE, - bottom_room_num, - lambda user: is_restricted(user, self.bot.config)) + view = JoinView(room, + lambda discord_id: get_mmr_from_discord_id(self.URL, + discord_id, self.TRACK_TYPE), + self.SUB_RANGE_MMR_ALLOWANCE, + bottom_room_num, + lambda user: is_restricted(user, self.bot.config)) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") @@ -1250,11 +1238,7 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) - players = [] - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, check_players, self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, check_players) + players = await get_mmr(url, check_players, self.TRACK_TYPE) for i in range(0, 12): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) @@ -1307,11 +1291,7 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) - players = [] - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, check_players, self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, check_players) + players = await get_mmr(url, check_players, self.TRACK_TYPE) for i in range(0, num_times): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) diff --git a/mmr.py b/mmr.py index 87269ac..f709a7a 100644 --- a/mmr.py +++ b/mmr.py @@ -1,6 +1,7 @@ import aiohttp import discord from mogi_objects import Player +import common headers = {'Content-type': 'application/json'} @@ -82,7 +83,14 @@ async def mkw_mmr(url, members, track_type): return players -async def mk8dx_get_mmr_from_discord_id(discord_id, url): +async def get_mmr(url, members, track_type): + if common.SERVER is common.Server.MK8DX: + return await mk8dx_mmr(url, members) + elif common.SERVER is common.Server.MKW: + return await mkw_mmr(url, members, track_type) + + +async def mk8dx_get_mmr_from_discord_id(url, discord_id): base_url = url + '/api/player?' timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession( @@ -100,7 +108,7 @@ async def mk8dx_get_mmr_from_discord_id(discord_id, url): return player_data['mmr'] -async def mkw_get_mmr_from_discord_id(discord_id, track_type, url): +async def mkw_get_mmr_from_discord_id(url, discord_id, track_type): tracks = track_type base_url = url + \ '/api/ladderplayer.php?ladder_type=' + tracks + '&' @@ -119,3 +127,10 @@ async def mkw_get_mmr_from_discord_id(discord_id, track_type, url): if 'current_mmr' not in player_data.keys(): return "Player has no mmr" return player_data['current_mmr'] + + +async def get_mmr_from_discord_id(url, discord_id, track_type): + if common.SERVER is common.Server.MK8DX: + return await mk8dx_get_mmr_from_discord_id(url, discord_id) + elif common.SERVER is common.Server.MKW: + return await mkw_get_mmr_from_discord_id(url, discord_id, track_type) From 91162d96801e3fa96b29c7661e40b5837dc998be Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:32:30 -0500 Subject: [PATCH 144/292] Bug fix --- common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.py b/common.py index 1c8ee9b..6ce0e6c 100644 --- a/common.py +++ b/common.py @@ -2,7 +2,7 @@ from enum import Enum, auto from typing import TypeVar, Type, List -_T = TypeVar('T') +_T = TypeVar('_T') with open('./config.json', 'r') as cjson: CONFIG = json.load(cjson) From 21e8b71a062a12db1530cc422f85a02a678500c5 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:21:16 -0500 Subject: [PATCH 145/292] Interface to pull all ratings every 10 minutes for mkw and mk8dx --- cogs/SquadQueue.py | 37 ++++++++++++-------------- mmr.py | 65 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7434590..1e09c17 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -11,7 +11,7 @@ import common from common import divide_chunks -from mmr import mk8dx_mmr, mk8dx_get_mmr_from_discord_id, mkw_mmr, mkw_get_mmr_from_discord_id +import mmr from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier, get_tier_mk8dx import asyncio from collections import defaultdict @@ -139,6 +139,8 @@ def __init__(self, bot): # These will be refreshed every 24 hours to ensure that the correct name displays for the options self.helper_staff_roles: Dict[str, discord.Role] = {} + self.ratings = mmr.Ratings() + @commands.Cog.listener() async def on_ready(self): self.GUILD = self.bot.get_guild(self.bot.config["guild_id"]) @@ -171,7 +173,7 @@ async def on_ready(self): print(f"History Channel - {self.HISTORY_CHANNEL}", flush=True) print(f"General Channel - {self.GENERAL_CHANNEL}", flush=True) print("Ready!", flush=True) - + self.refresh_ratings.start() self.refresh_helper_roles.start() async def lockdown(self, channel: discord.TextChannel): @@ -327,10 +329,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " msg += "If you believe this is a mistake, please contact a staff member for help.\n" else: - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, [member], self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, [member]) + players = self.ratings.get_rating([member]) if len(players) == 0 or players[0] is None: msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " @@ -429,15 +428,13 @@ async def sub(self, interaction: discord.Interaction): view = None if common.SERVER is common.Server.MKW: view = JoinView(room, - lambda discord_id: mkw_get_mmr_from_discord_id( - discord_id, self.TRACK_TYPE, self.URL), + self.ratings.get_rating_from_discord_id, self.SUB_RANGE_MMR_ALLOWANCE, bottom_room_num, is_restricted) elif common.SERVER is common.Server.MK8DX: view = JoinView(room, - lambda discord_id: mk8dx_get_mmr_from_discord_id( - discord_id, self.URL), + self.ratings.get_rating_from_discord_id, self.SUB_RANGE_MMR_ALLOWANCE, bottom_room_num, is_restricted) @@ -1211,6 +1208,14 @@ async def refresh_helper_roles(self): except Exception as e: print(traceback.format_exc()) + @tasks.loop(minutes=10) + async def refresh_ratings(self): + """Refreshes the ratings""" + try: + await self.ratings.update_ratings() + except Exception as e: + print(traceback.format_exc()) + def get_event_str(self, mogi: Mogi): mogi_time = discord.utils.format_dt(mogi.start_time, style="F") mogi_time_relative = discord.utils.format_dt( @@ -1250,11 +1255,7 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) - players = [] - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, check_players, self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, check_players) + players = self.ratings.get_rating(check_players) for i in range(0, 12): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) @@ -1307,11 +1308,7 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) - players = [] - if common.SERVER is common.Server.MKW: - players = await mkw_mmr(url, check_players, self.TRACK_TYPE) - elif common.SERVER is common.Server.MK8DX: - players = await mk8dx_mmr(url, check_players) + players = self.ratings.get_rating(check_players) for i in range(0, num_times): player = Player( players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) diff --git a/mmr.py b/mmr.py index 87269ac..159fd61 100644 --- a/mmr.py +++ b/mmr.py @@ -1,27 +1,64 @@ import aiohttp -import discord from mogi_objects import Player +import common +import discord +from typing import List +import asyncio headers = {'Content-type': 'application/json'} +class RatingsNotReady(Exception): + pass + +class Ratings: + def __init__(self): + self.first_run_complete = False + self.ratings = {} + + async def update_ratings(self): + if common.SERVER is common.Server.MK8DX: + rating_func = self._pull_mk8dx_ratings + elif common.SERVER is common.Server.MKW: + rating_func = self._pull_mkw_ratings + else: + raise Exception("Unreachable code.") + + status = await rating_func() + # If we failed to pull ratings, wait 60 seconds and try again + if not status: + print(f"Failed to pull ratings for {common.SERVER.name}. Waiting 60 seconds and trying again.") + await asyncio.sleep(60) + status = await rating_func() + # If we failed to pull ratings again, fail. + if not status: + print(f"Failed to pull ratings for {common.SERVER.name} on 2nd attempt. Skipping.") + return + self.first_run_complete = True + + async def _pull_mk8dx_ratings(self) -> bool: + pass -# class LoungeData: -# def __init__(self): -# self._data = None + async def _pull_mkw_ratings(self): + base_url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr" + # Do stuff + resp = {} + self._parse_mkw_ratings(resp) -# async def lounge_api_full(self): -# async with aiohttp.ClientSession() as session: -# async with session.get("https://www.mk8dx-lounge.com/api/player/list") as response: -# if response.status == 200: -# _data_full = await response.json() -# self._data = [ -# player for player in _data_full['players'] if "discordId" in player] + def _parse_mk8dx_ratings(self, results: dict): + pass -# def data(self): -# return self._data + def _parse_mkw_ratings(self, results: dict): + pass + def get_rating_from_discord_id(self, discord_id: str) -> int | None: + if not self.first_run_complete: + raise RatingsNotReady("Ratings not pulled yet.") + return self.ratings.get(discord_id) -# lounge_data = LoungeData() + def get_rating(self, members: List[discord.User | discord.Member]) -> List[Player]: + if not self.first_run_complete: + raise RatingsNotReady("Ratings not pulled yet.") + return [] async def mk8dx_mmr(url, members): From 3d878b09cfa54429b5488bb1b3db8c3f5869f0f4 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:22:19 -0500 Subject: [PATCH 146/292] Remove unnecessary url variable in testing functions --- cogs/SquadQueue.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1e09c17..acdb615 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -284,15 +284,12 @@ async def join_queue(self, interaction: discord.Interaction, host=False): await interaction.response.defer() async with self.LOCK: member = interaction.user - url = self.URL if not self.is_production: if common.SERVER is common.Server.MK8DX: # is actually a user and not a member member = await self.bot.fetch_user(318637887597969419) - url = "https://www.mk8dx-lounge.com" elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(82862780591378432) - url = "https://www.mkwlounge.gg" mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: await interaction.followup.send("Queue has not started yet.") @@ -1245,14 +1242,11 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): return member = ctx.author - url = self.URL if not self.is_production: if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) - url = "https://www.mk8dx-lounge.com" elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(82862780591378432) - url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) @@ -1298,14 +1292,11 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem return member = ctx.author - url = self.URL if not self.is_production: if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) - url = "https://www.mk8dx-lounge.com" elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(82862780591378432) - url = "https://www.mkwlounge.gg" check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) From 798a6e1ad30607fb48a7e57a4c4dea7ee904d864 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:23:08 -0500 Subject: [PATCH 147/292] Refactor of rating function name in JoinView --- mogi_objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index d3b87dd..3c3b441 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -453,10 +453,10 @@ async def general_vote_callback(self, interaction: discord.Interaction): class JoinView(View): - def __init__(self, room: Room, get_mmr, sub_range_mmr_allowance, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): + def __init__(self, room: Room, get_rating_from_discord_id, sub_range_mmr_allowance, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): super().__init__(timeout=1200) self.room = room - self.get_mmr = get_mmr + self.get_rating_from_discord_id = get_rating_from_discord_id self.sub_range_mmr_allowance = sub_range_mmr_allowance self.bottom_room_num = bottom_room_num self.is_restricted = is_restricted @@ -473,7 +473,7 @@ async def button_callback(self, interaction: discord.Interaction, button): "You are already in this room.", ephemeral=True) return try: - user_mmr = await self.get_mmr(interaction.user.id) + user_mmr = self.get_rating_from_discord_id(interaction.user.id) except: await interaction.followup.send( "MMR lookup for player has failed, please try again.", ephemeral=True) From 4ff06800eb0913a7d89a56f6439019071b73aeff Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 15 Jul 2024 19:34:45 -0700 Subject: [PATCH 148/292] reverted unnecessary check for game --- cogs/SquadQueue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8f0fe9d..a44cbff 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -962,8 +962,7 @@ def allowed_players_check(players): late_player_list = all_confirmed_players[first_late_player_index:] proposed_list = sorted(mogi.generate_proposed_list( allowed_players_check), reverse=True) - if common.SERVER is common.Server.MKW: - await mogi.populate_host_fcs() + await mogi.populate_host_fcs() for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): From 2f65c81c3033b4ea9327c66acecf9fb285016234 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 15 Jul 2024 20:10:16 -0700 Subject: [PATCH 149/292] changed method of checking late player for mk8dx --- cogs/SquadQueue.py | 19 +++++-------------- mogi_objects.py | 7 ------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a44cbff..13db0d0 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -444,19 +444,13 @@ async def list_task(self): all_confirmed_players = mogi.players_on_confirmed_teams() first_late_player_index = ( mogi.num_players // mogi.players_per_room) * mogi.players_per_room - if common.SERVER is common.Server.MK8DX: - for i, player in enumerate(all_confirmed_players, 1): - if i > first_late_player_index: - player.is_late = True - else: - player.is_late = False + on_time_players = sorted( + all_confirmed_players[:first_late_player_index], reverse=True) + late_players = all_confirmed_players[first_late_player_index:] msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" msg += "**Current Mogi List:**\n" if common.SERVER is common.Server.MKW: - on_time_players = sorted( - all_confirmed_players[:first_late_player_index], reverse=True) - late_players = all_confirmed_players[first_late_player_index:] for i, player in enumerate(on_time_players, 1): msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" if i % mogi.players_per_room == 0: @@ -469,11 +463,8 @@ async def list_task(self): elif common.SERVER is common.Server.MK8DX: all_confirmed_players.sort(reverse=True) for i, player in enumerate(all_confirmed_players, 1): - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)" - if player.is_late: - msg += " (late)\n" - else: - msg += "\n" + late_str = " (late)" if player in late_players else "" + msg += f"{i}) {player.lounge_name} ({player.mmr} MMR){late_str}\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" diff --git a/mogi_objects.py b/mogi_objects.py index d3b87dd..16fb033 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -275,7 +275,6 @@ def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed self.score = 0 self.host = host self.host_fc = None - self.is_late = False @property def mention(self): @@ -290,12 +289,6 @@ def __repr__(self): def __lt__(self, other: Player): return self.mmr < other.mmr - def set_is_late(self): - self.late = True - - def get_is_late(self): - return self.late - class VoteView(View): def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int, tier_info): From 573ad61305e91d0f430a67e56542e1a202296f62 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 15 Jul 2024 20:42:43 -0700 Subject: [PATCH 150/292] Added an explicit display_time variable for mogis to control how start time is presented --- cogs/SquadQueue.py | 22 ++++++++++---------- example_configs/prod/mk8dx_prod_config.json | 1 + example_configs/prod/mkw_ct_prod_config.json | 1 + example_configs/prod/mkw_rt_prod_config.json | 1 + mogi_objects.py | 3 ++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 13db0d0..ba1f41f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -121,6 +121,9 @@ def __init__(self, bot): # number of minutes after QUEUE_OPEN_TIME that teams can join the mogi self.JOINING_TIME = timedelta(minutes=bot.config["JOINING_TIME"]) + self.DISPLAY_OFFSET_MINUTES = timedelta( + minutes=bot.config["DISPLAY_OFFSET_MINUTES"]) + # number of minutes after JOINING_TIME for any potential extra teams to join self.EXTENSION_TIME = timedelta(minutes=bot.config["EXTENSION_TIME"]) @@ -724,6 +727,7 @@ async def peek_bot_config(self, interaction: discord.Interaction): msg += f"SITE URL: {self.URL}\n" msg += f"FIRST EVENT TIME: {self.FIRST_EVENT_TIME}\n" msg += f"TIME BETWEEN EVENTS: {self.QUEUE_OPEN_TIME} minutes\n" + msg += f"DISPLAY TIME OFFSET MINUTES FROM JOIN TIME END: {self.DISPLAY_OFFSET_MINUTES} minutes\n" msg += f"EVENT JOINING TIME: {self.JOINING_TIME} minutes\n" msg += f"EXTENSION TIME: {self.EXTENSION_TIME} minutes\n" msg += f"ROOM JOIN PENALTY TIME: {self.ROOM_JOIN_PENALTY_TIME} minutes\n" @@ -859,11 +863,7 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): """Writes the teams, tier and average of each room per hour.""" try: if mogi is not None: - # I want the time displayed to be at the hour but the current way reads as :55 minutes on each queue which looks bad... - room_display_time = mogi.start_time - if common.SERVER is common.Server.MK8DX: - room_display_time = mogi.start_time + timedelta(minutes=5) - await history_channel.send(f"{discord.utils.format_dt(room_display_time)} Rooms") + await history_channel.send(f"{discord.utils.format_dt(mogi.display_time)} Rooms") for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( @@ -884,13 +884,11 @@ async def check_room_channels(self, mogi): if num_created_rooms >= mogi.max_possible_rooms: return for i in range(num_created_rooms, mogi.max_possible_rooms): - start_time = mogi.start_time - if common.SERVER is common.Server.MK8DX: - start_time += timedelta(minutes=5) - minute = start_time.minute + display_time = mogi.display_time + minute = display_time.minute if len(str(minute)) == 1: minute = '0' + str(minute) - room_name = f"{start_time.month}/{start_time.day}, {start_time.hour}:{minute}:00 - Room {i+1}" + room_name = f"{display_time.month}/{display_time.day}, {display_time.hour}:{minute}:00 - Room {i+1}" try: room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, @@ -1125,6 +1123,7 @@ async def schedule_que_event(self): return next_event_open_time = self.compute_next_event_open_time() next_event_start_time = next_event_open_time + self.JOINING_TIME + next_event_display_time = next_event_start_time + self.DISPLAY_OFFSET_MINUTES # We don't want to schedule the next event if it would open after it's joining period and during its extension period if next_event_start_time < datetime.now(timezone.utc): return @@ -1147,7 +1146,8 @@ async def schedule_que_event(self): players_per_room=12, mogi_channel=self.MOGI_CHANNEL, is_automated=True, - start_time=next_event_start_time) + start_time=next_event_start_time, + display_time=next_event_display_time) print(f"Started Queue for {next_event_start_time}", flush=True) diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index 94ac015..9430b3c 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -37,6 +37,7 @@ "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 60, "JOINING_TIME": 55, + "DISPLAY_OFFSET_MINUTES": 5, "EXTENSION_TIME": 3, "ROOM_JOIN_PENALTY_TIME": 5, "MOGI_LIFETIME": 180, diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index 91048a7..27d17c4 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -48,6 +48,7 @@ "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 90, "JOINING_TIME": 85, + "DISPLAY_OFFSET_MINUTES": 0, "EXTENSION_TIME": 5, "ROOM_JOIN_PENALTY_TIME": 8, "MOGI_LIFETIME": 180, diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 3b65358..46515fe 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -48,6 +48,7 @@ "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, + "DISPLAY_OFFSET_MINUTES": 0, "EXTENSION_TIME": 5, "ROOM_JOIN_PENALTY_TIME": 8, "MOGI_LIFETIME": 180, diff --git a/mogi_objects.py b/mogi_objects.py index 16fb033..0e67c24 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -21,7 +21,7 @@ class Mogi: ALGORITHM_STATUS_SUCCESS_EMPTY = 4 def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, mogi_channel: discord.TextChannel, - is_automated=False, start_time=None, additional_extension_minutes=0): + is_automated=False, start_time=None, display_time=None, additional_extension_minutes=0): self.started = False self.gathering = False self.making_rooms_run = False @@ -34,6 +34,7 @@ def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, self.rooms: List[Room] = [] self.is_automated = is_automated self.start_time = start_time if is_automated else None + self.display_time = display_time if is_automated else None self.additional_extension = timedelta( minutes=additional_extension_minutes) From e4a05ca624c01fcb57e777eaa51dc25be1ab6efb Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:53:17 -0500 Subject: [PATCH 151/292] All functionality for validating API rating responses, parsing, and reading in ratings for both MKW and MK8DX/ --- mmr.py | 208 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/mmr.py b/mmr.py index 159fd61..210b08e 100644 --- a/mmr.py +++ b/mmr.py @@ -4,12 +4,31 @@ import discord from typing import List import asyncio +import traceback headers = {'Content-type': 'application/json'} + class RatingsNotReady(Exception): pass + +class RatingRequestFailure(Exception): + pass + + +class BadRatingData(RatingRequestFailure): + pass + + +class BadPlayerDataLength(BadRatingData): + pass + + +class BadPlayerData(BadRatingData): + pass + + class Ratings: def __init__(self): self.first_run_complete = False @@ -36,19 +55,97 @@ async def update_ratings(self): self.first_run_complete = True async def _pull_mk8dx_ratings(self) -> bool: - pass + url = f"{common.CONFIG["url"]}/api/player/list" + await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) async def _pull_mkw_ratings(self): - base_url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr" - # Do stuff - resp = {} - self._parse_mkw_ratings(resp) + url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr" + await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) + + @staticmethod + async def _pull_ratings(url, parser, validator) -> bool: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status != 200: + print(f"{common.SERVER.name} returned status {response.status}") + return False + data = await response.json() + try: + validator(data) + except RatingRequestFailure: + print(f"{common.SERVER.name}'s data from API was formatted incorrectly.") + print(traceback.format_exc()) + return False + parser(data) + + def _validate_mk8dx_response(self, results: dict): + if type(results) is not dict: + raise BadRatingData("Response is not a dictionary") + all_players = results.get("players") + if all_players is None: + raise BadRatingData("Key word 'players' not found in JSON response.") + # + required_player_amount = 10000 + if len(all_players) < required_player_amount: + raise BadPlayerDataLength( + f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") + + required_fields = [("name", str), ("discordId", str), ("mmr", int)] + for player in all_players: + for required_field_name, required_field_type in required_fields: + if required_field_name not in player: + raise BadPlayerData( + f"Missing required field '{required_field_name}' in the following player: {player}") + field_data = player[required_field_name] + if type(field_data) is not required_field_type: + raise BadPlayerData( + f"For field '{required_field_name}', expected type '{required_field_type}' received {type(field_data)} for player: {player}") def _parse_mk8dx_ratings(self, results: dict): - pass + self.ratings.clear() + all_players = results.get("players") + for player in all_players: + self.ratings[player["discordId"]] = player["mmr"] + + def _validate_mkw_response(self, results: dict): + if type(results) is not dict: + raise BadRatingData("Response is not a dictionary") + mkw_status = results.get("status") + if mkw_status is None: + raise BadRatingData("Key word 'status' not found in JSON response.") + if mkw_status != "success": + raise BadRatingData(f"'status' had value of {mkw_status} in JSON response.") + + all_players = results.get("results") + if all_players is None: + raise BadRatingData("Key word 'results' not found in JSON response.") + # + required_player_amount = 1000 + if len(all_players) < required_player_amount: + raise BadPlayerDataLength( + f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") + + for player in all_players: + if "discord_user_id" not in player: + raise BadPlayerData(f"Missing required field 'discord_user_id' in the following player: {player}") + if "current_mmr" not in player: + raise BadPlayerData(f"Missing required field 'current_mmr' in the following player: {player}") + discord_user_id = player.get("discord_user_id") + current_mmr = player.get("current_mmr") + if not (type(discord_user_id) is str or discord_user_id is None): + raise BadPlayerData( + f"For field 'discord_user_id', expected type 'str' or None, received {type(discord_user_id)} for player: {player}") + if type(current_mmr) is not int: + raise BadPlayerData( + f"For field 'current_mmr', expected type 'int' received {type(current_mmr)} for player: {player}") def _parse_mkw_ratings(self, results: dict): - pass + self.ratings.clear() + all_players = results.get("players") + for player in all_players: + discord_user_id = player.get("discord_user_id") + if discord_user_id is not None: + self.ratings[player["discord_user_id"]] = player["current_mmr"] def get_rating_from_discord_id(self, discord_id: str) -> int | None: if not self.first_run_complete: @@ -59,100 +156,3 @@ def get_rating(self, members: List[discord.User | discord.Member]) -> List[Playe if not self.first_run_complete: raise RatingsNotReady("Ratings not pulled yet.") return [] - - -async def mk8dx_mmr(url, members): - base_url = url + '/api/player?' - players = [] - timeout = aiohttp.ClientTimeout(total=10) - try: - async with aiohttp.ClientSession( - timeout=timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: - for member in members: - request_text = f"discordId={member.id}" - request_url = base_url + request_text - async with session.get(request_url, headers=headers) as resp: - if resp.status != 200: - players.append(None) - continue - player_data = await resp.json() - if 'mmr' not in player_data.keys(): - players.append( - Player(member, player_data['name'], None)) - continue - players.append( - Player(member, player_data['name'], player_data['mmr'])) - except: - print(f"Fetch for player {members[0]} has failed.", flush=True) - return players - - -async def mkw_mmr(url, members, track_type): - tracks = track_type - base_url = url + '/api/ladderplayer.php?ladder_type=' + tracks + '&' - players = [] - timeout = aiohttp.ClientTimeout(total=10) - try: - async with aiohttp.ClientSession( - timeout=timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: - for member in members: - request_text = f"discord_user_id={member.id}" - request_url = base_url + request_text - async with session.get(request_url, headers=headers) as resp: - if resp.status != 200: - players.append(None) - continue - result = await resp.json() - player_data = result["results"][0] - if 'current_mmr' not in player_data.keys(): - players.append( - Player(member, player_data['player_name'], None)) - continue - players.append( - Player(member, player_data['player_name'], player_data['current_mmr'])) - except: - print(f"Fetch for player {members[0]} has failed.", flush=True) - return players - - -async def mk8dx_get_mmr_from_discord_id(discord_id, url): - base_url = url + '/api/player?' - timeout = aiohttp.ClientTimeout(total=10) - async with aiohttp.ClientSession( - timeout=timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: - request_text = f"discordId={discord_id}" - request_url = base_url + request_text - async with session.get(request_url, headers=headers) as resp: - if resp.status != 200: - return "Player does not exist" - player_data = await resp.json() - if 'mmr' not in player_data.keys(): - return "Player has no mmr" - return player_data['mmr'] - - -async def mkw_get_mmr_from_discord_id(discord_id, track_type, url): - tracks = track_type - base_url = url + \ - '/api/ladderplayer.php?ladder_type=' + tracks + '&' - timeout = aiohttp.ClientTimeout(total=10) - async with aiohttp.ClientSession( - timeout=timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: - request_text = f"discord_user_id={discord_id}" - request_url = base_url + request_text - async with session.get(request_url, headers=headers) as resp: - if resp.status != 200: - return "Player does not exist" - result = await resp.json() - player_data = result["results"][0] - if 'current_mmr' not in player_data.keys(): - return "Player has no mmr" - return player_data['current_mmr'] From 6b2088a07e161efa82095b779196f4955edf1378 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:08:25 -0500 Subject: [PATCH 152/292] Finished Ratings.get_rating --- mmr.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/mmr.py b/mmr.py index 210b08e..e911afa 100644 --- a/mmr.py +++ b/mmr.py @@ -59,7 +59,7 @@ async def _pull_mk8dx_ratings(self) -> bool: await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) async def _pull_mkw_ratings(self): - url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr" + url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr,player_name" await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) @staticmethod @@ -105,7 +105,7 @@ def _parse_mk8dx_ratings(self, results: dict): self.ratings.clear() all_players = results.get("players") for player in all_players: - self.ratings[player["discordId"]] = player["mmr"] + self.ratings[player["discordId"]] = (player["mmr"], player["name"]) def _validate_mkw_response(self, results: dict): if type(results) is not dict: @@ -126,10 +126,13 @@ def _validate_mkw_response(self, results: dict): f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") for player in all_players: + if "player_name" not in player: + raise BadPlayerData(f"Missing required field 'player_name' in the following player: {player}") if "discord_user_id" not in player: raise BadPlayerData(f"Missing required field 'discord_user_id' in the following player: {player}") if "current_mmr" not in player: raise BadPlayerData(f"Missing required field 'current_mmr' in the following player: {player}") + player_name = player.get("player_name") discord_user_id = player.get("discord_user_id") current_mmr = player.get("current_mmr") if not (type(discord_user_id) is str or discord_user_id is None): @@ -138,6 +141,9 @@ def _validate_mkw_response(self, results: dict): if type(current_mmr) is not int: raise BadPlayerData( f"For field 'current_mmr', expected type 'int' received {type(current_mmr)} for player: {player}") + if type(player_name) is not str: + raise BadPlayerData( + f"For field 'player_name', expected type 'str' received {type(player_name)} for player: {player}") def _parse_mkw_ratings(self, results: dict): self.ratings.clear() @@ -145,14 +151,25 @@ def _parse_mkw_ratings(self, results: dict): for player in all_players: discord_user_id = player.get("discord_user_id") if discord_user_id is not None: - self.ratings[player["discord_user_id"]] = player["current_mmr"] + self.ratings[player["discord_user_id"]] = (player["current_mmr"], player["player_name"]) def get_rating_from_discord_id(self, discord_id: str) -> int | None: if not self.first_run_complete: raise RatingsNotReady("Ratings not pulled yet.") - return self.ratings.get(discord_id) + if discord_id in self.ratings: + return self.ratings.get(discord_id)[0] + # Make clear that we intend to return None by explicitly doing so + else: + return None def get_rating(self, members: List[discord.User | discord.Member]) -> List[Player]: if not self.first_run_complete: raise RatingsNotReady("Ratings not pulled yet.") - return [] + all_players = [] + for member in members: + member_id = str(member.id) # Are discord member IDs already strings...? + if member_id not in self.ratings: + continue + rating, name = self.ratings[member_id] + all_players.append(Player(member, name, rating)) + return all_players From 8618c2e34914467c0888b1baa03a5eaa98ca8914 Mon Sep 17 00:00:00 2001 From: BadWolf1023 <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:26:46 -0500 Subject: [PATCH 153/292] Merge issue fix --- cogs/SquadQueue.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 06ca2ab..ff44be5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -425,19 +425,11 @@ async def sub(self, interaction: discord.Interaction): timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" await self.SUB_CHANNEL.send(msg, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) - view = None - if common.SERVER is common.Server.MKW: - view = JoinView(room, - self.ratings.get_rating_from_discord_id, - self.SUB_RANGE_MMR_ALLOWANCE, - bottom_room_num, - is_restricted) - elif common.SERVER is common.Server.MK8DX: - view = JoinView(room, - self.ratings.get_rating_from_discord_id, - self.SUB_RANGE_MMR_ALLOWANCE, - bottom_room_num, - is_restricted) + view = JoinView(room, + self.ratings.get_rating_from_discord_id, + self.SUB_RANGE_MMR_ALLOWANCE, + bottom_room_num, + is_restricted) await self.SUB_CHANNEL.send(view=view, delete_after=self.SUB_MESSAGE_LIFETIME_SECONDS) cooldowns[interaction.user.id] = current_time # Updates cooldown await interaction.response.send_message("Sent out request for sub.") From 49469a5dda5a69fee351b7cdaddcceb7c037e7c5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Tue, 16 Jul 2024 01:52:55 -0500 Subject: [PATCH 154/292] Bug fix --- mmr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmr.py b/mmr.py index e911afa..a2f4e9c 100644 --- a/mmr.py +++ b/mmr.py @@ -147,7 +147,7 @@ def _validate_mkw_response(self, results: dict): def _parse_mkw_ratings(self, results: dict): self.ratings.clear() - all_players = results.get("players") + all_players = results.get("results") for player in all_players: discord_user_id = player.get("discord_user_id") if discord_user_id is not None: From 1584e4c115274ee4ed87e41ba189bba081d7feac Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Tue, 16 Jul 2024 22:20:56 -0700 Subject: [PATCH 155/292] fixed way tier is calculated for rooms --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 0e67c24..e066ce0 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -345,7 +345,7 @@ async def make_teams(self, format_): if common.SERVER is common.Server.MKW: self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " elif common.SERVER is common.Server.MK8DX: - self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - Tier {get_tier_mk8dx(room_mmr)}** " + self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - Tier {get_tier_mk8dx(room_mmr - 500)}** " msg += self.header_text + "\n" teams = [] @@ -391,7 +391,7 @@ async def make_teams(self, format_): await self.thread.edit(name=new_thread_name) elif common.SERVER is common.Server.MK8DX: new_thread_name = self.thread.name + \ - f" - Tier {get_tier_mk8dx(room_mmr)}" + f" - Tier {get_tier_mk8dx(room_mmr - 500)}" await self.thread.edit(name=new_thread_name) async def find_winner(self): From a208798ec2447e0e5629862b2d695814f52385e7 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:20:32 -0500 Subject: [PATCH 156/292] f-string bug fix --- cogs/SquadQueue.py | 35 +++++++++++++++++++++-------------- mmr.py | 4 ++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ff44be5..e9c45b3 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -320,22 +320,29 @@ async def join_queue(self, interaction: discord.Interaction, host=False): player.host = host return - msg = "" - placement_role_id = self.bot.config["placement_role_id"] - if self.is_production and member.get_role(placement_role_id): - starting_player_mmr = self.PLACEMENT_PLAYER_MMR - players = [ - Player(member, member.display_name, starting_player_mmr)] - msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi with a starting MMR of {starting_player_mmr}. " - msg += "If you believe this is a mistake, please contact a staff member for help.\n" - else: - players = self.ratings.get_rating([member]) + # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, + # so this will check the leaderboard first + players = self.ratings.get_rating([member]) + msg = "" + # If the no rating was found... if len(players) == 0 or players[0] is None: - msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was unsuccessful. " - msg += "Please try again. If the problem continues then contact a staff member for help." - await interaction.followup.send(msg) - return + # ... check for placement discord role ID: + placement_role_id = self.bot.config["placement_role_id"] + if member.get_role(placement_role_id): + starting_player_mmr = self.PLACEMENT_PLAYER_MMR + players = [ + Player(member, member.display_name, starting_player_mmr)] + msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi " \ + f"with a starting MMR of {starting_player_mmr}. If you believe this is a mistake, " \ + f"please contact a staff member for help.\n" + # ...if discord user doesn't have placement role ID, send error to Discord + else: + msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was " \ + f"unsuccessful. Please try again. If the problem continues then contact a staff member " \ + f"for help." + await interaction.followup.send(msg) + return players[0].confirmed = True players[0].host = host diff --git a/mmr.py b/mmr.py index a2f4e9c..9a7e38f 100644 --- a/mmr.py +++ b/mmr.py @@ -55,12 +55,12 @@ async def update_ratings(self): self.first_run_complete = True async def _pull_mk8dx_ratings(self) -> bool: - url = f"{common.CONFIG["url"]}/api/player/list" await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) + url = f"""{common.CONFIG["url"]}/api/player/list""" async def _pull_mkw_ratings(self): - url = f"{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}all&fields=discord_user_id,current_mmr,player_name" await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) + url = f"""{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}&all&fields=discord_user_id,current_mmr,player_name""" @staticmethod async def _pull_ratings(url, parser, validator) -> bool: From 9865693a44bec236e57fb34efab4c824385d3ac8 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:21:07 -0500 Subject: [PATCH 157/292] Type hint for function return --- mmr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mmr.py b/mmr.py index 9a7e38f..ffa32f3 100644 --- a/mmr.py +++ b/mmr.py @@ -58,8 +58,8 @@ async def _pull_mk8dx_ratings(self) -> bool: await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) url = f"""{common.CONFIG["url"]}/api/player/list""" - async def _pull_mkw_ratings(self): await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) + async def _pull_mkw_ratings(self) -> bool: url = f"""{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}&all&fields=discord_user_id,current_mmr,player_name""" @staticmethod From 1e033d8b237c1a03c509845ff72818cb572bd91e Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:21:31 -0500 Subject: [PATCH 158/292] bug fix: need to return boolean status (success/fail) --- mmr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mmr.py b/mmr.py index ffa32f3..a137c4f 100644 --- a/mmr.py +++ b/mmr.py @@ -55,12 +55,12 @@ async def update_ratings(self): self.first_run_complete = True async def _pull_mk8dx_ratings(self) -> bool: - await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) url = f"""{common.CONFIG["url"]}/api/player/list""" + return await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) - await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) async def _pull_mkw_ratings(self) -> bool: url = f"""{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}&all&fields=discord_user_id,current_mmr,player_name""" + return await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) @staticmethod async def _pull_ratings(url, parser, validator) -> bool: @@ -77,6 +77,8 @@ async def _pull_ratings(url, parser, validator) -> bool: print(traceback.format_exc()) return False parser(data) + return True + return False # Didn't run request successfully, so we return it was a failure def _validate_mk8dx_response(self, results: dict): if type(results) is not dict: From 9e02bedad942698ee7be13d4781f90d37040c355 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:21:44 -0500 Subject: [PATCH 159/292] Function description --- mmr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mmr.py b/mmr.py index a137c4f..169624a 100644 --- a/mmr.py +++ b/mmr.py @@ -64,6 +64,7 @@ async def _pull_mkw_ratings(self) -> bool: @staticmethod async def _pull_ratings(url, parser, validator) -> bool: + """Returns True if the ratings were pulled and successfully stored.""" async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status != 200: From 5477acbd89f1384d5c6878050e3791ced9bc5113 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:33:56 -0500 Subject: [PATCH 160/292] Required changes for validating & parsing MK8DX JSON --- mmr.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/mmr.py b/mmr.py index 169624a..d961e5f 100644 --- a/mmr.py +++ b/mmr.py @@ -93,22 +93,35 @@ def _validate_mk8dx_response(self, results: dict): raise BadPlayerDataLength( f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") - required_fields = [("name", str), ("discordId", str), ("mmr", int)] + strongly_required_fields = [("name", str)] + weakly_required_fields = [("discordId", str), ("mmr", int)] for player in all_players: - for required_field_name, required_field_type in required_fields: - if required_field_name not in player: + # Ensure all strongly required fields are in the player JSON and that the type is correct + for strongly_req_field_name, strongly_req_field_type in strongly_required_fields: + if strongly_req_field_name not in player: raise BadPlayerData( - f"Missing required field '{required_field_name}' in the following player: {player}") - field_data = player[required_field_name] - if type(field_data) is not required_field_type: + f"Missing required field '{strongly_req_field_name}' in the following player: {player}") + field_data = player[strongly_req_field_name] + if type(field_data) is not strongly_req_field_type: raise BadPlayerData( - f"For field '{required_field_name}', expected type '{required_field_type}' received {type(field_data)} for player: {player}") + f"For field '{strongly_req_field_name}', expected type '{strongly_req_field_type}' received {type(field_data)} for player: {player}") + # Ensure that if the weakly required field is in the JSON, the type is correct + for weakly_req_field_name, weakly_req_field_type in strongly_required_fields: + if weakly_req_field_name in player: + field_data = player[weakly_req_field_name] + if type(field_data) is not weakly_req_field_type: + raise BadPlayerData( + f"For field '{weakly_req_field_name}', expected type '{weakly_req_field_type}' received {type(field_data)} for player: {player}") def _parse_mk8dx_ratings(self, results: dict): self.ratings.clear() all_players = results.get("players") for player in all_players: - self.ratings[player["discordId"]] = (player["mmr"], player["name"]) + discord_id = player.get("discordID") + if discord_id is None: + continue + rating = common.CONFIG["PLACEMENT_PLAYER_MMR"] if player.get("mmr") is None else player.get("mmr") + self.ratings[discord_id] = (rating, player["name"]) def _validate_mkw_response(self, results: dict): if type(results) is not dict: From 9c57e4311890451b70b76a35765568cc4b6fdc74 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:39:52 -0500 Subject: [PATCH 161/292] Minor bug fixes Wrong name API field for MK8DX. Wrong name for list. --- cogs/SquadQueue.py | 1 + mmr.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index e9c45b3..8ec7297 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -323,6 +323,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, # so this will check the leaderboard first players = self.ratings.get_rating([member]) + print(players) msg = "" # If the no rating was found... diff --git a/mmr.py b/mmr.py index d961e5f..d0c2635 100644 --- a/mmr.py +++ b/mmr.py @@ -106,7 +106,7 @@ def _validate_mk8dx_response(self, results: dict): raise BadPlayerData( f"For field '{strongly_req_field_name}', expected type '{strongly_req_field_type}' received {type(field_data)} for player: {player}") # Ensure that if the weakly required field is in the JSON, the type is correct - for weakly_req_field_name, weakly_req_field_type in strongly_required_fields: + for weakly_req_field_name, weakly_req_field_type in weakly_required_fields: if weakly_req_field_name in player: field_data = player[weakly_req_field_name] if type(field_data) is not weakly_req_field_type: @@ -117,7 +117,7 @@ def _parse_mk8dx_ratings(self, results: dict): self.ratings.clear() all_players = results.get("players") for player in all_players: - discord_id = player.get("discordID") + discord_id = player.get("discordId") if discord_id is None: continue rating = common.CONFIG["PLACEMENT_PLAYER_MMR"] if player.get("mmr") is None else player.get("mmr") From 234eb8196f090e3a33c6bde7e261d1fc232d5c06 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:45:33 -0500 Subject: [PATCH 162/292] Remove print --- cogs/SquadQueue.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8ec7297..e9c45b3 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -323,7 +323,6 @@ async def join_queue(self, interaction: discord.Interaction, host=False): # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, # so this will check the leaderboard first players = self.ratings.get_rating([member]) - print(players) msg = "" # If the no rating was found... From 81a4bf1873f775c667505927d22c8241e6830924 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:53:20 -0500 Subject: [PATCH 163/292] Fix race condition: switching host status --- cogs/SquadQueue.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index e9c45b3..d2575cf 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -303,21 +303,21 @@ async def join_queue(self, interaction: discord.Interaction, host=False): member) if player is not None: + original_host_status = player.host + player.host = host # The player is already signed up, but they might be changing to a host or non-host. Begin checks: # The player was queued as host, and they queued again as a host - if player.host and host: + if original_host_status and host: await interaction.followup.send(f"{interaction.user.mention} is already signed up as a host.") # The player was queued as host, and they queued again as a non-host - elif player.host and not host: + elif original_host_status and not host: await interaction.followup.send(f"{interaction.user.mention} has changed to a non-host.") # The player was not queued as host, but they are changing to a host - elif not player.host and host: + elif not original_host_status and host: await interaction.followup.send(f"{interaction.user.mention} has changed to a host.") # The player was not queued as host and did not change to a host - elif not player.host and not host: + elif not original_host_status and not host: await interaction.followup.send(f"{interaction.user.mention} is already signed up.") - - player.host = host return # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, From 21fac8c1af5755f608aad5445ae67947b0fa97b5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:56:07 -0500 Subject: [PATCH 164/292] Remove check, race condition no longer possible Since get_mmr call is not asynchronous, we do not relinquish control to the loop, so no race condition exists anymore. Remove unneeded check. --- mogi_objects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index d5010ef..97bbd56 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -472,11 +472,7 @@ async def button_callback(self, interaction: discord.Interaction, button): await interaction.followup.send( "MMR lookup for player has failed, please try again.", ephemeral=True) return - # Need a 2nd check to control the race condition introduced by "await self.get_mmr" - if interaction.user.id in self.room.get_player_list() + self.room.subs: - await interaction.followup.send( - "You are already in this room.", ephemeral=True) - return + mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low if isinstance(user_mmr, int) and mmr_high + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: From 5b2b3573084b6026ad4cb29254e1d3276108ab46 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:25:58 -0500 Subject: [PATCH 165/292] Big commit, see description - Remove all usage of locks - most lines are just tabbed in, but Github unfortunately reads the entire line as changed. - Remove usage of defer. We can immediately respond since there is no significant delay between receiving the command and responding. - Refactor extension code to make race condition handling easier - Refactor check room channel code and address remaining race condition in it - Move room channel creations to a task --- cogs/SquadQueue.py | 354 +++++++++++++++++++++++++-------------------- mogi_objects.py | 11 +- 2 files changed, 204 insertions(+), 161 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d2575cf..a372dfa 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -89,8 +89,6 @@ def __init__(self, bot): self.GENERAL_CHANNEL = None - self.LOCK = asyncio.Lock() - self.URL = bot.config["url"] self.PLACEMENT_PLAYER_MMR = self.bot.config["PLACEMENT_PLAYER_MMR"] @@ -144,6 +142,10 @@ def __init__(self, bot): self.ratings = mmr.Ratings() + # Parameters for tracking if we should send an extension message or not + self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) + self.cur_extension_message = None + @commands.Cog.listener() async def on_ready(self): self.GUILD = self.bot.get_guild(self.bot.config["guild_id"]) @@ -178,6 +180,7 @@ async def on_ready(self): print("Ready!", flush=True) self.refresh_ratings.start() self.refresh_helper_roles.start() + self.check_room_channels_task.start() async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -266,7 +269,7 @@ async def extend(self, interaction: discord.Interaction, minutes: int): """ mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: - await interaction.followup.send("Queue has not started yet.") + await interaction.response.send_message("Queue has not started yet.") return mogi.additional_extension += timedelta(minutes=minutes) await interaction.response.send_message(f"Extended queue by an additional {minutes} minute(s).") @@ -284,101 +287,105 @@ async def can(self, interaction: discord.Interaction): await self.join_queue(interaction) async def join_queue(self, interaction: discord.Interaction, host=False): - await interaction.response.defer() - async with self.LOCK: - member = interaction.user - if not self.is_production: - if common.SERVER is common.Server.MK8DX: - # is actually a user and not a member - member = await self.bot.fetch_user(318637887597969419) - elif common.SERVER is common.Server.MKW: - member = await self.bot.fetch_user(82862780591378432) - mogi = self.get_mogi(interaction) - if mogi is None or not mogi.started or not mogi.gathering: - await interaction.followup.send("Queue has not started yet.") - return + member = interaction.user + if not self.is_production: + if common.SERVER is common.Server.MK8DX: + # is actually a user and not a member + member = await self.bot.fetch_user(318637887597969419) + elif common.SERVER is common.Server.MKW: + member = await self.bot.fetch_user(82862780591378432) + mogi = self.get_mogi(interaction) + if mogi is None or not mogi.started or not mogi.gathering: + await interaction.response.send_message("Queue has not started yet.") + return - player_team = mogi.check_player(member) - player = None if player_team is None else player_team.get_player( - member) - - if player is not None: - original_host_status = player.host - player.host = host - # The player is already signed up, but they might be changing to a host or non-host. Begin checks: - # The player was queued as host, and they queued again as a host - if original_host_status and host: - await interaction.followup.send(f"{interaction.user.mention} is already signed up as a host.") - # The player was queued as host, and they queued again as a non-host - elif original_host_status and not host: - await interaction.followup.send(f"{interaction.user.mention} has changed to a non-host.") - # The player was not queued as host, but they are changing to a host - elif not original_host_status and host: - await interaction.followup.send(f"{interaction.user.mention} has changed to a host.") - # The player was not queued as host and did not change to a host - elif not original_host_status and not host: - await interaction.followup.send(f"{interaction.user.mention} is already signed up.") - return + player_team = mogi.check_player(member) + player = None if player_team is None else player_team.get_player( + member) - # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, - # so this will check the leaderboard first - players = self.ratings.get_rating([member]) - - msg = "" - # If the no rating was found... - if len(players) == 0 or players[0] is None: - # ... check for placement discord role ID: - placement_role_id = self.bot.config["placement_role_id"] - if member.get_role(placement_role_id): - starting_player_mmr = self.PLACEMENT_PLAYER_MMR - players = [ - Player(member, member.display_name, starting_player_mmr)] - msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi " \ - f"with a starting MMR of {starting_player_mmr}. If you believe this is a mistake, " \ - f"please contact a staff member for help.\n" - # ...if discord user doesn't have placement role ID, send error to Discord - else: - msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was " \ - f"unsuccessful. Please try again. If the problem continues then contact a staff member " \ - f"for help." - await interaction.followup.send(msg) - return + if player is not None: + original_host_status = player.host + player.host = host + # The player is already signed up, but they might be changing to a host or non-host. Begin checks: + # The player was queued as host, and they queued again as a host + if original_host_status and host: + await interaction.response.send_message(f"{interaction.user.mention} is already signed up as a host.") + # The player was queued as host, and they queued again as a non-host + elif original_host_status and not host: + await interaction.response.send_message(f"{interaction.user.mention} has changed to a non-host.") + # The player was not queued as host, but they are changing to a host + elif not original_host_status and host: + await interaction.response.send_message(f"{interaction.user.mention} has changed to a host.") + # The player was not queued as host and did not change to a host + elif not original_host_status and not host: + await interaction.response.send_message(f"{interaction.user.mention} is already signed up.") + return - players[0].confirmed = True - players[0].host = host - squad = Team(players) - mogi.teams.append(squad) - host_str = " as a host " if host else " " - msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + # FIRST look up the player - sometimes MK8DX bots add placement role to non placement players, + # so this will check the leaderboard first + players = self.ratings.get_rating([member]) - await interaction.followup.send(msg) - await self.check_room_channels(mogi) - await self.check_num_teams(mogi) + msg = "" + # If the no rating was found... + if len(players) == 0 or players[0] is None: + # ... check for placement discord role ID: + placement_role_id = self.bot.config["placement_role_id"] + if member.get_role(placement_role_id): + starting_player_mmr = self.PLACEMENT_PLAYER_MMR + players = [ + Player(member, member.display_name, starting_player_mmr)] + msg += f"{players[0].lounge_name} is assumed to be a new player and will be playing this mogi " \ + f"with a starting MMR of {starting_player_mmr}. If you believe this is a mistake, " \ + f"please contact a staff member for help.\n" + # ...if discord user doesn't have placement role ID, send error to Discord + else: + msg = f"{interaction.user.mention} fetch for MMR has failed and joining the queue was " \ + f"unsuccessful. Please try again. If the problem continues then contact a staff member " \ + f"for help." + await interaction.response.send_message(msg) + return + + players[0].confirmed = True + players[0].host = host + squad = Team(players) + mogi.teams.append(squad) + host_str = " as a host " if host else " " + msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + + event_status_launched = self.check_close_event_change() + try: + await interaction.response.send_message(msg) + finally: + if event_status_launched: + await self.launch_mogi() @app_commands.command(name="d") @app_commands.guild_only() async def drop(self, interaction: discord.Interaction): """Remove user from mogi""" - await interaction.response.defer() - async with self.LOCK: - mogi = self.get_mogi(interaction) - if mogi is None or not mogi.started or not mogi.gathering: - await interaction.followup.send("Queue has not started yet.") - return + mogi = self.get_mogi(interaction) + if mogi is None or not mogi.started or not mogi.gathering: + await interaction.response.send_message("Queue has not started yet.") + return - member = interaction.user - squad = mogi.check_player(member) - if squad is None: - await interaction.followup.send(f"{member.display_name} is not currently in this event; type `/c` or `/ch` to join") - return - mogi.teams.remove(squad) - msg = "Removed " - msg += ", ".join([p.lounge_name for p in squad.players]) - msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" - msg += f", `[{mogi.count_registered()} players]`" + member = interaction.user + squad = mogi.check_player(member) + if squad is None: + await interaction.response.send_message(f"{member.display_name} is not currently in this event; type `/c` or `/ch` to join") + return + mogi.teams.remove(squad) + msg = "Removed " + msg += ", ".join([p.lounge_name for p in squad.players]) + msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" + msg += f", `[{mogi.count_registered()} players]`" + + event_status_launched = self.check_close_event_change() + try: + await interaction.response.send_message(msg) + finally: + if event_status_launched: + await self.launch_mogi() - await interaction.followup.send(msg) - await self.check_num_teams(mogi) @app_commands.command(name="sub") @app_commands.guild_only() @@ -580,24 +587,28 @@ async def scoreboard(self, interaction: discord.Interaction): @app_commands.guild_only() async def remove_player(self, interaction: discord.Interaction, member: discord.Member): """Removes a specific player from the current queue. Staff use only.""" - await interaction.response.defer() - async with self.LOCK: - mogi = self.get_mogi(interaction) - if mogi is None or not mogi.started or not mogi.gathering: - await interaction.followup.send("Queue has not started yet.") - return + mogi = self.get_mogi(interaction) + if mogi is None or not mogi.started or not mogi.gathering: + await interaction.response.send_message("Queue has not started yet.") + return - squad = mogi.check_player(member) - if squad is None: - await interaction.followup.send(f"{member.display_name} is not currently in this event") - return - mogi.teams.remove(squad) - msg = "Staff has removed " - msg += ", ".join([p.lounge_name for p in squad.players]) - msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" - msg += f", `[{mogi.count_registered()} players]`" + squad = mogi.check_player(member) + if squad is None: + await interaction.response.send_message(f"{member.display_name} is not currently in this event") + return + mogi.teams.remove(squad) + msg = "Staff has removed " + msg += ", ".join([p.lounge_name for p in squad.players]) + msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" + msg += f", `[{mogi.count_registered()} players]`" + + event_status_launched = self.check_close_event_change() + try: + await interaction.response.send_message(msg) + finally: + if event_status_launched: + await self.launch_mogi() - await interaction.followup.send(msg) @app_commands.command(name="ping_staff") @app_commands.guild_only() @@ -886,8 +897,6 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): # since discord only allows 50 thread channels to be created per 5 minutes. async def check_room_channels(self, mogi): num_created_rooms = len(mogi.rooms) - if num_created_rooms >= mogi.max_possible_rooms: - return for i in range(num_created_rooms, mogi.max_possible_rooms): display_time = mogi.display_time minute = display_time.minute @@ -898,6 +907,11 @@ async def check_room_channels(self, mogi): room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, invitable=False) + # Address race condition - race condition would result in making too many rooms + if len(mogi.rooms) >= mogi.max_possible_rooms: + await room_channel.delete() + return + except Exception as e: print(traceback.format_exc()) err_msg = f"\nAn error has occurred while creating a room channel:\n{e}" @@ -928,6 +942,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic mogi.making_rooms_run = True mogi.gathering = False + await self.check_room_channels(mogi) + await self.lockdown(mogi.mogi_channel) if was_gathering: await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") @@ -1024,49 +1040,60 @@ def allowed_players_check(players): self.old_events.append(self.ongoing_event) self.ongoing_event = None - async def check_num_teams(self, mogi: Mogi): - if not mogi.gathering or not mogi.is_automated: - return - cur_time = datetime.now(timezone.utc) - if mogi.start_time <= cur_time: - num_leftover_teams = mogi.count_registered() % int( - (12 / mogi.max_player_per_team)) - if num_leftover_teams == 0: - mogi.gathering = False - await self.lockdown(mogi.mogi_channel) - await mogi.mogi_channel.send( - "A sufficient amount of players has been reached, so the mogi has been closed to extra players. Rooms will be made within the next minute.") - - async def ongoing_mogi_checks(self): + def check_close_event_change(self) -> Tuple[bool, bool]: + """Returns a bool indicating if the event was gathering but this function then closed the mogi depending on + the time and number of players or other logic""" mogi = self.ongoing_event if mogi is not None: - # If it's not automated, not started, we've already started making the rooms, don't run this - async with self.LOCK: - if not mogi.is_automated or not mogi.started or mogi.making_rooms_run: - return - cur_time = datetime.now(timezone.utc) - force_start_time = mogi.start_time + \ - self.EXTENSION_TIME + mogi.additional_extension - if force_start_time <= cur_time: + # If it's not automated, don't run this + # If the mogi has not started, don't run this + # If the mogi is not gathering, don't run this + # If the mogi has already made the rooms, don't run this. + # This logic was taken from a much more complex bot. It could be greatly simplified since all events here + # are automated and follow a certain flow, but I am not going to change what isn't broken. + if not mogi.is_automated or not mogi.started or mogi.making_rooms_run or not mogi.gathering: + return False + cur_time = datetime.now(timezone.utc) + force_start_time = mogi.start_time + \ + self.EXTENSION_TIME + mogi.additional_extension + if force_start_time <= cur_time: + mogi.gathering = False + self.cur_extension_message = None + return True + elif mogi.start_time <= cur_time and mogi.gathering: + # check if there are an even amount of teams since we are past the queue time + num_leftover_teams = mogi.count_registered() % int( + (12 / mogi.max_player_per_team)) + if num_leftover_teams == 0: mogi.gathering = False - elif mogi.start_time <= cur_time and mogi.gathering: - # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % int( - (12 / mogi.max_player_per_team)) - if num_leftover_teams == 0: - mogi.gathering = False - else: - if int(cur_time.second / 20) == 0: - minutes_left = (force_start_time - - cur_time).seconds // 60 - x_teams = int( - int(12 / mogi.max_player_per_team) - num_leftover_teams) - await mogi.mogi_channel.send( - f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless.") - if not mogi.gathering: - await self.delete_list_messages(0) - await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") - await self.add_teams_to_rooms(mogi, (mogi.start_time.minute) % 60, True) + self.cur_extension_message = None + return True + else: + cur_time = datetime.now(timezone.utc).replace(second=0, microsecond=0) + # At this point, we're in the extension time. So if the extension timestamp is in a different + # minute than the last one, we set the new extension message to be sent. + if cur_time != self.last_extension_message_timestamp: + self.last_extension_message_timestamp = cur_time + minutes_left = (force_start_time - + cur_time).seconds // 60 + x_teams = int( + int(12 / mogi.max_player_per_team) - num_leftover_teams) + self.cur_extension_message = f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless." + return False + + async def launch_mogi(self): + mogi = self.ongoing_event + if mogi is not None: + await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") + await self.delete_list_messages(0) + await self.add_teams_to_rooms(mogi, mogi.start_time.minute % 60, True) + + async def check_send_extension_message(self): + if self.cur_extension_message is not None: + to_send = self.cur_extension_message + self.cur_extension_message = None + if self.ongoing_event is not None: + await self.ongoing_event.mogi_channel.send(to_send) async def scheduler_mogi_start(self): cur_time = datetime.now(timezone.utc) @@ -1108,7 +1135,9 @@ async def sqscheduler(self): except Exception as e: print(traceback.format_exc()) try: - await self.ongoing_mogi_checks() + if self.check_close_event_change(): + await self.launch_mogi() + await self.check_send_extension_message() except Exception as e: print(traceback.format_exc()) @@ -1171,6 +1200,15 @@ async def delete_old_mogis(self): except Exception as e: print(traceback.format_exc()) + @tasks.loop(minutes=3) + async def check_room_channels_task(self): + try: + if self.ongoing_event is not None: + await self.check_room_channels(self.ongoing_event) + except: + print(traceback.format_exc()) + + @tasks.loop(hours=24) async def refresh_helper_roles(self): """Refreshes the helper staff role names for the /ping_staff command using the role IDs in the config""" @@ -1247,8 +1285,8 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): mogi.teams.append(squad) msg = f"{players[0].lounge_name} added 12 times." await self.queue_or_send(ctx, msg) - await self.check_room_channels(mogi) - await self.check_num_teams(mogi) + if self.check_close_event_change(): + await self.launch_mogi() @commands.command(name="debug_add_many_ratings") @commands.is_owner() @@ -1259,15 +1297,21 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): if (not await self.is_started(ctx, mogi) or not await self.is_gathering(ctx, mogi)): return + member = ctx.author + if self.is_production: + if common.SERVER is common.Server.MK8DX: + member = await self.bot.fetch_user(318637887597969419) + elif common.SERVER is common.Server.MKW: + member = await self.bot.fetch_user(433353529655296011) for i, rating in enumerate(ratings, 1): if rating.isdecimal(): - player = Player(ctx.author, f"{ctx.author.display_name} {i}", int( + player = Player(member, f"{member.name} {i}", int( rating), confirmed=True) mogi.teams.append(Team([player])) msg = f"Players added with the following ratings: {' '.join(['`' + r + '`' for r in ratings])}" await self.queue_or_send(ctx, msg) - await self.check_room_channels(mogi) - await self.check_num_teams(mogi) + if self.check_close_event_change(): + await self.launch_mogi() @commands.command(name="debug_add_many_players") @commands.is_owner() @@ -1281,11 +1325,11 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem return member = ctx.author - if not self.is_production: + if self.is_production: if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: - member = await self.bot.fetch_user(82862780591378432) + member = await self.bot.fetch_user(314861232693706752) check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) @@ -1297,8 +1341,8 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem mogi.teams.append(squad) msg = f"{players[0].lounge_name} added {num_times} times." await self.queue_or_send(ctx, msg) - await self.check_room_channels(mogi) - await self.check_num_teams(mogi) + if self.check_close_event_change(): + await self.launch_mogi() @commands.command(name="debug_start_rooms") @commands.is_owner() diff --git a/mogi_objects.py b/mogi_objects.py index 97bbd56..ba4eb31 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -457,19 +457,18 @@ def __init__(self, room: Room, get_rating_from_discord_id, sub_range_mmr_allowan @discord.ui.button(label="Join Room") async def button_callback(self, interaction: discord.Interaction, button): - await interaction.response.defer() if self.is_restricted is not None and self.is_restricted(interaction.user): - await interaction.followup.send( + await interaction.response.send_message( "Players with the muted or restricted role cannot use the sub button.", ephemeral=True) return if interaction.user.id in self.room.get_player_list() + self.room.subs: - await interaction.followup.send( + await interaction.response.send_message( "You are already in this room.", ephemeral=True) return try: user_mmr = self.get_rating_from_discord_id(interaction.user.id) except: - await interaction.followup.send( + await interaction.response.send_message( "MMR lookup for player has failed, please try again.", ephemeral=True) return @@ -478,11 +477,11 @@ async def button_callback(self, interaction: discord.Interaction, button): if isinstance(user_mmr, int) and mmr_high + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: self.room.subs.append(interaction.user.id) button.disabled = True - await interaction.followup.edit_message(interaction.message.id, view=self) + await interaction.response.edit_message(interaction.message.id, view=self) mention = interaction.user.mention await self.room.thread.send(f"{mention} has joined the room.") else: - await interaction.followup.send( + await interaction.response.send_message( "You do not meet room requirements", ephemeral=True) From 5e4b036b14c3ea41ad66b3e8487c28adade7ff30 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:26:20 -0500 Subject: [PATCH 166/292] Type hinting for IDE --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a372dfa..3717383 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -87,7 +87,7 @@ def __init__(self, bot): self.HISTORY_CHANNEL = None - self.GENERAL_CHANNEL = None + self.GENERAL_CHANNEL: discord.TextChannel = None self.URL = bot.config["url"] From 340576a18058bcf3ffb0b7e4170a08f80283dce0 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:28:52 -0500 Subject: [PATCH 167/292] Bug fix (minor display issue) with extension display --- cogs/SquadQueue.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 3717383..f5896b3 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1069,11 +1069,11 @@ def check_close_event_change(self) -> Tuple[bool, bool]: self.cur_extension_message = None return True else: - cur_time = datetime.now(timezone.utc).replace(second=0, microsecond=0) + cur_extension_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) # At this point, we're in the extension time. So if the extension timestamp is in a different # minute than the last one, we set the new extension message to be sent. - if cur_time != self.last_extension_message_timestamp: - self.last_extension_message_timestamp = cur_time + if cur_extension_timestamp != self.last_extension_message_timestamp: + self.last_extension_message_timestamp = cur_extension_timestamp minutes_left = (force_start_time - cur_time).seconds // 60 x_teams = int( From 1e95c7fa9fab271020a4f3d33cee9b8a5730891a Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:29:50 -0500 Subject: [PATCH 168/292] Bug fix --- mmr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mmr.py b/mmr.py index d0c2635..c81c46c 100644 --- a/mmr.py +++ b/mmr.py @@ -169,7 +169,8 @@ def _parse_mkw_ratings(self, results: dict): if discord_user_id is not None: self.ratings[player["discord_user_id"]] = (player["current_mmr"], player["player_name"]) - def get_rating_from_discord_id(self, discord_id: str) -> int | None: + def get_rating_from_discord_id(self, discord_id: int | str) -> int | None: + discord_id = str(discord_id) if not self.first_run_complete: raise RatingsNotReady("Ratings not pulled yet.") if discord_id in self.ratings: From 20bb288e04a02246462e45dfc06917f7cb0a6d3d Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 02:30:08 -0500 Subject: [PATCH 169/292] Fix incorrect type hinting --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index d5010ef..ab31cd2 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -169,7 +169,7 @@ def __init__(self, teams, room_num: int, thread: discord.Thread): self.view = None self.finished = False self.host_list: List["Player"] = [] - self.subs: List["Player"] = [] + self.subs: List[int] = [] @property def mmr_high(self) -> int: From 499a7eecc8b4c554d0e958b0fe712ec3ad355ee5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 19 Jul 2024 03:28:05 -0500 Subject: [PATCH 170/292] Bug fix --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index ba4eb31..02c6d6c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -477,7 +477,7 @@ async def button_callback(self, interaction: discord.Interaction, button): if isinstance(user_mmr, int) and mmr_high + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: self.room.subs.append(interaction.user.id) button.disabled = True - await interaction.response.edit_message(interaction.message.id, view=self) + await interaction.response.edit_message(view=self) mention = interaction.user.mention await self.room.thread.send(f"{mention} has joined the room.") else: From 38bf1461b9fea099aa4c430aa7068b3b21842625 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:39:05 -0500 Subject: [PATCH 171/292] Hotfix: Bot error on start bc not defined --- cogs/SquadQueue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index f5896b3..14e32c3 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -66,6 +66,10 @@ def __init__(self, bot): self.sq_times = [] + # Parameters for tracking if we should send an extension message or not + self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) + self.cur_extension_message = None + self._scheduler_task = self.sqscheduler.start() self._msgqueue_task = self.send_queued_messages.start() self._list_task = self.list_task.start() From 6f2fd556e9ee1250359d6d97f7bd83f4cf37e51c Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:39:25 -0500 Subject: [PATCH 172/292] Testing Bot: start queue immediately --- cogs/SquadQueue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 14e32c3..8de4519 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -136,6 +136,10 @@ def __init__(self, bot): tzinfo=timezone.utc) + timedelta( minutes=bot.config["FIRST_EVENT_TIME"]) + # If we're testing, set the time to the current time so we start a new event immediately + if not self.is_production: + self.FIRST_EVENT_TIME = datetime.now(timezone.utc) + with open('./timezones.json', 'r') as cjson: self.timezones = json.load(cjson) From d59d4e6b56819f2b0e5f6dad2e556ef99bec98c5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:40:17 -0500 Subject: [PATCH 173/292] See description Continuation of 38bf1461b9fea099aa4c430aa7068b3b21842625 --- cogs/SquadQueue.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8de4519..37cf241 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -150,9 +150,6 @@ def __init__(self, bot): self.ratings = mmr.Ratings() - # Parameters for tracking if we should send an extension message or not - self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) - self.cur_extension_message = None @commands.Cog.listener() async def on_ready(self): From 9fa82ebe56f3ee0139040efb6e9016fcff876c84 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:40:38 -0500 Subject: [PATCH 174/292] Better testing for MKW --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 37cf241..eeec92d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -298,7 +298,8 @@ async def join_queue(self, interaction: discord.Interaction, host=False): # is actually a user and not a member member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: - member = await self.bot.fetch_user(82862780591378432) + # member = await self.bot.fetch_user(82862780591378432) + pass mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: await interaction.response.send_message("Queue has not started yet.") From 6d197c6776c98e7ea57768714573a9cc5a712dbc Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:40:56 -0500 Subject: [PATCH 175/292] Hotfix: fix no lockdown when not enough rooms --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index eeec92d..9902a95 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -939,6 +939,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic return if mogi.making_rooms_run and started_automatically: return + + await self.lockdown(mogi.mogi_channel) if mogi.max_possible_rooms == 0: self.ongoing_event = None await mogi.mogi_channel.send(f"Not enough players to fill a single room! This mogi will be cancelled.") @@ -950,7 +952,6 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic await self.check_room_channels(mogi) - await self.lockdown(mogi.mogi_channel) if was_gathering: await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") From b0ffbf69d4b7a4c6923148845129a5e4f37dabfa Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:41:46 -0500 Subject: [PATCH 176/292] Make valid players check class function --- cogs/SquadQueue.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9902a95..9af5606 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -220,6 +220,15 @@ async def queue_or_send(self, ctx, msg, delay=0): if delay > 0: await sendmsg.delete(delay=delay) + # Support for both server to implement their own algorithm for a player list being allowed + # For now, both servers have a simple rating range check that a list of players must meet, + # but each server could implement more complex checks on a given list of players. + def allowed_players_check(self, players: List[Player]): + if common.SERVER is common.Server.MKW: + return mkw_players_allowed(players, self.room_mmr_threshold) + elif common.SERVER is common.Server.MK8DX: + return mk8dx_players_allowed(players, self.room_mmr_threshold) + # goes thru the msg queue for each channel and combines them # into as few messsages as possible, then sends them @tasks.loop(seconds=2) @@ -962,30 +971,19 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic for r in self.bot.config["roles_for_channels"]: extra_members.append(mogi.mogi_channel.guild.get_role(r)) - # Support for both server to implement their own algorithm for a player list being allowed - # For now, both servers have a simple rating range check that a list of players must meet, - # but each server could implement more complex checks on a given list of players. - if common.SERVER is common.Server.MKW: - def allowed_players_check(players): - return mkw_players_allowed(players, self.room_mmr_threshold) - elif common.SERVER is common.Server.MK8DX: - def allowed_players_check(players): - return mk8dx_players_allowed(players, self.room_mmr_threshold) - all_confirmed_players = mogi.players_on_confirmed_teams() first_late_player_index = ( mogi.num_players//mogi.players_per_room) * mogi.players_per_room regular_player_list = all_confirmed_players[:first_late_player_index] late_player_list = all_confirmed_players[first_late_player_index:] - proposed_list = sorted(mogi.generate_proposed_list( - allowed_players_check), reverse=True) + proposed_list = mogi.generate_proposed_list(self.allowed_players_check) await mogi.populate_host_fcs() for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): added_str = ": **Added from late players**" if player in late_player_list else "" msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} MMR){added_str}\n""" - if not allowed_players_check(room_players): + if not self.allowed_players_check(room_players): msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: curr_room = mogi.rooms[room_number - 1] From d13362bfa65e02756ea0649b9c0065e3c633f15e Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:42:11 -0500 Subject: [PATCH 177/292] Hotfix: how did this happen? --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9af5606..d0c9823 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1303,7 +1303,7 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): or not await self.is_gathering(ctx, mogi)): return member = ctx.author - if self.is_production: + if not self.is_production: if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: From f6abb73f8771917c889ad1324abcbc9b0816dc09 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:42:41 -0500 Subject: [PATCH 178/292] Hotfix: negative ratings did not read in testing func --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d0c9823..70c0b5e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1309,7 +1309,7 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(433353529655296011) for i, rating in enumerate(ratings, 1): - if rating.isdecimal(): + if common.is_int(rating): player = Player(member, f"{member.name} {i}", int( rating), confirmed=True) mogi.teams.append(Team([player])) From 6ea903abaea6192b8b96b27134435f9a27567df5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:42:54 -0500 Subject: [PATCH 179/292] Hotfix: how did this get in? --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 70c0b5e..db4cec7 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1330,7 +1330,7 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem return member = ctx.author - if self.is_production: + if not self.is_production: if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: From 828b03e8c8debb4f96582eb4a2a543156249c2de Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:43:12 -0500 Subject: [PATCH 180/292] Minor line refactor --- cogs/SquadQueue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index db4cec7..d300083 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1067,8 +1067,7 @@ def check_close_event_change(self) -> Tuple[bool, bool]: return True elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % int( - (12 / mogi.max_player_per_team)) + num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) if num_leftover_teams == 0: mogi.gathering = False self.cur_extension_message = None From e7bf762d5a6733a31d62f142782dab95f2ba876f Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:43:40 -0500 Subject: [PATCH 181/292] Feature: Continue extension if rooms would cancel --- cogs/SquadQueue.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d300083..c6de131 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1068,10 +1068,21 @@ def check_close_event_change(self) -> Tuple[bool, bool]: elif mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) + # If have en even number of players... if num_leftover_teams == 0: - mogi.gathering = False - self.cur_extension_message = None - return True + # ...and none of the rooms will be cancelled, close the queue and begin + if not mogi.any_room_cancelled(self.allowed_players_check): + mogi.gathering = False + self.cur_extension_message = None + return True + # ...any of the rooms will be cancelled, set the error message + else: + self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) + minutes_left = (force_start_time - cur_time).seconds // 60 + self.cur_extension_message = f"One or more of the rooms will be cancelled, so the extension " \ + f"will continue. Starting in {minutes_left + 1} minute(s) " \ + f"regardless." + else: cur_extension_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) # At this point, we're in the extension time. So if the extension timestamp is in a different From 756509392ca0ca2639b939567ec9480fd11e2a43 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:43:52 -0500 Subject: [PATCH 182/292] common.is_int helper function --- common.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common.py b/common.py index 6ce0e6c..8552213 100644 --- a/common.py +++ b/common.py @@ -32,3 +32,11 @@ def flatten(matrix: List[List[_T]]) -> List[_T]: for row in matrix: flat_list.extend(row) return flat_list + + +def is_int(var: str) -> bool: + try: + int(var) + return True + except ValueError: + return False \ No newline at end of file From 58e4900b2581cae7cc2c4450691b6df8aeab485a Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:45:06 -0500 Subject: [PATCH 183/292] Any room cancelled functions --- mogi_objects.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 49382ab..554f0e2 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -19,6 +19,7 @@ class Mogi: ALGORITHM_STATUS_2_OR_MORE_ROOMS = 2 ALGORITHM_STATUS_SUCCESS_FOUND = 3 ALGORITHM_STATUS_SUCCESS_EMPTY = 4 + ALGORITHM_STATUS_SUCCESS_SOME_INVALID = 5 def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, mogi_channel: discord.TextChannel, is_automated=False, start_time=None, display_time=None, additional_extension_minutes=0): @@ -123,6 +124,22 @@ def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], b else: raise ValueError(f"Unknown server in config: {common.Server}") + def _count_cancelled(self, player_list: List[Player], valid_players_check: Callable[[List[Player]], bool] = None) -> int: + """Using the provided valid players check function and player list, returns the number of rooms that would be cancelled.""" + if valid_players_check is None: + return 0 + return sum(1 for room_players in common.divide_chunks(player_list, self.players_per_room) if not valid_players_check(room_players)) + + def _any_cancelled(self, player_list: List[Player], valid_players_check: Callable[[List[Player]], bool] = None) -> bool: + return self._count_cancelled(player_list, valid_players_check) > 0 + + def any_room_cancelled(self, valid_players_check: Callable[[List[Player]], bool] = None) -> bool: + """Using the provided valid players check function, returns if any of the rooms will be cancelled.""" + if valid_players_check is None: + return False + proposed_list = self.generate_proposed_list(valid_players_check) + return self._any_cancelled(proposed_list, valid_players_check) + def check_player(self, member): for team in self.teams: if team.has_player(member): From 60369a051ef60915d6c561a8ddfea4c7def460c0 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:46:43 -0500 Subject: [PATCH 184/292] Fix bug where players show all removed when cancelled mogi --- mogi_objects.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 554f0e2..c29d675 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -99,15 +99,17 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla break cur_check_list.append(late_players.pop(0)) # Even after checking the late players, we did not find - return [], Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY + return best_collection, Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY def _mk8dx_generate_final_list(self) -> List[Player]: confirmed_players = self.players_on_confirmed_teams() return confirmed_players[0:self.players_per_room * self.max_possible_rooms] def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: - result, _ = self._one_room_final_list_algorithm(valid_players_check) - return result + result, status = self._all_room_final_list_algorithm(valid_players_check) + if status is Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS: + return self.players_on_confirmed_teams() + return sorted(common.flatten(result), reverse=True) # I do not want to formally prove that sorting here is correct def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], bool] = None) -> List[Player]: """Algorithm that generates a proposed list of players that will play. This algorithm may differ between From 11827f4a63458e1aba01727cd2957e912e5989db Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:47:28 -0500 Subject: [PATCH 185/292] Any modifications, including sorting, should be done by the algorithms. --- mogi_objects.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index c29d675..0d0630a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -84,7 +84,7 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS if self.max_possible_rooms > 1: confirmed_players = self.players_on_confirmed_teams() - return confirmed_players[0:self.players_per_room * self.max_possible_rooms], Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS + return sorted(confirmed_players[0:self.players_per_room * self.max_possible_rooms], reverse=True), Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS # At this point, we can only make one possible room, so our algorithm will be used confirmed_players = self.players_on_confirmed_teams() cur_check_list = list(confirmed_players[0:self.players_per_room]) @@ -94,7 +94,7 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla best_collection = Mogi._minimize_range( cur_check_list, self.players_per_room) if valid_players_check(best_collection): - return best_collection, Mogi.ALGORITHM_STATUS_SUCCESS_FOUND + return sorted(best_collection, reverse=True), Mogi.ALGORITHM_STATUS_SUCCESS_FOUND if len(late_players) == 0: break cur_check_list.append(late_players.pop(0)) @@ -103,7 +103,9 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla def _mk8dx_generate_final_list(self) -> List[Player]: confirmed_players = self.players_on_confirmed_teams() - return confirmed_players[0:self.players_per_room * self.max_possible_rooms] + if self.max_possible_rooms == 0: + return confirmed_players + return sorted(confirmed_players[0:self.players_per_room * self.max_possible_rooms], reverse=True) def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: result, status = self._all_room_final_list_algorithm(valid_players_check) From a5bba140bd06b088102f9cbc5695dfba989f676f Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:47:48 -0500 Subject: [PATCH 186/292] All room algorithm complete --- mogi_objects.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 0d0630a..5d97981 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -79,6 +79,36 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N lowest_player_index + num_players] return best_collection + + + def _all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: + if self.max_possible_rooms == 0: + return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS + all_confirmed_players = self.players_on_confirmed_teams() + first_late_player_index = (self.num_players // self.players_per_room) * self.players_per_room + on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) + late_players = all_confirmed_players[first_late_player_index:] + player_rooms: List[List[Player]] = list(common.divide_chunks(on_time_players, self.players_per_room)) + + any_invalid = False + for pr_index, player_room in enumerate(player_rooms, 0): + for lp_index in range(len(late_players)+1): + current_late_check_players = late_players[0:lp_index] + best_collection = Mogi._minimize_range(player_room + current_late_check_players, self.players_per_room) + if valid_players_check(best_collection): + best_collection.sort(reverse=True) + # Get all the players who were swapped out of the room and put them at the front of the late player list + swapped_out_players = [p for p in player_room if p not in best_collection] + swapped_in_players = [p for p in best_collection if p not in player_room] + late_players = swapped_out_players + list(filter(lambda p: p not in swapped_in_players, late_players)) + player_rooms[pr_index] = best_collection + break + else: + # After checking all late players, no room player list with a valid range could be found for this room + any_invalid = True + + return player_rooms, (Mogi.ALGORITHM_STATUS_SUCCESS_SOME_INVALID if any_invalid else Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[Player], int]: if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS From a8cc80234380141e37ac7708f643b6afe103da02 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:48:10 -0500 Subject: [PATCH 187/292] All room old algorithm (not used) for alternative functionality --- mogi_objects.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index 5d97981..2896447 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -79,6 +79,33 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N lowest_player_index + num_players] return best_collection + def _OLD_all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: + if self.max_possible_rooms == 0: + return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS + all_confirmed_players = self.players_on_confirmed_teams() + first_late_player_index = (self.num_players // self.players_per_room) * self.players_per_room + on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) + late_players = all_confirmed_players[first_late_player_index:] + player_rooms: List[List[Player]] = list(common.divide_chunks(on_time_players, self.players_per_room)) + + any_invalid = False + for pr_index, player_room in enumerate(player_rooms, 0): + for lp_index in range(len(late_players)+1): + current_late_check_players = late_players[0:lp_index] + best_collection = Mogi._minimize_range(player_room + current_late_check_players, self.players_per_room) + if valid_players_check(best_collection): + best_collection.sort(reverse=True) + # Get all the players who were swapped out of the room and put them at the front of the late player list + swapped_out_players = [p for p in player_room if p not in best_collection] + swapped_in_players = [p for p in best_collection if p not in player_room] + late_players = swapped_out_players + list(filter(lambda p: p not in swapped_in_players, late_players)) + player_rooms[pr_index] = best_collection + break + else: + # After checking all late players, no room player list with a valid range could be found for this room + any_invalid = True + + return player_rooms, (Mogi.ALGORITHM_STATUS_SUCCESS_SOME_INVALID if any_invalid else Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) def _all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: From 2328c5d106feaada27f2c830bfec91769bb6798c Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 06:38:38 -0500 Subject: [PATCH 188/292] Suggested lifetime changes --- example_configs/prod/mkw_ct_prod_config.json | 2 +- example_configs/prod/mkw_rt_prod_config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index 27d17c4..9bee5ad 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -51,7 +51,7 @@ "DISPLAY_OFFSET_MINUTES": 0, "EXTENSION_TIME": 5, "ROOM_JOIN_PENALTY_TIME": 8, - "MOGI_LIFETIME": 180, + "MOGI_LIFETIME": 150, "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 9000 diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 46515fe..d9ca465 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -51,7 +51,7 @@ "DISPLAY_OFFSET_MINUTES": 0, "EXTENSION_TIME": 5, "ROOM_JOIN_PENALTY_TIME": 8, - "MOGI_LIFETIME": 180, + "MOGI_LIFETIME": 120, "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 8000 From e69e44add6ab0af7e304e32bf33a3c854040c6e9 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:40:45 -0500 Subject: [PATCH 189/292] Changes to configs --- example_configs/prod/mk8dx_prod_config.json | 3 ++- example_configs/prod/mkw_ct_prod_config.json | 3 ++- example_configs/prod/mkw_rt_prod_config.json | 12 +++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index 9430b3c..a4db25e 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -43,5 +43,6 @@ "MOGI_LIFETIME": 180, "SUB_RANGE_MMR_ALLOWANCE": 500, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 9999999 + "ROOM_MMR_THRESHOLD": 9999999, + "USE_THREADS": true } \ No newline at end of file diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index 9bee5ad..5e40493 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -54,5 +54,6 @@ "MOGI_LIFETIME": 150, "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 9000 + "ROOM_MMR_THRESHOLD": 9000, + "USE_THREADS": true } \ No newline at end of file diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index d9ca465..70e975f 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -54,5 +54,15 @@ "MOGI_LIFETIME": 120, "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, - "ROOM_MMR_THRESHOLD": 8000 + "ROOM_MMR_THRESHOLD": 8000, + "USE_THREADS": false, + "TIER_CHANNELS": { + "1": { + "tier_role_id": 1264149575070126102, + "channel_ids": [ + 1264120088651628596, + 1264120865881325629 + ] + } + } } \ No newline at end of file From f85b5567c3a5e65f90cb21243a3d96847013064c Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:41:31 -0500 Subject: [PATCH 190/292] Bug fix in two Mogi methods --- mogi_objects.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 2896447..f8b4b7e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -219,13 +219,17 @@ def players_on_confirmed_teams(self) -> List[Player]: def is_room_thread(self, channel_id: int): for room in self.rooms: - if room.thread.id == channel_id: + if room is None or room.channel is None: + continue + if room.channel.id == channel_id: return True return False def get_room_from_thread(self, channel_id: int): for room in self.rooms: - if room.thread.id == channel_id: + if room is None or room.channel is None: + continue + if room.channel.id == channel_id: return room return None From db99d42919610d58de6c7aa140586c6f29ca8741 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:42:36 -0500 Subject: [PATCH 191/292] Adjust debugging function for MKW --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c6de131..ab4cb4d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1317,7 +1317,7 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): if common.SERVER is common.Server.MK8DX: member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: - member = await self.bot.fetch_user(433353529655296011) + member = await self.GUILD.fetch_member(1114699357179088917) for i, rating in enumerate(ratings, 1): if common.is_int(rating): player = Player(member, f"{member.name} {i}", int( From b60096574a575b13239ead7aa4674fc132bbc131 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:44:48 -0500 Subject: [PATCH 192/292] check_room_channels_task -> check_room_threads_task --- cogs/SquadQueue.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ab4cb4d..2d029bb 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -185,7 +185,7 @@ async def on_ready(self): print("Ready!", flush=True) self.refresh_ratings.start() self.refresh_helper_roles.start() - self.check_room_channels_task.start() + self.check_room_threads_task.start() async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -910,7 +910,7 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): # make thread channels while the event is gathering instead of at the end, # since discord only allows 50 thread channels to be created per 5 minutes. - async def check_room_channels(self, mogi): + async def check_room_threads(self, mogi: Mogi): num_created_rooms = len(mogi.rooms) for i in range(num_created_rooms, mogi.max_possible_rooms): display_time = mogi.display_time @@ -959,7 +959,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic mogi.making_rooms_run = True mogi.gathering = False - await self.check_room_channels(mogi) + await self.check_room_threads(mogi) if was_gathering: await mogi.mogi_channel.send("Mogi is now closed; players can no longer join or drop from the event") @@ -1216,10 +1216,10 @@ async def delete_old_mogis(self): print(traceback.format_exc()) @tasks.loop(minutes=3) - async def check_room_channels_task(self): + async def check_room_threads_task(self): try: if self.ongoing_event is not None: - await self.check_room_channels(self.ongoing_event) + await self.check_room_threads(self.ongoing_event) except: print(traceback.format_exc()) From 3e055ed6de4b1525b685d2e780cdeecacafdc1d4 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:48:43 -0500 Subject: [PATCH 193/292] Room.thread -> Room.channel --- cogs/SquadQueue.py | 10 +++++----- mogi_objects.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 2d029bb..6d34767 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -552,7 +552,7 @@ async def on_message(self, message: discord.Message): if mogi is None: return room = discord.utils.find( - lambda r: r.thread.id == message.channel.id, mogi.rooms) + lambda r: r.channel.id == message.channel.id, mogi.rooms) if room is None or not room.teams: return team = discord.utils.find( @@ -583,7 +583,7 @@ async def scoreboard(self, interaction: discord.Interaction): return room = discord.utils.find( - lambda room: room.thread.id == interaction.channel_id, mogi.rooms) + lambda room: room.channel.id == interaction.channel_id, mogi.rooms) if not room: await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) return @@ -901,7 +901,7 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): f"Skipping room {index} in function write_history.", flush=True) continue msg = room.view.header_text - msg += f"{room.thread.jump_url}\n" + msg += f"{room.channel.jump_url}\n" msg += room.view.teams_text msg += "ㅤ" await history_channel.send(msg) @@ -1006,11 +1006,11 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic If you need staff's assistance, use the `/ping_staff` command in this channel.""" try: - await curr_room.thread.send(room_msg) view = VoteView(room_players, curr_room.thread, mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME, self.TIER_INFO) + await curr_room.channel.send(room_msg) curr_room.view = view - await curr_room.thread.send(view=view) + await curr_room.channel.send(view=view) except discord.DiscordException: err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" err_msg += player_mentions + extra_member_mentions diff --git a/mogi_objects.py b/mogi_objects.py index f8b4b7e..d214983 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -244,10 +244,10 @@ async def populate_host_fcs(self): class Room: - def __init__(self, teams, room_num: int, thread: discord.Thread): + def __init__(self, teams, room_num: int, channel: discord.Thread | discord.TextChannel, tier_info): self.teams: List["Team"] = teams self.room_num = room_num - self.thread = thread + self.channel = channel self.view = None self.finished = False self.host_list: List["Player"] = [] @@ -561,7 +561,7 @@ async def button_callback(self, interaction: discord.Interaction, button): button.disabled = True await interaction.response.edit_message(view=self) mention = interaction.user.mention - await self.room.thread.send(f"{mention} has joined the room.") + await self.room.channel.send(f"{mention} has joined the room.") else: await interaction.response.send_message( "You do not meet room requirements", ephemeral=True) From 07c6941478c1082c09b1f47934b2ee7f47f9c236 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:51:05 -0500 Subject: [PATCH 194/292] Mogi.get_room_from_thread -> Mogi.get_room_from_channel_id --- cogs/SquadQueue.py | 4 ++-- mogi_objects.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 6d34767..1d09d50 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -426,12 +426,12 @@ async def sub(self, interaction: discord.Interaction): mogi = self.ongoing_event if mogi is not None: if mogi.is_room_thread(interaction.channel_id): - room = mogi.get_room_from_thread(interaction.channel_id) + room = mogi.get_room_from_channel_id(interaction.channel_id) bottom_room_num = len(mogi.rooms) is_room_thread = True for old_mogi in self.old_events: if old_mogi.is_room_thread(interaction.channel.id): - room = old_mogi.get_room_from_thread(interaction.channel.id) + room = old_mogi.get_room_from_channel_id(interaction.channel.id) bottom_room_num = len(old_mogi.rooms) is_room_thread = True break diff --git a/mogi_objects.py b/mogi_objects.py index d214983..18bdc52 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -225,7 +225,7 @@ def is_room_thread(self, channel_id: int): return True return False - def get_room_from_thread(self, channel_id: int): + def get_room_from_channel_id(self, channel_id: int): for room in self.rooms: if room is None or room.channel is None: continue @@ -408,7 +408,7 @@ async def make_teams(self, format_): timedelta(minutes=5) random.shuffle(self.players) - room = self.mogi.get_room_from_thread(self.thread.id) + room = self.mogi.get_room_from_channel_id(self.room_channel.id) msg = f"""**Poll Ended!** From 486bb908379803635532759d1b22bc1941c49fee Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:52:27 -0500 Subject: [PATCH 195/292] Mogi.is_room_thread -> Mogi.channel_id_in_rooms --- cogs/SquadQueue.py | 10 +++++----- mogi_objects.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1d09d50..f8a623b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -425,12 +425,12 @@ async def sub(self, interaction: discord.Interaction): bottom_room_num = 1 mogi = self.ongoing_event if mogi is not None: - if mogi.is_room_thread(interaction.channel_id): + if mogi.channel_id_in_rooms(interaction.channel_id): room = mogi.get_room_from_channel_id(interaction.channel_id) bottom_room_num = len(mogi.rooms) is_room_thread = True for old_mogi in self.old_events: - if old_mogi.is_room_thread(interaction.channel.id): + if old_mogi.channel_id_in_rooms(interaction.channel.id): room = old_mogi.get_room_from_channel_id(interaction.channel.id) bottom_room_num = len(old_mogi.rooms) is_room_thread = True @@ -544,10 +544,10 @@ async def on_message(self, message: discord.Message): return mogi = None if self.ongoing_event is not None: - mogi = self.ongoing_event if self.ongoing_event.is_room_thread( + mogi = self.ongoing_event if self.ongoing_event.channel_id_in_rooms( message.channel.id) else None if mogi is None: - mogi = discord.utils.find(lambda m: m.is_room_thread( + mogi = discord.utils.find(lambda m: m.channel_id_in_rooms( message.channel.id), self.old_events) if mogi is None: return @@ -576,7 +576,7 @@ async def scoreboard(self, interaction: discord.Interaction): await interaction.response.send_message(f"Cannot use this command here.", ephemeral=True) return - mogi = discord.utils.find(lambda mogi: mogi.is_room_thread( + mogi = discord.utils.find(lambda mogi: mogi.channel_id_in_rooms( interaction.channel_id), self.old_events) if not mogi: await interaction.response.send_message(f"The Mogi object cannot be found.", ephemeral=True) diff --git a/mogi_objects.py b/mogi_objects.py index 18bdc52..ef6556b 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -217,7 +217,7 @@ def confirmed_teams(self) -> List["Team"]: def players_on_confirmed_teams(self) -> List[Player]: return flatten([team.players for team in self.confirmed_teams()]) - def is_room_thread(self, channel_id: int): + def channel_id_in_rooms(self, channel_id: int): for room in self.rooms: if room is None or room.channel is None: continue From 913bd3c3e48af621fed7b393b1d6fa285c5c66e4 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:55:21 -0500 Subject: [PATCH 196/292] Refactor of get_tier --- cogs/SquadQueue.py | 13 ++++++++----- mogi_objects.py | 34 ++++++++++++++++------------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index f8a623b..0dc4150 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -12,7 +12,7 @@ import common from common import divide_chunks import mmr -from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView, get_tier, get_tier_mk8dx +from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView import asyncio from collections import defaultdict from typing import Dict, List, Tuple @@ -590,7 +590,7 @@ async def scoreboard(self, interaction: discord.Interaction): format = round(12/len(room.teams)) - msg = f"!submit {format} {get_tier_mk8dx(room.mmr_average - 500)}\n" + msg = f"!submit {format_} {room.tier}\n" for team in room.teams: for player in team.players: msg += f"{player.lounge_name} {player.score}\n" @@ -913,6 +913,9 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): async def check_room_threads(self, mogi: Mogi): num_created_rooms = len(mogi.rooms) for i in range(num_created_rooms, mogi.max_possible_rooms): + if not common.CONFIG["USE_THREADS"]: + mogi.rooms.append(Room(None, i + 1, None, self.TIER_INFO)) + continue display_time = mogi.display_time minute = display_time.minute if len(str(minute)) == 1: @@ -932,7 +935,7 @@ async def check_room_threads(self, mogi: Mogi): err_msg = f"\nAn error has occurred while creating a room channel:\n{e}" await mogi.mogi_channel.send(err_msg) return - mogi.rooms.append(Room(None, i+1, room_channel)) + mogi.rooms.append(Room(None, i+1, room_channel, self.TIER_INFO)) @staticmethod async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): @@ -1006,9 +1009,9 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic If you need staff's assistance, use the `/ping_staff` command in this channel.""" try: - view = VoteView(room_players, curr_room.thread, - mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME, self.TIER_INFO) await curr_room.channel.send(room_msg) + view = VoteView(room_players, curr_room.channel, + mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME) curr_room.view = view await curr_room.channel.send(view=view) except discord.DiscordException: diff --git a/mogi_objects.py b/mogi_objects.py index ef6556b..b72f4d0 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -253,6 +253,13 @@ def __init__(self, teams, room_num: int, channel: discord.Thread | discord.TextC self.host_list: List["Player"] = [] self.subs: List[int] = [] + @property + def tier(self) -> str: + if common.SERVER is common.Server.MK8DX: + return get_tier_mk8dx(round(self.avg_mmr) - 500) + if common.SERVER is common.Server.MKW: + return str(get_tier_mkw(self.avg_mmr, self.tier_info)) + @property def mmr_high(self) -> int: if self.teams is None: @@ -374,10 +381,10 @@ def __lt__(self, other: Player): class VoteView(View): - def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int, tier_info): + def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): super().__init__() self.players = players - self.thread: discord.Thread = thread + self.room_channel: discord.Thread | discord.TextChannel = thread self.mogi = mogi self.room = room self.header_text = "" @@ -421,13 +428,9 @@ async def make_teams(self, format_): msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" msg += f"Winner: {format_[1]}\n\n" - room_mmr = round(sum([p.mmr for p in self.players]) / 12) - room.mmr_average = room_mmr self.header_text = "" - if common.SERVER is common.Server.MKW: - self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - T{get_tier(room_mmr, self.tier_info)}** " - elif common.SERVER is common.Server.MK8DX: - self.header_text += f"**Room {room.room_num} MMR: {room_mmr} - Tier {get_tier_mk8dx(room_mmr - 500)}** " + tier_text = "Tier " if common.SERVER is common.Server.MK8DX else "T" + self.header_text += f"**Room {room.room_num} MMR: {room.avg_mmr} - {tier_text}{room.tier}** " msg += self.header_text + "\n" teams = [] @@ -466,15 +469,10 @@ async def make_teams(self, format_): room.teams = teams self.found_winner = True - await self.thread.send(msg) - if common.SERVER is common.Server.MKW: - new_thread_name = self.thread.name + \ - f" - T{get_tier(room_mmr, self.tier_info)}" - await self.thread.edit(name=new_thread_name) - elif common.SERVER is common.Server.MK8DX: - new_thread_name = self.thread.name + \ - f" - Tier {get_tier_mk8dx(room_mmr - 500)}" - await self.thread.edit(name=new_thread_name) + await self.room_channel.send(msg) + if common.CONFIG["USE_THREADS"]: + new_thread_name = self.room_channel.name + f" - {tier_text}{room.tier}" + await self.room_channel.edit(name=new_thread_name) async def find_winner(self): if not self.found_winner: @@ -567,7 +565,7 @@ async def button_callback(self, interaction: discord.Interaction, button): "You do not meet room requirements", ephemeral=True) -def get_tier(mmr: int, tier_info): +def get_tier_mkw(mmr: int, tier_info): for tier in tier_info: if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): From 70b300c7467eb88a549df80d47f148d519221a75 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:55:44 -0500 Subject: [PATCH 197/292] Bug fix: coroutine not awaited --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 0dc4150..56e7b44 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -783,7 +783,7 @@ async def reset_bot(self, interaction: discord.Interaction): async def schedule_sq_times(self, ctx, timestamps: commands.Greedy[int]): """Saves a list of sq times to skip over. Input a list of unix utc timestamps. Staff use only.""" if not self.is_staff(ctx.author) and not await self.bot.is_owner(ctx.author): - self.queue_or_send( + await self.queue_or_send( ctx, "You do not have permission to use this command.") return From 12b49e271a2f338e70f51e867a0ed41880412e05 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:56:14 -0500 Subject: [PATCH 198/292] Refactor: fix global name shadowing --- cogs/SquadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 56e7b44..7ec9112 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -588,13 +588,13 @@ async def scoreboard(self, interaction: discord.Interaction): await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) return - format = round(12/len(room.teams)) + format_ = round(12/len(room.teams)) msg = f"!submit {format_} {room.tier}\n" for team in room.teams: for player in team.players: msg += f"{player.lounge_name} {player.score}\n" - if format != 1: + if format_ != 1: msg += "\n" await interaction.response.send_message(msg) From 91c2f56f74d1fcd21d65e4783ea704ca07178ac8 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:56:37 -0500 Subject: [PATCH 199/292] Add imports --- cogs/SquadQueue.py | 2 +- mogi_objects.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 7ec9112..b5e999b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -4,12 +4,12 @@ import discord from discord.ext import commands, tasks from discord import app_commands -from dateutil.parser import parse from datetime import datetime, timezone, timedelta import time import json import common +import mogi_objects from common import divide_chunks import mmr from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView diff --git a/mogi_objects.py b/mogi_objects.py index b72f4d0..04d675a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -1,5 +1,6 @@ from __future__ import annotations +import traceback import common from common import flatten import random From d2df780764f403e9053da6aa52190f31847ca886 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Sat, 20 Jul 2024 07:59:36 -0500 Subject: [PATCH 200/292] Full room and role maintainer functionality The switch of USE_THREADS is fully used. Text channel recycling functionality will happen if USE_THREADS is false in the config. Members will receive a role for their room when the rooms are made. The role maintainer removes roles that members of the server should no longer have because their old mogi lifetime has ended. --- cogs/SquadQueue.py | 51 +++++++++++++++++++++++ mogi_objects.py | 101 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b5e999b..51a130c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -56,6 +56,8 @@ class SquadQueue(commands.Cog): def __init__(self, bot): self.bot: commands.Bot = bot + self.bot_startup_time = datetime.now(timezone.utc) + self.is_production = bot.config["is_production"] self.next_event: Mogi = None @@ -186,6 +188,8 @@ async def on_ready(self): self.refresh_ratings.start() self.refresh_helper_roles.start() self.check_room_threads_task.start() + if not common.CONFIG["USE_THREADS"]: + self.maintain_roles.start() async def lockdown(self, channel: discord.TextChannel): # everyone_perms = channel.permissions_for(channel.guild.default_role) @@ -1009,6 +1013,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic If you need staff's assistance, use the `/ping_staff` command in this channel.""" try: + await curr_room.prepare_room_channel(self.GUILD, all_events=([self.ongoing_event] + self.old_events)) await curr_room.channel.send(room_msg) view = VoteView(room_players, curr_room.channel, mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME) @@ -1019,6 +1024,11 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic err_msg += player_mentions + extra_member_mentions msg += err_msg print(traceback.format_exc()) + except mogi_objects.RoleAddFailure: + pass + except mogi_objects.PrepFailure as e: + await mogi.mogi_channel.send(f"ERROR: **{e}**") + try: await mogi.mogi_channel.send(msg) @@ -1218,6 +1228,47 @@ async def delete_old_mogis(self): except Exception as e: print(traceback.format_exc()) + @tasks.loop(minutes=1) + async def maintain_roles(self): + """Removes roles from people who are no longer supposed to see tier channels.""" + try: + should_start_role_removal = (self.bot_startup_time + timedelta(minutes=self.MOGI_LIFETIME)) <= datetime.now(timezone.utc) + if not self.is_production: + should_start_role_removal = True + print("Removing roles.") + if should_start_role_removal: + members = [member async for member in self.GUILD.fetch_members(limit=None)] + tier_roles = [] + tier_role_data = common.CONFIG["TIER_CHANNELS"] + for tier, tier_data in tier_role_data.items(): + tier_roles.append(self.GUILD.get_role(tier_data["tier_role_id"])) + + all_mogis = [] + if self.ongoing_event is not None: + all_mogis.append(self.ongoing_event) + all_mogis.extend([ev for ev in self.old_events if ev is not None]) + all_rooms = [] + for mogi in all_mogis: + for room in mogi.rooms: + if room is not None and room.room_role is not None: + all_rooms.append(room) + for member in members: + should_have_roles = set() + for room in all_rooms: + if room.check_player(member) is not None: + should_have_roles.add(room.room_role) + should_not_have_roles = set(tier_roles) - should_have_roles + member_roles = set(member.roles) + roles_to_remove = should_not_have_roles.intersection(member_roles) + if len(roles_to_remove) > 0: + try: + await member.remove_roles(*roles_to_remove) + except: + print(traceback.format_exc()) + + except Exception as e: + print(traceback.format_exc()) + @tasks.loop(minutes=3) async def check_room_threads_task(self): try: diff --git a/mogi_objects.py b/mogi_objects.py index 04d675a..fd0cd92 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -11,6 +11,30 @@ import host_fcs +class PrepFailure(Exception): + pass + + +class WrongChannelType(TypeError, PrepFailure): + pass + + +class RoleFailure(PrepFailure): + pass + + +class RoleAddFailure(RoleFailure): + pass + + +class RoleNotFound(RoleFailure): + pass + + +class NoFreeChannels(PrepFailure): + pass + + def average(list_: List[int | float]) -> float: return sum(list_) / len(list_) @@ -253,6 +277,8 @@ def __init__(self, teams, room_num: int, channel: discord.Thread | discord.TextC self.finished = False self.host_list: List["Player"] = [] self.subs: List[int] = [] + self.tier_info = tier_info + self.room_role = None @property def tier(self) -> str: @@ -308,6 +334,76 @@ def get_host_str(self) -> str: result += f"\n**Host ({self.host_list[0].member.display_name}) Friend Code: {self.host_list[0].host_fc}**" return result + def check_player(self, member): + if self.teams is None: + return None + for team in self.teams: + if team.has_player(member): + return team + return None + + async def assign_member_room_role(self, member: discord.Member): + if self.room_role is None: + raise RoleNotFound(f"Could not find role for tier {self.tier}") + try: + await member.add_roles(self.room_role) + except: + raise RoleAddFailure(f"Failed to add role {self.room_role.name} to {member}\n") + + async def assign_roles(self): + role_add_fail_text = "" + for player in self.players: + try: + await self.assign_member_room_role(player.member) + except RoleAddFailure: + role_add_fail_text += f"Failed to add role {self.room_role.name} to {player.member}\n" + print(traceback.format_exc()) + + if role_add_fail_text != "": + await self.channel.send(role_add_fail_text) + raise RoleAddFailure(role_add_fail_text) + + async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi | None]): + if common.CONFIG["USE_THREADS"]: + return + """ + "1": { + "tier_role_id": 1114695238716497980, + "channel_ids": [ + 1264120088651628596, + 1264120865881325629 + ] + } + """ + tier_data = common.CONFIG["TIER_CHANNELS"][self.tier] + + all_tier_channel_ids = set(tier_data["channel_ids"]) + in_use_tier_channel_ids = set() + for event in all_events: + if event is None: + continue + for tier_channel_id in all_tier_channel_ids: + if event.channel_id_in_rooms(tier_channel_id): + in_use_tier_channel_ids.add(tier_channel_id) + + free_channel_ids = all_tier_channel_ids - in_use_tier_channel_ids + if len(free_channel_ids) == 0: + raise NoFreeChannels(f"No free channels for tier {self.tier}") + + channel_id = free_channel_ids.pop() + found_channel = guild.get_channel(channel_id) + if not isinstance(found_channel, discord.TextChannel): + raise WrongChannelType(f"For tier {self.tier}, channel id {channel_id} is of type {type(found_channel)}, expected discord.TextChannel") + + self.channel = found_channel + + tier_role_id = tier_data["tier_role_id"] + self.room_role = guild.get_role(tier_role_id) + if self.room_role is None: + raise RoleNotFound(f"Could not find role for role id {tier_role_id} for tier {self.tier}") + + await self.assign_roles() + class Team: def __init__(self, players: List["Player"]): @@ -392,7 +488,6 @@ def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): self.teams_text = "" self.found_winner = False self.penalty_time = penalty_time - self.tier_info = tier_info self.votes = {"FFA": [], "2v2": [], "3v3": [], @@ -559,6 +654,10 @@ async def button_callback(self, interaction: discord.Interaction, button): self.room.subs.append(interaction.user.id) button.disabled = True await interaction.response.edit_message(view=self) + try: + await self.room.assign_member_room_role(interaction.user) + except RoleAddFailure as e: + self.room.channel.send(str(e)) mention = interaction.user.mention await self.room.channel.send(f"{mention} has joined the room.") else: From 4eb5ac8df3aa9ad9ccb7acb4dbc3fbdf3ba1b455 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:11:54 -0500 Subject: [PATCH 201/292] SquadQueue.maintain_roles: Refactor + comments --- cogs/SquadQueue.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 51a130c..8a3f922 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1228,30 +1228,36 @@ async def delete_old_mogis(self): except Exception as e: print(traceback.format_exc()) - @tasks.loop(minutes=1) + @tasks.loop(minutes=10) async def maintain_roles(self): """Removes roles from people who are no longer supposed to see tier channels.""" try: should_start_role_removal = (self.bot_startup_time + timedelta(minutes=self.MOGI_LIFETIME)) <= datetime.now(timezone.utc) if not self.is_production: should_start_role_removal = True - print("Removing roles.") if should_start_role_removal: + # Fetch all members (API call) for server to ensure role information is up-to-date + # WARNING: if your server is large, you need a higher loop time on the task members = [member async for member in self.GUILD.fetch_members(limit=None)] + + # Gather all discord.Role objects tier_roles = [] tier_role_data = common.CONFIG["TIER_CHANNELS"] for tier, tier_data in tier_role_data.items(): tier_roles.append(self.GUILD.get_role(tier_data["tier_role_id"])) - all_mogis = [] + # Create a list of all current and existing old events + all_mogis = [ev for ev in self.old_events if ev is not None] if self.ongoing_event is not None: - all_mogis.append(self.ongoing_event) - all_mogis.extend([ev for ev in self.old_events if ev is not None]) + all_mogis.insert(0, self.ongoing_event) + + # Create a list of all Rooms who have a valid room role all_rooms = [] for mogi in all_mogis: - for room in mogi.rooms: - if room is not None and room.room_role is not None: - all_rooms.append(room) + all_rooms.extend([r for r in mogi.rooms if r is not None and r.room_role is not None]) + + # Go through each member in the server and, based on what roles they should have because of the + # events they are in, remove any roles they shouldn't have for member in members: should_have_roles = set() for room in all_rooms: From 58f3fddd3b30b8bf7283219afd84a0cb6a00dee4 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:12:22 -0500 Subject: [PATCH 202/292] Add Mogi.all_room_channel_ids() --- mogi_objects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index fd0cd92..46cfe4e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -242,6 +242,9 @@ def confirmed_teams(self) -> List["Team"]: def players_on_confirmed_teams(self) -> List[Player]: return flatten([team.players for team in self.confirmed_teams()]) + def all_room_channel_ids(self) -> Set[int]: + return {r.channel.id for r in self.rooms if r is not None and r.channel is not None} + def channel_id_in_rooms(self, channel_id: int): for room in self.rooms: if room is None or room.channel is None: From bcc032063423cca57a32ccdbdf6c0ac3f5654874 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:12:36 -0500 Subject: [PATCH 203/292] Refactor Mogi.channel_id_in_rooms() --- mogi_objects.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 46cfe4e..2ca558c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -246,12 +246,7 @@ def all_room_channel_ids(self) -> Set[int]: return {r.channel.id for r in self.rooms if r is not None and r.channel is not None} def channel_id_in_rooms(self, channel_id: int): - for room in self.rooms: - if room is None or room.channel is None: - continue - if room.channel.id == channel_id: - return True - return False + return channel_id in self.all_room_channel_ids() def get_room_from_channel_id(self, channel_id: int): for room in self.rooms: From 50718a75067fd4660425ea0d1cd97de304aa80e2 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:13:03 -0500 Subject: [PATCH 204/292] Buf fix with Room.avg_mmr --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 2ca558c..1772b91 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -298,10 +298,10 @@ def mmr_low(self) -> int: return min(self.players).mmr @property - def avg_mmr(self) -> float: + def avg_mmr(self) -> int: if self.teams is None: return 0 - return average([p.mmr for p in self.players]) + return int(average([p.mmr for p in self.players])) @property def players(self) -> List[Player]: From 4979ab12f4254e17dd7081636a1c3c22150cbb67 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:13:24 -0500 Subject: [PATCH 205/292] Remove testing text --- mogi_objects.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 1772b91..8a5da45 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -364,15 +364,6 @@ async def assign_roles(self): async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi | None]): if common.CONFIG["USE_THREADS"]: return - """ - "1": { - "tier_role_id": 1114695238716497980, - "channel_ids": [ - 1264120088651628596, - 1264120865881325629 - ] - } - """ tier_data = common.CONFIG["TIER_CHANNELS"][self.tier] all_tier_channel_ids = set(tier_data["channel_ids"]) From 82b6ace605ac4d83ed8e708766fd9813f05985b4 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:14:02 -0500 Subject: [PATCH 206/292] Finalized implementation of recycled tier channels --- mogi_objects.py | 53 +++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 8a5da45..c4a8b1e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -2,12 +2,13 @@ import traceback import common +from contextlib import suppress from common import flatten import random import discord from datetime import datetime, timezone, timedelta from discord.ui import View -from typing import List, Tuple, Callable +from typing import List, Tuple, Callable, Set import host_fcs @@ -285,6 +286,21 @@ def tier(self) -> str: if common.SERVER is common.Server.MKW: return str(get_tier_mkw(self.avg_mmr, self.tier_info)) + @property + def tier_collection(self) -> str: + if common.SERVER is common.Server.MK8DX: + return self.tier + if common.SERVER is common.Server.MKW: + tier_number = self.tier + if not common.is_int(tier_number): + return "HT" + tier_number = int(tier_number) + if tier_number <= 2: + return "LT" + if tier_number >= 5: + return "HT" + return "MT" + @property def mmr_high(self) -> int: if self.teams is None: @@ -364,33 +380,32 @@ async def assign_roles(self): async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi | None]): if common.CONFIG["USE_THREADS"]: return - tier_data = common.CONFIG["TIER_CHANNELS"][self.tier] - - all_tier_channel_ids = set(tier_data["channel_ids"]) - in_use_tier_channel_ids = set() + + # Find the available tier channels for the tier (collection) + tier_collection = self.tier_collection + tier_data = common.CONFIG["TIER_CHANNELS"][tier_collection] + free_tier_channel_ids = list(tier_data["channel_ids"]) for event in all_events: if event is None: continue - for tier_channel_id in all_tier_channel_ids: - if event.channel_id_in_rooms(tier_channel_id): - in_use_tier_channel_ids.add(tier_channel_id) - - free_channel_ids = all_tier_channel_ids - in_use_tier_channel_ids - if len(free_channel_ids) == 0: - raise NoFreeChannels(f"No free channels for tier {self.tier}") - - channel_id = free_channel_ids.pop() - found_channel = guild.get_channel(channel_id) + for channel_id in event.all_room_channel_ids(): + with suppress(ValueError): + free_tier_channel_ids.remove(channel_id) + # If there are no available tier channels, raise an exception + if len(free_tier_channel_ids) == 0: + raise NoFreeChannels(f"No free channels for tier {tier_collection}") + + # Get the discord.GuildChannel of the first available channel id + found_channel = guild.get_channel(free_tier_channel_ids[0]) if not isinstance(found_channel, discord.TextChannel): - raise WrongChannelType(f"For tier {self.tier}, channel id {channel_id} is of type {type(found_channel)}, expected discord.TextChannel") - + raise WrongChannelType(f"For tier {tier_collection}, channel id {channel_id} is of type {type(found_channel)}, expected discord.TextChannel") self.channel = found_channel + # Assign the role for the tier collection to all players in the room so they can see the tier tier_role_id = tier_data["tier_role_id"] self.room_role = guild.get_role(tier_role_id) if self.room_role is None: - raise RoleNotFound(f"Could not find role for role id {tier_role_id} for tier {self.tier}") - + raise RoleNotFound(f"Could not find role for role id {tier_role_id} for tier {tier_collection}") await self.assign_roles() From b34c36d34b6d5bc6f6b07fec536c1673f0a791d3 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:14:17 -0500 Subject: [PATCH 207/292] RT config for recycled tier channels --- example_configs/prod/mkw_rt_prod_config.json | 35 +++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 70e975f..d99eeff 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -57,12 +57,39 @@ "ROOM_MMR_THRESHOLD": 8000, "USE_THREADS": false, "TIER_CHANNELS": { - "1": { - "tier_role_id": 1264149575070126102, + "HT": { + "tier_role_id": 1264625004637720656, "channel_ids": [ - 1264120088651628596, - 1264120865881325629 + 389456664232984577, + 830217477291114516, + 387361702829883392, + 805889218516877372, + 841730455422107669, + 1264728881253060708 + ] + }, + "MT": { + "tier_role_id": 1264625254752583803, + "channel_ids": [ + 862105563701968906, + 953805745994997761, + 722454335287590963, + 746216972869435402, + 1265451963236094044, + 1265452192371048548 + ] + }, + "LT": { + "tier_role_id": 1264625310079385731, + "channel_ids": [ + 389456944181674004, + 581228135651999744, + 746458027359469700, + 387361809587240960, + 389457003820482571, + 1264728728920129597 ] } + } } \ No newline at end of file From 0cca8972cb17ac30710f6ac7345e381081e8e4bf Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:15:08 -0500 Subject: [PATCH 208/292] Feature: Backup thread creation in no available tier channels --- cogs/SquadQueue.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8a3f922..841d207 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -999,6 +999,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic player_mentions = " ".join([p.mention for p in room_players]) extra_member_mentions = " ".join( [m.mention for m in extra_members if m is not None]) + room_msg = "" if common.SERVER is common.Server.MKW: room_msg += f"""{msg} @@ -1012,8 +1013,26 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic {player_mentions} {extra_member_mentions} If you need staff's assistance, use the `/ping_staff` command in this channel.""" + # The below try/except clause around Room.prepare_room_channel only runs if the config's USE_THREADS is set to false try: await curr_room.prepare_room_channel(self.GUILD, all_events=([self.ongoing_event] + self.old_events)) + # Non-fatal error, message already sent to room channel, continue with room creation + except mogi_objects.RoleAddFailure: + pass + # semi-critical failures, but the room can proceed + except mogi_objects.RoleNotFound as e: + await mogi.mogi_channel.send(f"**ERROR:** {e}") + # critical failure, but we can recover by creating a thread + except (mogi_objects.WrongChannelType, mogi_objects.NoFreeChannels) as e: + await mogi.mogi_channel.send(f"**ERROR:** {e}, attempting to create room thread instead...") + room_name = f"{mogi.display_time.month}/{mogi.display_time.day}, {mogi.display_time.hour}:{mogi.display_time.minute:02}:00 - Room {room_number}" + curr_room.channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, invitable=False) + # critical failure, the room cannot proceed + except mogi_objects.WrongChannelType as e: + await mogi.mogi_channel.send(f"**ERROR:** {e}") + continue + + try: await curr_room.channel.send(room_msg) view = VoteView(room_players, curr_room.channel, mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME) @@ -1024,11 +1043,6 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic err_msg += player_mentions + extra_member_mentions msg += err_msg print(traceback.format_exc()) - except mogi_objects.RoleAddFailure: - pass - except mogi_objects.PrepFailure as e: - await mogi.mogi_channel.send(f"ERROR: **{e}**") - try: await mogi.mogi_channel.send(msg) From 1ffdd4654c782e4e8ba9ecd23edadc49238dacfd Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:25:24 -0500 Subject: [PATCH 209/292] Create README.md --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a1e5c8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +This branch lets the bot recyle using existing text channels rather than creating threads for each room. From 42c4ce6a0a7a71ce6d7a9d5e428e73243c50de66 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:35:07 -0500 Subject: [PATCH 210/292] Remove testing statement --- cogs/SquadQueue.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 841d207..5764cf4 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1247,8 +1247,6 @@ async def maintain_roles(self): """Removes roles from people who are no longer supposed to see tier channels.""" try: should_start_role_removal = (self.bot_startup_time + timedelta(minutes=self.MOGI_LIFETIME)) <= datetime.now(timezone.utc) - if not self.is_production: - should_start_role_removal = True if should_start_role_removal: # Fetch all members (API call) for server to ensure role information is up-to-date # WARNING: if your server is large, you need a higher loop time on the task From bdce04df29ae3a11d23ebbb4599f25ca8c2cd5e1 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:49:33 -0500 Subject: [PATCH 211/292] Config changes for min and max mmr --- example_configs/prod/mk8dx_prod_config.json | 2 ++ example_configs/prod/mkw_ct_prod_config.json | 2 ++ example_configs/prod/mkw_rt_prod_config.json | 2 ++ 3 files changed, 6 insertions(+) diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index a4db25e..c9e1568 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -44,5 +44,7 @@ "SUB_RANGE_MMR_ALLOWANCE": 500, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 9999999, + "MATCHMAKING_BOTTOM_MMR": null, + "MATCHMAKING_TOP_MMR": null, "USE_THREADS": true } \ No newline at end of file diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index 5e40493..5e5038a 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -55,5 +55,7 @@ "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 9000, + "MATCHMAKING_BOTTOM_MMR": null, + "MATCHMAKING_TOP_MMR": null, "USE_THREADS": true } \ No newline at end of file diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index d99eeff..1653979 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -55,6 +55,8 @@ "SUB_RANGE_MMR_ALLOWANCE": 700, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, "ROOM_MMR_THRESHOLD": 8000, + "MATCHMAKING_BOTTOM_MMR": -2000, + "MATCHMAKING_TOP_MMR": 11000, "USE_THREADS": false, "TIER_CHANNELS": { "HT": { From e7cfe9815ec7ed6539fa5ebd280a0f519240f295 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:49:59 -0500 Subject: [PATCH 212/292] Player minimum maximum mmr helper properties --- mogi_objects.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mogi_objects.py b/mogi_objects.py index c4a8b1e..3f798fb 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -467,6 +467,22 @@ def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed self.host = host self.host_fc = None + @property + def considered_mmr(self): + minimum_mmr = common.CONFIG["MATCHMAKING_BOTTOM_MMR"] + maximum_mmr = common.CONFIG["MATCHMAKING_TOP_MMR"] + if minimum_mmr is None or maximum_mmr is None: + return self.mmr + if self.mmr < minimum_mmr: + return minimum_mmr + if self.mmr > maximum_mmr: + return maximum_mmr + return self.mmr + + @property + def is_matchmaking_mmr_adjusted(self): + return self.mmr != self.considered_mmr + @property def mention(self): """String that, when sent in a Discord message, will mention and ping the player.""" From bd1c4ad4b3002b4e370197812532c022a118c9a8 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:52:39 -0500 Subject: [PATCH 213/292] Refactor Player property name --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 3f798fb..a207cc7 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -468,7 +468,7 @@ def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed self.host_fc = None @property - def considered_mmr(self): + def adjusted_mmr(self): minimum_mmr = common.CONFIG["MATCHMAKING_BOTTOM_MMR"] maximum_mmr = common.CONFIG["MATCHMAKING_TOP_MMR"] if minimum_mmr is None or maximum_mmr is None: @@ -481,7 +481,7 @@ def considered_mmr(self): @property def is_matchmaking_mmr_adjusted(self): - return self.mmr != self.considered_mmr + return self.mmr != self.adjusted_mmr @property def mention(self): From 199c6009520ae4129e2d13e16273ef0948e736d6 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:53:12 -0500 Subject: [PATCH 214/292] Change list display --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5764cf4..ed10818 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -486,7 +486,8 @@ async def list_task(self): msg += "**Current Mogi List:**\n" if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr}" if player.is_matchmaking_mmr_adjusted else "" + msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" if len(on_time_players) == 0: # Text looks better this way From ed06ceeaf05e94365609d4a0eb1498cc93ae3629 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:55:19 -0500 Subject: [PATCH 215/292] Adjusted mmr text when player queues --- cogs/SquadQueue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ed10818..81e3c5a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -370,6 +370,8 @@ async def join_queue(self, interaction: discord.Interaction, host=False): mogi.teams.append(squad) host_str = " as a host " if host else " " msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + if player.is_matchmaking_mmr_adjusted: + msg += f"\nPlayer considered {player.adjusted_mmr} MMR for matchmaking purposes." event_status_launched = self.check_close_event_change() try: From 441f2f60d92dbdd5f3f914fd1b928120e46b1b23 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:57:42 -0500 Subject: [PATCH 216/292] Late player adjusted mmr text --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 81e3c5a..5c54186 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -496,7 +496,8 @@ async def list_task(self): msg += "\n" msg += "**Late Players:**\n" for i, player in enumerate(late_players, 1): - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR)\n" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr}" if player.is_matchmaking_mmr_adjusted else "" + msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" elif common.SERVER is common.Server.MK8DX: all_confirmed_players.sort(reverse=True) for i, player in enumerate(all_confirmed_players, 1): From 12e7afd9ba9e9ce18063e39ed528629dd8ea96b7 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 01:57:54 -0500 Subject: [PATCH 217/292] Threshold function to use adjusted mmr --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5c54186..23af071 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -39,7 +39,7 @@ def is_restricted(user: discord.User | discord.Member) -> bool: def basic_threshold_players_allowed(players: List[Player], threshold: int) -> bool: """Returns True if the highest player's rating minus the lowest player's rating is below the given threshold""" sorted_players = sorted(players, key=lambda p: p.mmr) - return (sorted_players[-1].mmr - sorted_players[0].mmr) <= threshold + return (sorted_players[-1].adjusted_mmr - sorted_players[0].adjusted_mmr) <= threshold def mkw_players_allowed(players: List[Player], threshold: int) -> bool: From 52ce9f955330e028229cfc3f7a544c88633735e6 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:15:32 -0500 Subject: [PATCH 218/292] Use adjusted_mmr in appropriate places --- mogi_objects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index a207cc7..0495dfe 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -94,11 +94,11 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N sorted_players = sorted(players) # In the beginning, the best found collection of players is the first 12 best_collection = sorted_players[0:num_players] - cur_min = best_collection[-1].mmr - best_collection[0].mmr + cur_min = best_collection[-1].adjusted_mmr - best_collection[0].adjusted_mmr # Find the collection of players with the least rating spread for lowest_player_index, highest_player in enumerate(sorted_players[num_players:], 1): lowest_player = sorted_players[lowest_player_index] - cur_range = highest_player.mmr - lowest_player.mmr + cur_range = highest_player.adjusted_mmr - lowest_player.adjusted_mmr if cur_range < cur_min: cur_min = cur_range best_collection = sorted_players[lowest_player_index: @@ -305,13 +305,13 @@ def tier_collection(self) -> str: def mmr_high(self) -> int: if self.teams is None: return None - return max(self.players).mmr + return max(self.players).adjusted_mmr @property def mmr_low(self) -> int: if self.teams is None: return None - return min(self.players).mmr + return min(self.players).adjusted_mmr @property def avg_mmr(self) -> int: @@ -663,6 +663,8 @@ async def button_callback(self, interaction: discord.Interaction, button): return try: user_mmr = self.get_rating_from_discord_id(interaction.user.id) + if isinstance(user_mmr, int): + user_mmr = Player(None, "", user_mmr).adjusted_mmr except: await interaction.response.send_message( "MMR lookup for player has failed, please try again.", ephemeral=True) From 3fa252ab76d0a6cf64c6c3cf71c8f913a24642d8 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:16:31 -0500 Subject: [PATCH 219/292] Display spacing fix with adjusted mmr --- cogs/SquadQueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 23af071..ee265a1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -488,7 +488,7 @@ async def list_task(self): msg += "**Current Mogi List:**\n" if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): - adjusted_mmr_text = f"MMR -> {player.adjusted_mmr}" if player.is_matchmaking_mmr_adjusted else "" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" @@ -496,7 +496,7 @@ async def list_task(self): msg += "\n" msg += "**Late Players:**\n" for i, player in enumerate(late_players, 1): - adjusted_mmr_text = f"MMR -> {player.adjusted_mmr}" if player.is_matchmaking_mmr_adjusted else "" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" elif common.SERVER is common.Server.MK8DX: all_confirmed_players.sort(reverse=True) From d26c67ea542fe4289672cba391c176c48a3db688 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:16:46 -0500 Subject: [PATCH 220/292] Remove unused function --- mogi_objects.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 0495dfe..dcd64e7 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -105,35 +105,6 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N lowest_player_index + num_players] return best_collection - def _OLD_all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: - if self.max_possible_rooms == 0: - return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS - all_confirmed_players = self.players_on_confirmed_teams() - first_late_player_index = (self.num_players // self.players_per_room) * self.players_per_room - on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) - late_players = all_confirmed_players[first_late_player_index:] - player_rooms: List[List[Player]] = list(common.divide_chunks(on_time_players, self.players_per_room)) - - any_invalid = False - for pr_index, player_room in enumerate(player_rooms, 0): - for lp_index in range(len(late_players)+1): - current_late_check_players = late_players[0:lp_index] - best_collection = Mogi._minimize_range(player_room + current_late_check_players, self.players_per_room) - if valid_players_check(best_collection): - best_collection.sort(reverse=True) - # Get all the players who were swapped out of the room and put them at the front of the late player list - swapped_out_players = [p for p in player_room if p not in best_collection] - swapped_in_players = [p for p in best_collection if p not in player_room] - late_players = swapped_out_players + list(filter(lambda p: p not in swapped_in_players, late_players)) - player_rooms[pr_index] = best_collection - break - else: - # After checking all late players, no room player list with a valid range could be found for this room - any_invalid = True - - return player_rooms, (Mogi.ALGORITHM_STATUS_SUCCESS_SOME_INVALID if any_invalid else Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) - - def _all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS From 28d2dea47208fa22a109d92d1376944328b3abef Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:20:14 -0500 Subject: [PATCH 221/292] Add adjusted MMR to display in queue join made rooms --- cogs/SquadQueue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ee265a1..04b1c5a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -993,7 +993,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic msg = f"`Room {room_number} - Player List`\n" for player_num, player in enumerate(room_players, 1): added_str = ": **Added from late players**" if player in late_player_list else "" - msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} MMR){added_str}\n""" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" + msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR){added_str}\n""" if not self.allowed_players_check(room_players): msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: @@ -1062,7 +1063,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic msg = "`Late players:`\n" for i, player in enumerate(not_in_proposed_list, 1): removed_str = ": **Removed from player list**" if player in regular_player_list else "" - msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} MMR) {removed_str}\n" + adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" + msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} {adjusted_mmr_text}MMR) {removed_str}\n" try: await mogi.mogi_channel.send(msg) except discord.DiscordException: From 0ec187968cd8de0ee431537c2518e120682299fc Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:24:33 -0500 Subject: [PATCH 222/292] Bug fix --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 04b1c5a..9e539d0 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -370,7 +370,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): mogi.teams.append(squad) host_str = " as a host " if host else " " msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" - if player.is_matchmaking_mmr_adjusted: + if players[0].is_matchmaking_mmr_adjusted: msg += f"\nPlayer considered {player.adjusted_mmr} MMR for matchmaking purposes." event_status_launched = self.check_close_event_change() From cf4bceebd115a7ee5fba9eafbd1c3536341a96ef Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:25:34 -0500 Subject: [PATCH 223/292] Another bug fix --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9e539d0..1729463 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -371,7 +371,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): host_str = " as a host " if host else " " msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" if players[0].is_matchmaking_mmr_adjusted: - msg += f"\nPlayer considered {player.adjusted_mmr} MMR for matchmaking purposes." + msg += f"\nPlayer considered {players[0].adjusted_mmr} MMR for matchmaking purposes." event_status_launched = self.check_close_event_change() try: From ff313b7048d81767a27743462a8349664f65b64c Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:34:34 -0500 Subject: [PATCH 224/292] Do not display late player and adjusted mmr in rooms --- cogs/SquadQueue.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 1729463..272fe94 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -990,13 +990,15 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic proposed_list = mogi.generate_proposed_list(self.allowed_players_check) await mogi.populate_host_fcs() for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): - msg = f"`Room {room_number} - Player List`\n" + mogi_channel_msg = f"`Room {room_number} - Player List`\n" + room_msg = mogi_channel_msg for player_num, player in enumerate(room_players, 1): added_str = ": **Added from late players**" if player in late_player_list else "" adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - msg += f"""`{player_num}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR){added_str}\n""" + mogi_channel_msg += f"`{player_num}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR){added_str}\n" + room_msg += f"`{player_num}.` {player.lounge_name} ({player.mmr} MMR)\n" if not self.allowed_players_check(room_players): - msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." + mogi_channel_msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: curr_room = mogi.rooms[room_number - 1] curr_room.teams = [Team([p]) for p in room_players] @@ -1005,15 +1007,14 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic extra_member_mentions = " ".join( [m.mention for m in extra_members if m is not None]) - room_msg = "" if common.SERVER is common.Server.MKW: - room_msg += f"""{msg} + room_msg += f""" Vote for format FFA, 2v2, 3v3, 4v4, or 6v6. {player_mentions} {extra_member_mentions} If you need staff's assistance, use the `/ping_staff` command in this channel.""" elif common.SERVER is common.Server.MK8DX: - room_msg += f"""{msg} + room_msg += f""" Vote for format FFA, 2v2, 3v3, 4v4. {player_mentions} {extra_member_mentions} @@ -1046,11 +1047,11 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic except discord.DiscordException: err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" err_msg += player_mentions + extra_member_mentions - msg += err_msg + mogi_channel_msg += err_msg print(traceback.format_exc()) try: - await mogi.mogi_channel.send(msg) + await mogi.mogi_channel.send(mogi_channel_msg) except discord.DiscordException: print( f"Mogi Channel message for room {room_number} has failed to send.", flush=True) @@ -1060,13 +1061,13 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic not_in_proposed_list = [ p for p in all_confirmed_players if p not in proposed_list] if len(not_in_proposed_list) > 0: - msg = "`Late players:`\n" + mogi_channel_msg = "`Late players:`\n" for i, player in enumerate(not_in_proposed_list, 1): removed_str = ": **Removed from player list**" if player in regular_player_list else "" adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} {adjusted_mmr_text}MMR) {removed_str}\n" + mogi_channel_msg += f"`{i}.` {player.lounge_name} ({int(player.mmr)} {adjusted_mmr_text}MMR) {removed_str}\n" try: - await mogi.mogi_channel.send(msg) + await mogi.mogi_channel.send(mogi_channel_msg) except discord.DiscordException: print("Late Player message has failed to send.", flush=True) print(traceback.format_exc()) From a2e5b0bf2c885ba5c52f40fdee6ad1981a77ee62 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:13:35 -0500 Subject: [PATCH 225/292] Optimization to speed up room creation --- cogs/SquadQueue.py | 2 ++ mogi_objects.py | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 272fe94..4d9066d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1074,6 +1074,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic # We could have used asyncio.call_later(120, handle_voting_and_history) # and removed asyncio.sleep(120) in handle_voting_and_history + if not common.CONFIG["USE_THREADS"]: + asyncio.create_task(self.ongoing_event.assign_roles(guild=self.GUILD)) asyncio.create_task(SquadQueue.handle_voting_and_history( self.ongoing_event, self.HISTORY_CHANNEL)) self.old_events.append(self.ongoing_event) diff --git a/mogi_objects.py b/mogi_objects.py index dcd64e7..6b27b91 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -237,6 +237,15 @@ async def populate_host_fcs(self): if player is not None: player.host_fc = host_fc + async def assign_roles(self, guild: discord.Guild): + for room in self.rooms: + try: + to_skip = None if room.room_role is None else {room.room_role.id} + await room.assign_roles(guild=guild, role_skip=to_skip) + except RoleAddFailure: + pass + + class Room: def __init__(self, teams, room_num: int, channel: discord.Thread | discord.TextChannel, tier_info): @@ -335,9 +344,17 @@ async def assign_member_room_role(self, member: discord.Member): except: raise RoleAddFailure(f"Failed to add role {self.room_role.name} to {member}\n") - async def assign_roles(self): + async def assign_roles(self, guild: discord.Guild, role_skip=None): role_add_fail_text = "" + role_skip = set() if role_skip is None else set(role_skip) for player in self.players: + updated_member = guild.get_member(player.member.id) + if updated_member is not None: + member_role_ids = {r.id for r in updated_member.roles} + if len(role_skip.intersection(member_role_ids)) > 0: + print(f"Skipping {player.lounge_name} because has {role_skip.intersection(member_role_ids).pop()}") + continue + print(f"Assigning role to {player.lounge_name}") try: await self.assign_member_room_role(player.member) except RoleAddFailure: @@ -377,7 +394,7 @@ async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi self.room_role = guild.get_role(tier_role_id) if self.room_role is None: raise RoleNotFound(f"Could not find role for role id {tier_role_id} for tier {tier_collection}") - await self.assign_roles() + await self.assign_roles(guild=guild, role_skip=tier_data["role_ids_can_see_already"]+[tier_role_id]) class Team: From 51309353e20894016d2329b1f5f57e32462dc4a2 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:44:42 -0500 Subject: [PATCH 226/292] Refactor code out to SquadQueue.find_room_by_interaction --- cogs/SquadQueue.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 4d9066d..8159170 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -407,6 +407,20 @@ async def drop(self, interaction: discord.Interaction): if event_status_launched: await self.launch_mogi() + def find_room_by_interaction(self, interaction: discord.Interaction) -> Tuple[Room | None, int]: + """Given an interaction, returns the Room and room number associated with the interaction. + Searches current and old, undeleted events. If the interaction is not associated with a Room, + (None, 1) is returned.""" + all_mogis = list(self.old_events) + if self.ongoing_event is not None: + all_mogis.insert(0, self.ongoing_event) + for mogi in all_mogis: + if mogi.channel_id_in_rooms(interaction.channel.id): + room = mogi.get_room_from_channel_id(interaction.channel.id) + bottom_room_num = len(mogi.rooms) + return room, bottom_room_num + return None, 1 + @app_commands.command(name="sub") @app_commands.guild_only() @@ -426,22 +440,8 @@ async def sub(self, interaction: discord.Interaction): ephemeral=True) return - is_room_thread = False - room = None - bottom_room_num = 1 - mogi = self.ongoing_event - if mogi is not None: - if mogi.channel_id_in_rooms(interaction.channel_id): - room = mogi.get_room_from_channel_id(interaction.channel_id) - bottom_room_num = len(mogi.rooms) - is_room_thread = True - for old_mogi in self.old_events: - if old_mogi.channel_id_in_rooms(interaction.channel.id): - room = old_mogi.get_room_from_channel_id(interaction.channel.id) - bottom_room_num = len(old_mogi.rooms) - is_room_thread = True - break - if not is_room_thread: + room, bottom_room_num = self.find_room_by_interaction(interaction) + if room is None: await interaction.response.send_message( f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, the Mogi Object has been deleted.", ephemeral=True) From 767756b8f3ba0e6977826277f6bc62fdea5c2079 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:49:37 -0500 Subject: [PATCH 227/292] Added /host command --- cogs/SquadQueue.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8159170..daef8fe 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -407,6 +407,21 @@ async def drop(self, interaction: discord.Interaction): if event_status_launched: await self.launch_mogi() + @app_commands.command(name="host") + @app_commands.guild_only() + async def host(self, interaction: discord.Interaction): + """Display who is the host""" + room, _ = self.find_room_by_interaction(interaction) + if room is None: + await interaction.response.send_message( + f"More than {self.MOGI_LIFETIME} minutes have passed since mogi start, so this mogi has ended.", + ephemeral=True) + return + host_str = room.get_host_str() + if host_str == "": + host_str = "No one in this room queued as host! Ask someone who the host is." + await interaction.response.send_message(host_str) + def find_room_by_interaction(self, interaction: discord.Interaction) -> Tuple[Room | None, int]: """Given an interaction, returns the Room and room number associated with the interaction. Searches current and old, undeleted events. If the interaction is not associated with a Room, From 6f24e786c4e4aa72e212271d3b2f5f53fff7326d Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:49:47 -0500 Subject: [PATCH 228/292] Removed print statements --- mogi_objects.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 6b27b91..4d374b9 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -352,9 +352,7 @@ async def assign_roles(self, guild: discord.Guild, role_skip=None): if updated_member is not None: member_role_ids = {r.id for r in updated_member.roles} if len(role_skip.intersection(member_role_ids)) > 0: - print(f"Skipping {player.lounge_name} because has {role_skip.intersection(member_role_ids).pop()}") continue - print(f"Assigning role to {player.lounge_name}") try: await self.assign_member_room_role(player.member) except RoleAddFailure: From 006d27d2c4531fc67aa108974f13eb3b679806b2 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 04:59:44 -0500 Subject: [PATCH 229/292] Add race arg to /sub command --- cogs/SquadQueue.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index daef8fe..175218a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -440,7 +440,7 @@ def find_room_by_interaction(self, interaction: discord.Interaction) -> Tuple[Ro @app_commands.command(name="sub") @app_commands.guild_only() # @commands.cooldown(rate=1, per=120, type=commands.BucketType.user) - async def sub(self, interaction: discord.Interaction): + async def sub(self, interaction: discord.Interaction, races_left: int): """Sends out a request for a sub in the sub channel. Only works in thread channels for SQ rooms.""" current_time = time.time() lastCommandTime = cooldowns.get(interaction.user.id) @@ -464,13 +464,13 @@ async def sub(self, interaction: discord.Interaction): frequently_tagged_role_id = self.bot.config["frequently_tagged_role_id"] msg = f"<@&{frequently_tagged_role_id}> - " if bottom_room_num == 1: - msg += f"Room 1 is looking for a sub with any mmr\n" + msg += f"Room 1 is looking for a sub for {races_left} more races with any mmr\n" elif room.room_num == 1: - msg += f"Room 1 is looking for a sub with mmr >{room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}\n" + msg += f"Room 1 is looking for a sub for {races_left} more races with mmr >{room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}\n" elif room.room_num == bottom_room_num: - msg += f"Room {room.room_num} is looking for a sub with mmr <{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" + msg += f"Room {room.room_num} is looking for a sub for {races_left} more races with mmr <{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" else: - msg += f"Room {room.room_num} is looking for a sub with range {room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}-{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" + msg += f"Room {room.room_num} is looking for a sub for {races_left} more races with range {room.mmr_low - self.SUB_RANGE_MMR_ALLOWANCE}-{room.mmr_high + self.SUB_RANGE_MMR_ALLOWANCE}\n" message_delete_date = datetime.now( timezone.utc) + timedelta(seconds=self.SUB_MESSAGE_LIFETIME_SECONDS) msg += f"Message will auto-delete in {discord.utils.format_dt(message_delete_date, style='R')}" From 6e6cd977ce9d05cdedd1eeaf399d3aae4cf0adf3 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 05:12:50 -0500 Subject: [PATCH 230/292] Correct pen times for MKW --- mogi_objects.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 4d374b9..d9bab3a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -564,9 +564,18 @@ async def make_teams(self, format_): room_open_time = self.mogi.making_rooms_run_time potential_host_str = self.room.get_host_str() if potential_host_str == "": - msg += f"\nDecide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + if common.SERVER.MK8DX: + msg += f"\nDecide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + elif common.SERVER.MKW: + msg += f"\nDecide a host amongst yourselves; penalty is {self.penalty_time.minute} minutes after the room opens. Good luck!" else: - msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + if common.SERVER.MK8DX: + msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + else: + cur_time = datetime.now(timezone.utc) + room_open_time = cur_time + timedelta(minutes=2) + pen_time = cur_time + timedelta(minutes=self.penalty_time) + msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" room.teams = teams From 1d04ca143d361e08572d591ade10ae712df43304 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 05:27:54 -0500 Subject: [PATCH 231/292] Move host message text to first message --- cogs/SquadQueue.py | 16 ++++++++-------- mogi_objects.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 175218a..5028b2e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1022,18 +1022,18 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic extra_member_mentions = " ".join( [m.mention for m in extra_members if m is not None]) - if common.SERVER is common.Server.MKW: - room_msg += f""" -Vote for format FFA, 2v2, 3v3, 4v4, or 6v6. + potential_host_str = curr_room.get_host_str() + if potential_host_str == "": + potential_host_str += f"No one in the room queued as host. Decide a host amongst yourselves." + vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" + room_msg += f""" +{potential_host_str} + +Vote for format {vote_formats}. {player_mentions} {extra_member_mentions} If you need staff's assistance, use the `/ping_staff` command in this channel.""" - elif common.SERVER is common.Server.MK8DX: - room_msg += f""" -Vote for format FFA, 2v2, 3v3, 4v4. -{player_mentions} {extra_member_mentions} -If you need staff's assistance, use the `/ping_staff` command in this channel.""" # The below try/except clause around Room.prepare_room_channel only runs if the config's USE_THREADS is set to false try: await curr_room.prepare_room_channel(self.GUILD, all_events=([self.ongoing_event] + self.old_events)) diff --git a/mogi_objects.py b/mogi_objects.py index d9bab3a..6159576 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -565,17 +565,17 @@ async def make_teams(self, format_): potential_host_str = self.room.get_host_str() if potential_host_str == "": if common.SERVER.MK8DX: - msg += f"\nDecide a host amongst yourselves; room open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" elif common.SERVER.MKW: - msg += f"\nDecide a host amongst yourselves; penalty is {self.penalty_time.minute} minutes after the room opens. Good luck!" + msg += f"\nPenalty is {self.penalty_time.minute} minutes after the room opens. Good luck!" else: if common.SERVER.MK8DX: - msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" + msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: cur_time = datetime.now(timezone.utc) - room_open_time = cur_time + timedelta(minutes=2) + room_open_time = cur_time + timedelta(minutes=1) pen_time = cur_time + timedelta(minutes=self.penalty_time) - msg += f"{potential_host_str}\n\nRoom open at :{room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" + msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" room.teams = teams From 494e888cc1c9fe2fd2bf118067de7d577ee38891 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 05:33:07 -0500 Subject: [PATCH 232/292] Update mogi_objects.py --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 6159576..3c19f1e 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -573,9 +573,9 @@ async def make_teams(self, format_): msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: cur_time = datetime.now(timezone.utc) - room_open_time = cur_time + timedelta(minutes=1) + mkw_room_open_time = cur_time + timedelta(minutes=1) pen_time = cur_time + timedelta(minutes=self.penalty_time) - msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" + msg += f"\nRoom open at :{mkw_room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" room.teams = teams From 2dc8c287ecb33a86563bd09e5cba6cc9851fc451 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 05:56:59 -0500 Subject: [PATCH 233/292] Bug fix --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 3c19f1e..73a42fe 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -564,12 +564,12 @@ async def make_teams(self, format_): room_open_time = self.mogi.making_rooms_run_time potential_host_str = self.room.get_host_str() if potential_host_str == "": - if common.SERVER.MK8DX: + if common.SERVER is common.Server.MK8DX: msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" elif common.SERVER.MKW: msg += f"\nPenalty is {self.penalty_time.minute} minutes after the room opens. Good luck!" else: - if common.SERVER.MK8DX: + if common.SERVER is common.Server.MK8DX: msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: cur_time = datetime.now(timezone.utc) From 2e30635d9e3c8059dc9b30da88ced008b6dcc353 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:25:49 -0500 Subject: [PATCH 234/292] Another bug fix --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 73a42fe..74af3bf 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -567,7 +567,7 @@ async def make_teams(self, format_): if common.SERVER is common.Server.MK8DX: msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" elif common.SERVER.MKW: - msg += f"\nPenalty is {self.penalty_time.minute} minutes after the room opens. Good luck!" + msg += f"\nPenalty is {self.penalty_time} minutes after the room opens. Good luck!" else: if common.SERVER is common.Server.MK8DX: msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" From f4b0db250c760c6dc59d71d54a73adb552c56429 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 06:29:22 -0500 Subject: [PATCH 235/292] Auto extend feature --- cogs/SquadQueue.py | 16 +++++++++++----- mogi_objects.py | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5028b2e..473fc82 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1116,9 +1116,17 @@ def check_close_event_change(self) -> Tuple[bool, bool]: mogi.gathering = False self.cur_extension_message = None return True - elif mogi.start_time <= cur_time and mogi.gathering: + num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) + num_needed_teams = (12 // mogi.max_player_per_team) - num_leftover_teams + if common.SERVER is common.Server.MKW and (force_start_time - timedelta(minutes=1)) <= cur_time and num_leftover_teams != 0: + if not mogi.has_checked_auto_extend: + mogi.has_checked_auto_extend = True + if (mogi.max_possible_rooms == 0 and num_needed_teams <= 2) or (num_needed_teams-1) <= mogi.max_possible_rooms: + self.cur_extension_message = f"The extension criteria has been met, so queueing has been extended for 2 more minutes." + mogi.additional_extension += timedelta(minutes=2) + return False + if mogi.start_time <= cur_time and mogi.gathering: # check if there are an even amount of teams since we are past the queue time - num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) # If have en even number of players... if num_leftover_teams == 0: # ...and none of the rooms will be cancelled, close the queue and begin @@ -1142,9 +1150,7 @@ def check_close_event_change(self) -> Tuple[bool, bool]: self.last_extension_message_timestamp = cur_extension_timestamp minutes_left = (force_start_time - cur_time).seconds // 60 - x_teams = int( - int(12 / mogi.max_player_per_team) - num_leftover_teams) - self.cur_extension_message = f"Need {x_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless." + self.cur_extension_message = f"Need {num_needed_teams} more player(s) to start immediately. Starting in {minutes_left + 1} minute(s) regardless." return False async def launch_mogi(self): diff --git a/mogi_objects.py b/mogi_objects.py index 74af3bf..eaa490c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -64,6 +64,7 @@ def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, self.display_time = display_time if is_automated else None self.additional_extension = timedelta( minutes=additional_extension_minutes) + self.has_checked_auto_extend = False @property def num_players(self): From 5591d5a02cfc57f7e02ba3e389ffad978b6280e5 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:18:08 -0500 Subject: [PATCH 236/292] No teams for MKW 6v6 + drafting instructions --- mogi_objects.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index eaa490c..f47414a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -510,7 +510,7 @@ def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): if common.SERVER is common.Server.MKW: self.add_button("6v6", self.general_vote_callback) - async def make_teams(self, format_): + async def make_teams(self, players_per_team: int, vote_str: str): if common.SERVER is common.Server.MKW: self.mogi.making_rooms_run_time = datetime.now(timezone.utc) elif common.SERVER is common.Server.MK8DX: @@ -529,7 +529,7 @@ async def make_teams(self, format_): """ if common.SERVER is common.Server.MKW: msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" - msg += f"Winner: {format_[1]}\n\n" + msg += f"Winner: {vote_str}\n\n" self.header_text = "" tier_text = "Tier " if common.SERVER is common.Server.MK8DX else "T" @@ -537,9 +537,11 @@ async def make_teams(self, format_): msg += self.header_text + "\n" teams = [] - teams_per_room = int(12 / format_[0]) + teams_per_room = 12 // players_per_team + if common.SERVER is common.Server.MKW and vote_str == "6v6": + teams_per_room = 1 for j in range(teams_per_room): - team = Team(self.players[j * format_[0]:(j + 1) * format_[0]]) + team = Team(self.players[j * teams_per_room:(j + 1) * teams_per_room]) teams.append(team) teams.sort(key=lambda team: team.avg_mmr, reverse=True) @@ -557,8 +559,20 @@ async def make_teams(self, format_): if common.SERVER is common.Server.MK8DX: msg += f"\nTable: `/scoreboard`\n" - msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n\n" + elif common.SERVER is common.Server.MKW and vote_str == "6v6": + captain_1 = teams[0].players[0] + captain_2 = teams[1].players[0] + msg += f"\nCaptain #1: {captain_1.lounge_name}\n" + msg += f"Captain #2: {captain_2.lounge_name}\n" + msg += f"""Draft instructions: +1. {captain_1.lounge_name} choose 1 player +2. {captain_2.lounge_name} choose 2 players +3. {captain_1.lounge_name} choose 2 players +4. {captain_2.lounge_name} choose 2 players +5. {captain_1.lounge_name} choose 2 players +6. {captain_2.lounge_name} choose 2 players +7. {captain_1.lounge_name} choose 1 player\n\n""" penalty_time = self.mogi.making_rooms_run_time + \ timedelta(minutes=self.penalty_time) @@ -575,7 +589,7 @@ async def make_teams(self, format_): else: cur_time = datetime.now(timezone.utc) mkw_room_open_time = cur_time + timedelta(minutes=1) - pen_time = cur_time + timedelta(minutes=self.penalty_time) + pen_time = mkw_room_open_time + timedelta(minutes=self.penalty_time) msg += f"\nRoom open at :{mkw_room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" room.teams = teams @@ -588,9 +602,7 @@ async def make_teams(self, format_): async def find_winner(self): if not self.found_winner: - # for some reason max function wasnt working... # It's because you were replacing it. most_votes = len(max(self.votes.values(), key=len)) - winners = [] if len(self.votes["FFA"]) == most_votes: winners.append((1, "FFA")) @@ -608,7 +620,7 @@ async def find_winner(self): for curr_button in self.children: curr_button.disabled = True - await self.make_teams(winner) + await self.make_teams(*winner) def add_button(self, label, callback): button = discord.ui.Button(label=f"{label}: 0", custom_id=label) @@ -630,7 +642,7 @@ async def general_vote_callback(self, interaction: discord.Interaction): self.votes[vote].append(interaction.user.id) if len(self.votes[vote]) == 6: self.found_winner = True # This fixes a race condition - await self.make_teams((players_per_team, vote)) + await self.make_teams(players_per_team, vote) for curr_button in self.children: curr_button.label = f"{curr_button.custom_id}: {len(self.votes[curr_button.custom_id])}" if self.found_winner: From 91b2ffbbfaedeb65ec3b1bf0c52990d6c9927623 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:23:09 -0500 Subject: [PATCH 237/292] Bug fix --- mogi_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index f47414a..aa59270 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -513,6 +513,8 @@ def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): async def make_teams(self, players_per_team: int, vote_str: str): if common.SERVER is common.Server.MKW: self.mogi.making_rooms_run_time = datetime.now(timezone.utc) + if vote_str == "6v6": + players_per_team = 1 elif common.SERVER is common.Server.MK8DX: self.mogi.making_rooms_run_time = self.mogi.start_time + \ timedelta(minutes=5) @@ -538,8 +540,6 @@ async def make_teams(self, players_per_team: int, vote_str: str): teams = [] teams_per_room = 12 // players_per_team - if common.SERVER is common.Server.MKW and vote_str == "6v6": - teams_per_room = 1 for j in range(teams_per_room): team = Team(self.players[j * teams_per_room:(j + 1) * teams_per_room]) teams.append(team) From 0e062dc4f3581a9299d23b4ca330923f612c5c5d Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:41:03 -0500 Subject: [PATCH 238/292] fix drunk changes --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index aa59270..55bd879 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -541,7 +541,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): teams = [] teams_per_room = 12 // players_per_team for j in range(teams_per_room): - team = Team(self.players[j * teams_per_room:(j + 1) * teams_per_room]) + team = Team(self.players[j * players_per_team:(j + 1) * players_per_team]) teams.append(team) teams.sort(key=lambda team: team.avg_mmr, reverse=True) From 4abadfb5a36325e027242dcf625b23064ff9fcdf Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Thu, 25 Jul 2024 07:49:24 -0500 Subject: [PATCH 239/292] Adjust text formatting --- mogi_objects.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 55bd879..3f84d75 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -563,16 +563,16 @@ async def make_teams(self, players_per_team: int, vote_str: str): elif common.SERVER is common.Server.MKW and vote_str == "6v6": captain_1 = teams[0].players[0] captain_2 = teams[1].players[0] - msg += f"\nCaptain #1: {captain_1.lounge_name}\n" - msg += f"Captain #2: {captain_2.lounge_name}\n" + msg += f"\nCaptain #1: **{captain_1.lounge_name}**\n" + msg += f"Captain #2: **{captain_2.lounge_name}**\n" msg += f"""Draft instructions: -1. {captain_1.lounge_name} choose 1 player -2. {captain_2.lounge_name} choose 2 players -3. {captain_1.lounge_name} choose 2 players -4. {captain_2.lounge_name} choose 2 players -5. {captain_1.lounge_name} choose 2 players -6. {captain_2.lounge_name} choose 2 players -7. {captain_1.lounge_name} choose 1 player\n\n""" +1. **{captain_1.lounge_name}** picks 1 player +2. **{captain_2.lounge_name}** picks 2 players +3. **{captain_1.lounge_name}** picks 2 players +4. **{captain_2.lounge_name}** picks 2 players +5. **{captain_1.lounge_name}** picks 2 players +6. **{captain_2.lounge_name}** picks 2 players +7. **{captain_1.lounge_name}** picks 1 player\n""" penalty_time = self.mogi.making_rooms_run_time + \ timedelta(minutes=self.penalty_time) From a50b5e8c1c8889799dd4f2d3a707255025f29499 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:35:21 -0500 Subject: [PATCH 240/292] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aaa5a92..a141a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /credentials.json .idea/* /example_configs/test/* +staff_settings.pkl From c2bdfa3ccb62be16646d304637785bf713025664 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 00:35:39 -0500 Subject: [PATCH 241/292] Store staff settings + allow configuration changes --- cogs/SquadQueue.py | 163 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 129 insertions(+), 34 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 473fc82..eb0ca81 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -26,7 +26,7 @@ # Scheduled_Event = collections.namedtuple('Scheduled_Event', 'size time started mogi_channel') cooldowns = defaultdict(int) -MMR_THRESHOLD_PKL = "mmr_threshold.pkl" +STAFF_SETTINGS_PKL = "staff_settings.pkl" def is_restricted(user: discord.User | discord.Member) -> bool: @@ -52,6 +52,80 @@ def mk8dx_players_allowed(players: List[Player], threshold: int) -> bool: return basic_threshold_players_allowed(players, threshold) +@app_commands.guild_only() +class SettingsGroup(app_commands.Group): + def __init__(self, squad_queue: SquadQueue, *args, **kwargs): + super().__init__(*args, **kwargs) + self.squad_queue: SquadQueue = squad_queue + + @app_commands.guild_only() + @app_commands.command(name="mmr_threshold", description="Change the maximum MMR gap allowed for a room.") + @app_commands.describe(mmr_range="The maximum range allowed for a room. Rooms with a range larger than this will be cancelled.") + async def mmr_threshold(self, interaction: discord.Interaction, mmr_range: int): + self.squad_queue.room_mmr_threshold = mmr_range + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"MMR Threshold for Queue Rooms has been modified to {mmr_range} MMR.") + + @app_commands.command(name="time_between_events", description="Change the amount of time between each queue opening.") + @app_commands.describe(event_time="Amount of time between each queue's opening.") + @app_commands.guild_only() + async def time_between_events(self, interaction: discord.Interaction, event_time: int): + if event_time > 15: + self.squad_queue.QUEUE_OPEN_TIME = timedelta(minutes=event_time) + self.squad_queue.JOINING_TIME = timedelta( + minutes=event_time) - self.squad_queue.EXTENSION_TIME + self.squad_queue.dump_staff_settings() + await interaction.response.send_message( + f"The amount of time for each mogi has been changed to {event_time} minutes.") + else: + await interaction.response.send_message("Please enter a number of minutes greater than 15.") + + @app_commands.guild_only() + @app_commands.command(name="low_mmr_floor", description="Players below this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.describe(rating="Players below this rating will be considered this rating for matchmaking/sub purposes.") + async def low_mmr_floor(self, interaction: discord.Interaction, rating: int): + common.CONFIG["MATCHMAKING_BOTTOM_MMR"] = rating + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"Players below {rating} MMR will be considered {rating} MMR for matchmaking and subbing in purposes.") + + @app_commands.guild_only() + @app_commands.command(name="high_mmr_ceiling", description="Players above this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.describe(rating="Players above this rating will be considered this rating for matchmaking/sub purposes.") + async def high_mmr_ceiling(self, interaction: discord.Interaction, rating: int): + common.CONFIG["MATCHMAKING_TOP_MMR"] = rating + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"Players above {rating} MMR will be considered {rating} MMR for matchmaking and subbing in purposes.") + + @app_commands.guild_only() + @app_commands.command(name="first_event_open_time", description="Use to shift schedule.") + @app_commands.describe(minutes="The number of minutes AFTER 0:00 UTC that your first queue of the day would open.") + async def first_event_open_time(self, interaction: discord.Interaction, minutes: int): + old_amount = common.CONFIG["FIRST_EVENT_TIME"] + shift = minutes - old_amount + common.CONFIG["FIRST_EVENT_TIME"] = minutes + self.squad_queue.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), + tzinfo=timezone.utc) + timedelta(minutes=common.CONFIG["FIRST_EVENT_TIME"]) + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"Changed first queue of the day's open time to {minutes} minutes after 0:00 UTC, which will shift all events by {shift} minutes.") + + @app_commands.guild_only() + @app_commands.command(name="sub_range_allowance", description="Buffer from the lowest and highest player that someone's rating could be to be allowed to sub in.") + @app_commands.describe(buffer="The buffer. E.g. 500 buffer, low mmr 2000, high mmr 6000, so allowed range 1500-6500") + async def sub_range_allowance(self, interaction: discord.Interaction, buffer: int): + self.squad_queue.SUB_RANGE_MMR_ALLOWANCE = buffer + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"Players rated higher than {buffer} below a room's lowest player and lower than {buffer} above a room's highest player will be allowed to sub into the room.") + + @app_commands.guild_only() + @app_commands.command(name="placement_rating", description="Rating for placement players when they queue.") + @app_commands.describe(rating="The placement players' rating.") + async def placement_rating(self, interaction: discord.Interaction, rating: int): + self.squad_queue.PLACEMENT_PLAYER_MMR = rating + common.CONFIG["PLACEMENT_PLAYER_MMR"] = rating + self.squad_queue.dump_staff_settings() + await interaction.response.send_message(f"Placement players' rating will be {rating} MMR when they queue.") + + class SquadQueue(commands.Cog): def __init__(self, bot): self.bot: commands.Bot = bot @@ -108,12 +182,6 @@ def __init__(self, bot): self.SUB_MESSAGE_LIFETIME_SECONDS = bot.config["SUB_MESSAGE_LIFETIME_SECONDS"] self.room_mmr_threshold = bot.config["ROOM_MMR_THRESHOLD"] - if os.path.isfile(MMR_THRESHOLD_PKL): - try: - with open(MMR_THRESHOLD_PKL, 'rb') as f: - self.room_mmr_threshold = dill.load(f) - except: - print(traceback.format_exc()) self.TRACK_TYPE = bot.config["track_type"] @@ -140,7 +208,7 @@ def __init__(self, bot): # If we're testing, set the time to the current time so we start a new event immediately if not self.is_production: - self.FIRST_EVENT_TIME = datetime.now(timezone.utc) + self.FIRST_EVENT_TIME = datetime.now(timezone.utc).replace(second=0, microsecond=0) with open('./timezones.json', 'r') as cjson: self.timezones = json.load(cjson) @@ -152,6 +220,53 @@ def __init__(self, bot): self.ratings = mmr.Ratings() + self.load_staff_settings() + + def load_staff_settings(self, view_only=False): + if os.path.isfile(STAFF_SETTINGS_PKL): + try: + with open(STAFF_SETTINGS_PKL, 'rb') as f: + settings_dict = dill.load(f) + if view_only: + print(settings_dict) + return + if "ROOM_MMR_THRESHOLD" in settings_dict: + self.room_mmr_threshold = settings_dict["ROOM_MMR_THRESHOLD"] + if "QUEUE_OPEN_TIME" in settings_dict: + self.QUEUE_OPEN_TIME = settings_dict["QUEUE_OPEN_TIME"] + if "JOINING_TIME" in settings_dict: + self.JOINING_TIME = settings_dict["JOINING_TIME"] + if "MATCHMAKING_BOTTOM_MMR" in settings_dict: + common.CONFIG["MATCHMAKING_BOTTOM_MMR"] = settings_dict["MATCHMAKING_BOTTOM_MMR"] + if "MATCHMAKING_TOP_MMR" in settings_dict: + common.CONFIG["MATCHMAKING_TOP_MMR"] = settings_dict["MATCHMAKING_TOP_MMR"] + if "FIRST_EVENT_TIME" in settings_dict: + common.CONFIG["FIRST_EVENT_TIME"] = settings_dict["FIRST_EVENT_TIME"] + self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), + tzinfo=timezone.utc) + timedelta(minutes=settings_dict["FIRST_EVENT_TIME"]) + if "SUB_RANGE_MMR_ALLOWANCE" in settings_dict: + self.SUB_RANGE_MMR_ALLOWANCE = settings_dict["SUB_RANGE_MMR_ALLOWANCE"] + if "PLACEMENT_PLAYER_MMR" in settings_dict: + self.PLACEMENT_PLAYER_MMR = settings_dict["PLACEMENT_PLAYER_MMR"] + common.CONFIG["PLACEMENT_PLAYER_MMR"] = settings_dict["PLACEMENT_PLAYER_MMR"] + except: + print(traceback.format_exc()) + + def dump_staff_settings(self): + settings_dict = {"ROOM_MMR_THRESHOLD": self.room_mmr_threshold, + "QUEUE_OPEN_TIME": self.QUEUE_OPEN_TIME, + "JOINING_TIME": self.JOINING_TIME, + "SUB_RANGE_MMR_ALLOWANCE": self.SUB_RANGE_MMR_ALLOWANCE, + "MATCHMAKING_BOTTOM_MMR": common.CONFIG["MATCHMAKING_BOTTOM_MMR"], + "MATCHMAKING_TOP_MMR": common.CONFIG["MATCHMAKING_TOP_MMR"], + "FIRST_EVENT_TIME": common.CONFIG["FIRST_EVENT_TIME"], + "PLACEMENT_PLAYER_MMR": self.PLACEMENT_PLAYER_MMR} + try: + with open(STAFF_SETTINGS_PKL, 'wb') as f: + dill.dump(settings_dict, f) + except: + print(traceback.format_exc()) + @commands.Cog.listener() async def on_ready(self): @@ -178,6 +293,9 @@ async def on_ready(self): except: print("Purging Sub channel failed", flush=True) print(traceback.format_exc()) + + settings_group = SettingsGroup(self, name="settings", description="Change bot configuration. Staff use only.") + self.bot.tree.add_command(settings_group) print(f"Server - {self.GUILD}", flush=True) print(f"Join Channel - {self.MOGI_CHANNEL}", flush=True) print(f"Sub Channel - {self.SUB_CHANNEL}", flush=True) @@ -709,31 +827,6 @@ async def resume_mogi_scheduling(self, interaction: discord.Interaction): self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("Mogis will resume scheduling.") - @app_commands.command(name="change_event_time") - @app_commands.guild_only() - async def change_event_time(self, interaction: discord.Interaction, event_time: int): - """Change the amount of time for each event in the queue. Staff use only.""" - if event_time > 15: - self.QUEUE_OPEN_TIME = timedelta(minutes=event_time) - self.JOINING_TIME = timedelta( - minutes=event_time) - self.EXTENSION_TIME - await interaction.response.send_message( - f"The amount of time for each mogi has been changed to {event_time} minutes.") - else: - await interaction.response.send_message("Please enter a number of minutes greater than 15.") - - @app_commands.command(name="change_mmr_threshold") - @app_commands.guild_only() - async def change_mmr_threshold(self, interaction: discord.Interaction, mmr_threshold: int): - """Change the maximum MMR gap allowed for a room. Staff use only.""" - self.room_mmr_threshold = mmr_threshold - try: - with open(MMR_THRESHOLD_PKL, 'wb') as f: - dill.dump(self.room_mmr_threshold, f) - except: - print(traceback.format_exc()) - await interaction.response.send_message(f"MMR Threshold for Queue Rooms has been modified to {mmr_threshold} MMR.") - @app_commands.command(name="peek_bot_config") @app_commands.guild_only() async def peek_bot_config(self, interaction: discord.Interaction): @@ -788,7 +881,9 @@ async def peek_bot_config(self, interaction: discord.Interaction): msg += f"EVENT LIFETIME: {self.MOGI_LIFETIME} minutes\n" msg += f"EXTRA SUB RANGE ALLOWANCE: +- {self.SUB_RANGE_MMR_ALLOWANCE} MMR\n" msg += f"SUB MESSAGE LIFETIME: {self.SUB_MESSAGE_LIFETIME_SECONDS} seconds\n" - msg += f"ROOM MMR THRESHOLD: {self.room_mmr_threshold}" + msg += f"ROOM MMR THRESHOLD: {self.room_mmr_threshold}\n" + msg += f"MATCHMAKING BOTTOM MMR: {common.CONFIG['MATCHMAKING_BOTTOM_MMR']}\n" + msg += f"MATCHMAKING TOP MMR: {common.CONFIG['MATCHMAKING_TOP_MMR']}" await interaction.response.send_message(msg) @app_commands.command(name="reset_bot") From 27aa7511b98ec3628d014a8c13ba22c780c7a775 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:01:05 -0500 Subject: [PATCH 242/292] config settings for recycle tiers --- example_configs/prod/mkw_rt_prod_config.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 1653979..0555720 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -68,6 +68,12 @@ 805889218516877372, 841730455422107669, 1264728881253060708 + ], + "role_ids_can_see_already": [ + 871740024155168829, + 871739972313575465, + 871739726065963008, + 871739761574969384 ] }, "MT": { @@ -79,6 +85,11 @@ 746216972869435402, 1265451963236094044, 1265452192371048548 + ], + "role_ids_can_see_already": [ + 871739761574969384, + 871739875383181333, + 871739925794525185 ] }, "LT": { @@ -90,6 +101,12 @@ 387361809587240960, 389457003820482571, 1264728728920129597 + ], + "role_ids_can_see_already": [ + 871740123182690314, + 871740169290674236, + 950076470221504653, + 723753340063842345 ] } From c9144b7377ef0b64e9cfdb8850a39854d954d8a7 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 01:57:41 -0500 Subject: [PATCH 243/292] Config checker --- common.py | 5 ++- config_checker.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 config_checker.py diff --git a/common.py b/common.py index 8552213..81225f7 100644 --- a/common.py +++ b/common.py @@ -1,6 +1,7 @@ import json from enum import Enum, auto from typing import TypeVar, Type, List +import config_checker _T = TypeVar('_T') @@ -39,4 +40,6 @@ def is_int(var: str) -> bool: int(var) return True except ValueError: - return False \ No newline at end of file + return False + +config_checker.check(CONFIG) diff --git a/config_checker.py b/config_checker.py new file mode 100644 index 0000000..bfd2a58 --- /dev/null +++ b/config_checker.py @@ -0,0 +1,91 @@ + +STRONGLY_REQUIRED_FIELDS = [ + ("token", str), + ("is_production", bool), + ("lounge", str), + ("lounge_description", str), + ("guild_id", int), + ("queue_join_channel", int), + ("queue_sub_channel", int), + ("queue_list_channel", int), + ("queue_history_channel", int), + ("queue_general_channel", int), + ("admin_roles", list), + ("admin_roles.list_items", int), + ("helper_staff_roles", list), + ("helper_staff_roles.list_items", int), + ("members_for_channels", list), + ("members_for_channels.list_items", (int, bool)), + ("roles_for_channels", list), + ("roles_for_channels.list_items", int), + ("PLACEMENT_PLAYER_MMR", int), + ("placement_role_id", int), + ("frequently_tagged_role_id", int), + ("restricted_role_id", int), + ("muted_role_id", int), + ("queue_messages", bool), + ("sec_between_queue_msgs", int), + ("username", str), + ("password", str), + ("url", str), + ("track_type", str), + ("FIRST_EVENT_TIME", int), + ("FIRST_EVENT_TIME_DESCRIPTION", str), + ("JOINING_TIME", int), + ("QUEUE_OPEN_TIME", int), + ("DISPLAY_OFFSET_MINUTES", int), + ("EXTENSION_TIME", int), + ("ROOM_JOIN_PENALTY_TIME", int), + ("MOGI_LIFETIME", int), + ("SUB_RANGE_MMR_ALLOWANCE", int), + ("SUB_MESSAGE_LIFETIME_SECONDS", int), + ("ROOM_MMR_THRESHOLD", int), + ("MATCHMAKING_BOTTOM_MMR", (int, type(None))), + ("MATCHMAKING_TOP_MMR", (int, type(None))), + ("USE_THREADS", bool) +] + +def check(config: dict): + for required_field_name, required_field_type in STRONGLY_REQUIRED_FIELDS: + check_list_items = required_field_name.endswith(".list_items") + if check_list_items: + required_field_name = required_field_name[:-len(".list_items")] + + if required_field_name not in config: + raise ValueError(f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") + field_data = config[required_field_name] + if type(required_field_type) is not tuple: + required_field_type = (required_field_type,) + if check_list_items: + for item in field_data: + if type(item) not in required_field_type: + raise TypeError( + f"""For field '{required_field_name}', expected items to be of type {" or ".join([str(t) for t in required_field_type])}, but items in config were of type {type(item)}""") + else: + + if type(field_data) not in required_field_type: + raise TypeError( + f"""For field '{required_field_name}', expected type {" or ".join([str(t) for t in required_field_type])}, but config type is {type(field_data)}""") + + # Validate tier channels data if needed + if not config["USE_THREADS"]: + required_field_name = "TIER_CHANNELS" + if required_field_name not in config: + raise ValueError(f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") + tier_channel_data = config[required_field_name] + if type(tier_channel_data) is not dict: + raise TypeError(f"""For field '{required_field_name}', expected type dict, but config type is {type(tier_channel_data)}""") + for k, v in tier_channel_data.items(): + if type(k) is not str: + raise TypeError(f"""For field '{required_field_name}', the keys must all be of type str, but a key of type {type(k)} was found.""") + needed_keys = [("tier_role_id", int), ("channel_ids", list), ("role_ids_can_see_already", list)] + for sub_k, sub_type in needed_keys: + if sub_k not in v: + raise ValueError(f"Did not find field '{sub_k}' in each item of TIER_CHANNELS in the config. Your config appears to be out of date.") + sub_data = v[sub_k] + if type(sub_data) is not sub_type: + raise TypeError(f"""For field '{sub_k}' in TIER_CHANNELS data, found type {type(sub_data)}, expected {sub_type}.""") + + + print("Config file validated.") + From e314a5a32619e972ec0603624024bff7b770a886 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:21:10 -0500 Subject: [PATCH 244/292] Hotfix for vote issue --- mogi_objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 3f84d75..6b01b50 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -487,7 +487,7 @@ def __lt__(self, other: Player): class VoteView(View): def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): super().__init__() - self.players = players + self.players: List[Player] = players self.room_channel: discord.Thread | discord.TextChannel = thread self.mogi = mogi self.room = room @@ -629,6 +629,9 @@ def add_button(self, label, callback): async def general_vote_callback(self, interaction: discord.Interaction): if not self.found_winner: + if str(interaction.user.id) not in [p.member.id for p in self.players]: + await interaction.response.send_message("You are not a player in this event.", ephemeral=True) + return vote = interaction.data['custom_id'] players_per_team = 1 if vote != "FFA": From 68d8a1b2facab2e43d5fd48316acb910998a4cef Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:24:52 -0500 Subject: [PATCH 245/292] Update mogi_objects.py --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 6b01b50..824ce39 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -629,7 +629,7 @@ def add_button(self, label, callback): async def general_vote_callback(self, interaction: discord.Interaction): if not self.found_winner: - if str(interaction.user.id) not in [p.member.id for p in self.players]: + if interaction.user.id not in [p.member.id for p in self.players]: await interaction.response.send_message("You are not a player in this event.", ephemeral=True) return vote = interaction.data['custom_id'] From 3ef4e28c4cc4cf7171d6d0d3f74b099e30fe5cc9 Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:53:48 -0500 Subject: [PATCH 246/292] Docker fixes to save staff settings --- cogs/SquadQueue.py | 2 +- redeploy.sh | 6 ++++-- redeploy_CT.sh | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 redeploy_CT.sh diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index eb0ca81..b346791 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -26,7 +26,7 @@ # Scheduled_Event = collections.namedtuple('Scheduled_Event', 'size time started mogi_channel') cooldowns = defaultdict(int) -STAFF_SETTINGS_PKL = "staff_settings.pkl" +STAFF_SETTINGS_PKL = "./settings_data/staff_settings.pkl" def is_restricted(user: discord.User | discord.Member) -> bool: diff --git a/redeploy.sh b/redeploy.sh index 4cf9181..882142e 100644 --- a/redeploy.sh +++ b/redeploy.sh @@ -1,5 +1,7 @@ #!/bin/bash -docker build --tag mkwii-mogi-queuebot . docker stop mkwii-mogi-queuebot docker rm mkwii-mogi-queuebot -docker run -d --name mkwii-mogi-queuebot --restart unless-stopped mkwii-mogi-queuebot \ No newline at end of file +docker container prune +docker build --tag mkwii-mogi-queuebot . +docker image prune +docker run -d --name mkwii-mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped mkwii-mogi-queuebot diff --git a/redeploy_CT.sh b/redeploy_CT.sh new file mode 100644 index 0000000..b3a14b9 --- /dev/null +++ b/redeploy_CT.sh @@ -0,0 +1,7 @@ +#!/bin/bash +docker stop ct-mkwii-mogi-queuebot +docker rm ct-mkwii-mogi-queuebot +docker container prune +docker build --tag ct-mkwii-mogi-queuebot . +docker image prune +docker run -d --name ct-mkwii-mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped ct-mkwii-mogi-queuebot From 15500567976015b6e99a4c13999459cbd2361b5d Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:59:27 -0500 Subject: [PATCH 247/292] Fix temp roles being removed from subs --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b346791..d8c2fd5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1398,7 +1398,7 @@ async def maintain_roles(self): for member in members: should_have_roles = set() for room in all_rooms: - if room.check_player(member) is not None: + if room.check_player(member) is not None or member.id in room.subs: should_have_roles.add(room.room_role) should_not_have_roles = set(tier_roles) - should_have_roles member_roles = set(member.roles) From 993ca220627a5eb26af8bf575e60c1df8898276b Mon Sep 17 00:00:00 2001 From: Bad Wolf <23650362+BadWolf1023@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:50:27 -0500 Subject: [PATCH 248/292] Bug fix: do not attempt assigning role if using threads --- mogi_objects.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 824ce39..5aa1e8d 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -687,10 +687,11 @@ async def button_callback(self, interaction: discord.Interaction, button): self.room.subs.append(interaction.user.id) button.disabled = True await interaction.response.edit_message(view=self) - try: - await self.room.assign_member_room_role(interaction.user) - except RoleAddFailure as e: - self.room.channel.send(str(e)) + if not common.CONFIG["USE_THREADS"]: + try: + await self.room.assign_member_room_role(interaction.user) + except RoleFailure as e: + self.room.channel.send(str(e)) mention = interaction.user.mention await self.room.channel.send(f"{mention} has joined the room.") else: From 94876227e643ccc2f8b4ab198d4ee82b53ddba31 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Fri, 2 Aug 2024 17:05:37 -0700 Subject: [PATCH 249/292] forces races_left arg in sub to select a number between 1 and 12 --- cogs/SquadQueue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d8c2fd5..ca2b8e5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -554,11 +554,10 @@ def find_room_by_interaction(self, interaction: discord.Interaction) -> Tuple[Ro return room, bottom_room_num return None, 1 - @app_commands.command(name="sub") @app_commands.guild_only() # @commands.cooldown(rate=1, per=120, type=commands.BucketType.user) - async def sub(self, interaction: discord.Interaction, races_left: int): + async def sub(self, interaction: discord.Interaction, races_left: app_commands.Range[int, 1, 12]): """Sends out a request for a sub in the sub channel. Only works in thread channels for SQ rooms.""" current_time = time.time() lastCommandTime = cooldowns.get(interaction.user.id) From 995262307e3c750fbd7efba47f633888451427d3 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Fri, 2 Aug 2024 17:09:28 -0700 Subject: [PATCH 250/292] changed the link to the message at mogi start for mkw --- cogs/SquadQueue.py | 9 ++++++--- mogi_objects.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index ca2b8e5..8e31178 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1018,7 +1018,10 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): f"Skipping room {index} in function write_history.", flush=True) continue msg = room.view.header_text - msg += f"{room.channel.jump_url}\n" + if common.SERVER is common.Server.MKW: + msg += f"{room.view.room_start_msg_link}\n" + elif common.SERVER is common.Server.MK8DX: + msg += f"{room.channel.jump_url}\n" msg += room.view.teams_text msg += "ㅤ" await history_channel.send(msg) @@ -1148,9 +1151,9 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic continue try: - await curr_room.channel.send(room_msg) + sent_msg = await curr_room.channel.send(room_msg) view = VoteView(room_players, curr_room.channel, - mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME) + mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME, sent_msg.jump_url) curr_room.view = view await curr_room.channel.send(view=view) except discord.DiscordException: diff --git a/mogi_objects.py b/mogi_objects.py index 5aa1e8d..a2ee381 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -485,13 +485,14 @@ def __lt__(self, other: Player): class VoteView(View): - def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int): + def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int, room_start_msg_link): super().__init__() self.players: List[Player] = players self.room_channel: discord.Thread | discord.TextChannel = thread self.mogi = mogi self.room = room self.header_text = "" + self.room_start_msg_link = room_start_msg_link self.teams_text = "" self.found_winner = False self.penalty_time = penalty_time From 289a86ec634aee246055652ea03a4d748e64edb2 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Fri, 2 Aug 2024 17:09:56 -0700 Subject: [PATCH 251/292] added mk8dx version of redeploy --- redeploy_MK8DX.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 redeploy_MK8DX.sh diff --git a/redeploy_MK8DX.sh b/redeploy_MK8DX.sh new file mode 100644 index 0000000..21c56fd --- /dev/null +++ b/redeploy_MK8DX.sh @@ -0,0 +1,7 @@ +#!/bin/bash +docker stop mogi-queuebot +docker rm mogi-queuebot +docker container prune +docker build --tag mogi-queuebot . +docker image prune +docker run -d --name mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped mogi-queuebot \ No newline at end of file From b7f17af46f5b4339030841a7ebd2ce80413f02bb Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Thu, 8 Aug 2024 18:06:23 -0700 Subject: [PATCH 252/292] formatted whole project with autopep8 1.6.0 (pycodestyle: 2.8.0) --- .vscode/settings.json | 5 + cogs/SquadQueue.py | 313 ++++++++++++++++++++++++++++-------------- common.py | 4 +- config_checker.py | 32 +++-- host_fcs.py | 3 +- lounge.py | 1 + mmr.py | 71 ++++++---- mogi_objects.py | 197 ++++++++++++++++++-------- requirements.txt | 3 + unit_testing.py | 126 ++++++++++++----- 10 files changed, 518 insertions(+), 237 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..59d666b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.formatting.provider": "autopep8", + "python.formatting.autopep8Args": ["--aggressive", "--max-line-length=79"], + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 8e31178..bd7956e 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -32,14 +32,17 @@ def is_restricted(user: discord.User | discord.Member) -> bool: muted_role_id = common.CONFIG.get("muted_role_id") restricted_role_id = common.CONFIG.get("restricted_role_id") - return (muted_role_id is not None and user.get_role(muted_role_id)) \ - or (restricted_role_id is not None and user.get_role(restricted_role_id)) + return (muted_role_id is not None and user.get_role(muted_role_id)) or ( + restricted_role_id is not None and user.get_role(restricted_role_id)) -def basic_threshold_players_allowed(players: List[Player], threshold: int) -> bool: +def basic_threshold_players_allowed( + players: List[Player], + threshold: int) -> bool: """Returns True if the highest player's rating minus the lowest player's rating is below the given threshold""" sorted_players = sorted(players, key=lambda p: p.mmr) - return (sorted_players[-1].adjusted_mmr - sorted_players[0].adjusted_mmr) <= threshold + return (sorted_players[-1].adjusted_mmr - + sorted_players[0].adjusted_mmr) <= threshold def mkw_players_allowed(players: List[Player], threshold: int) -> bool: @@ -59,14 +62,17 @@ def __init__(self, squad_queue: SquadQueue, *args, **kwargs): self.squad_queue: SquadQueue = squad_queue @app_commands.guild_only() - @app_commands.command(name="mmr_threshold", description="Change the maximum MMR gap allowed for a room.") - @app_commands.describe(mmr_range="The maximum range allowed for a room. Rooms with a range larger than this will be cancelled.") + @app_commands.command(name="mmr_threshold", + description="Change the maximum MMR gap allowed for a room.") + @app_commands.describe( + mmr_range="The maximum range allowed for a room. Rooms with a range larger than this will be cancelled.") async def mmr_threshold(self, interaction: discord.Interaction, mmr_range: int): self.squad_queue.room_mmr_threshold = mmr_range self.squad_queue.dump_staff_settings() await interaction.response.send_message(f"MMR Threshold for Queue Rooms has been modified to {mmr_range} MMR.") - @app_commands.command(name="time_between_events", description="Change the amount of time between each queue opening.") + @app_commands.command(name="time_between_events", + description="Change the amount of time between each queue opening.") @app_commands.describe(event_time="Amount of time between each queue's opening.") @app_commands.guild_only() async def time_between_events(self, interaction: discord.Interaction, event_time: int): @@ -81,43 +87,58 @@ async def time_between_events(self, interaction: discord.Interaction, event_time await interaction.response.send_message("Please enter a number of minutes greater than 15.") @app_commands.guild_only() - @app_commands.command(name="low_mmr_floor", description="Players below this rating will be considered this rating for matchmaking/sub purposes.") - @app_commands.describe(rating="Players below this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.command( + name="low_mmr_floor", + description="Players below this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.describe( + rating="Players below this rating will be considered this rating for matchmaking/sub purposes.") async def low_mmr_floor(self, interaction: discord.Interaction, rating: int): common.CONFIG["MATCHMAKING_BOTTOM_MMR"] = rating self.squad_queue.dump_staff_settings() await interaction.response.send_message(f"Players below {rating} MMR will be considered {rating} MMR for matchmaking and subbing in purposes.") @app_commands.guild_only() - @app_commands.command(name="high_mmr_ceiling", description="Players above this rating will be considered this rating for matchmaking/sub purposes.") - @app_commands.describe(rating="Players above this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.command( + name="high_mmr_ceiling", + description="Players above this rating will be considered this rating for matchmaking/sub purposes.") + @app_commands.describe( + rating="Players above this rating will be considered this rating for matchmaking/sub purposes.") async def high_mmr_ceiling(self, interaction: discord.Interaction, rating: int): common.CONFIG["MATCHMAKING_TOP_MMR"] = rating self.squad_queue.dump_staff_settings() await interaction.response.send_message(f"Players above {rating} MMR will be considered {rating} MMR for matchmaking and subbing in purposes.") @app_commands.guild_only() - @app_commands.command(name="first_event_open_time", description="Use to shift schedule.") - @app_commands.describe(minutes="The number of minutes AFTER 0:00 UTC that your first queue of the day would open.") + @app_commands.command(name="first_event_open_time", + description="Use to shift schedule.") + @app_commands.describe( + minutes="The number of minutes AFTER 0:00 UTC that your first queue of the day would open.") async def first_event_open_time(self, interaction: discord.Interaction, minutes: int): old_amount = common.CONFIG["FIRST_EVENT_TIME"] shift = minutes - old_amount common.CONFIG["FIRST_EVENT_TIME"] = minutes - self.squad_queue.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), - tzinfo=timezone.utc) + timedelta(minutes=common.CONFIG["FIRST_EVENT_TIME"]) + self.squad_queue.FIRST_EVENT_TIME = datetime.combine( + datetime.utcnow().date(), + datetime.min.time(), + tzinfo=timezone.utc) + timedelta( + minutes=common.CONFIG["FIRST_EVENT_TIME"]) self.squad_queue.dump_staff_settings() await interaction.response.send_message(f"Changed first queue of the day's open time to {minutes} minutes after 0:00 UTC, which will shift all events by {shift} minutes.") @app_commands.guild_only() - @app_commands.command(name="sub_range_allowance", description="Buffer from the lowest and highest player that someone's rating could be to be allowed to sub in.") - @app_commands.describe(buffer="The buffer. E.g. 500 buffer, low mmr 2000, high mmr 6000, so allowed range 1500-6500") + @app_commands.command( + name="sub_range_allowance", + description="Buffer from the lowest and highest player that someone's rating could be to be allowed to sub in.") + @app_commands.describe( + buffer="The buffer. E.g. 500 buffer, low mmr 2000, high mmr 6000, so allowed range 1500-6500") async def sub_range_allowance(self, interaction: discord.Interaction, buffer: int): self.squad_queue.SUB_RANGE_MMR_ALLOWANCE = buffer self.squad_queue.dump_staff_settings() await interaction.response.send_message(f"Players rated higher than {buffer} below a room's lowest player and lower than {buffer} above a room's highest player will be allowed to sub into the room.") @app_commands.guild_only() - @app_commands.command(name="placement_rating", description="Rating for placement players when they queue.") + @app_commands.command(name="placement_rating", + description="Rating for placement players when they queue.") @app_commands.describe(rating="The placement players' rating.") async def placement_rating(self, interaction: discord.Interaction, rating: int): self.squad_queue.PLACEMENT_PLAYER_MMR = rating @@ -143,7 +164,8 @@ def __init__(self, bot): self.sq_times = [] # Parameters for tracking if we should send an extension message or not - self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) + self.last_extension_message_timestamp = datetime.now( + timezone.utc).replace(second=0, microsecond=0) self.cur_extension_message = None self._scheduler_task = self.sqscheduler.start() @@ -167,6 +189,8 @@ def __init__(self, bot): self.HISTORY_CHANNEL = None + self.SCHEDULE_CHANNEL = None + self.GENERAL_CHANNEL: discord.TextChannel = None self.URL = bot.config["url"] @@ -196,26 +220,33 @@ def __init__(self, bot): self.DISPLAY_OFFSET_MINUTES = timedelta( minutes=bot.config["DISPLAY_OFFSET_MINUTES"]) - # number of minutes after JOINING_TIME for any potential extra teams to join + # number of minutes after JOINING_TIME for any potential extra teams to + # join self.EXTENSION_TIME = timedelta(minutes=bot.config["EXTENSION_TIME"]) - assert (self.QUEUE_OPEN_TIME >= self.JOINING_TIME + self.EXTENSION_TIME) + assert ( + self.QUEUE_OPEN_TIME >= self.JOINING_TIME + + self.EXTENSION_TIME) - # This is the first event of the day's time. However, this isn't the first event the bot will run. This is literally the time of the first event in a daily schedule. - self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), - tzinfo=timezone.utc) + timedelta( - minutes=bot.config["FIRST_EVENT_TIME"]) + # This is the first event of the day's time. However, this isn't the + # first event the bot will run. This is literally the time of the first + # event in a daily schedule. + self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time( + ), tzinfo=timezone.utc) + timedelta(minutes=bot.config["FIRST_EVENT_TIME"]) - # If we're testing, set the time to the current time so we start a new event immediately + # If we're testing, set the time to the current time so we start a new + # event immediately if not self.is_production: - self.FIRST_EVENT_TIME = datetime.now(timezone.utc).replace(second=0, microsecond=0) + self.FIRST_EVENT_TIME = datetime.now( + timezone.utc).replace(second=0, microsecond=0) with open('./timezones.json', 'r') as cjson: self.timezones = json.load(cjson) # The following dictionary will be populated using the config's staff_helper_roles list. Players can # call these roles into their room/thread using the /staff role command - # These will be refreshed every 24 hours to ensure that the correct name displays for the options + # These will be refreshed every 24 hours to ensure that the correct + # name displays for the options self.helper_staff_roles: Dict[str, discord.Role] = {} self.ratings = mmr.Ratings() @@ -242,32 +273,32 @@ def load_staff_settings(self, view_only=False): common.CONFIG["MATCHMAKING_TOP_MMR"] = settings_dict["MATCHMAKING_TOP_MMR"] if "FIRST_EVENT_TIME" in settings_dict: common.CONFIG["FIRST_EVENT_TIME"] = settings_dict["FIRST_EVENT_TIME"] - self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time(), - tzinfo=timezone.utc) + timedelta(minutes=settings_dict["FIRST_EVENT_TIME"]) + self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time( + ), tzinfo=timezone.utc) + timedelta(minutes=settings_dict["FIRST_EVENT_TIME"]) if "SUB_RANGE_MMR_ALLOWANCE" in settings_dict: self.SUB_RANGE_MMR_ALLOWANCE = settings_dict["SUB_RANGE_MMR_ALLOWANCE"] if "PLACEMENT_PLAYER_MMR" in settings_dict: self.PLACEMENT_PLAYER_MMR = settings_dict["PLACEMENT_PLAYER_MMR"] common.CONFIG["PLACEMENT_PLAYER_MMR"] = settings_dict["PLACEMENT_PLAYER_MMR"] - except: + except BaseException: print(traceback.format_exc()) def dump_staff_settings(self): - settings_dict = {"ROOM_MMR_THRESHOLD": self.room_mmr_threshold, - "QUEUE_OPEN_TIME": self.QUEUE_OPEN_TIME, - "JOINING_TIME": self.JOINING_TIME, - "SUB_RANGE_MMR_ALLOWANCE": self.SUB_RANGE_MMR_ALLOWANCE, - "MATCHMAKING_BOTTOM_MMR": common.CONFIG["MATCHMAKING_BOTTOM_MMR"], - "MATCHMAKING_TOP_MMR": common.CONFIG["MATCHMAKING_TOP_MMR"], - "FIRST_EVENT_TIME": common.CONFIG["FIRST_EVENT_TIME"], - "PLACEMENT_PLAYER_MMR": self.PLACEMENT_PLAYER_MMR} + settings_dict = { + "ROOM_MMR_THRESHOLD": self.room_mmr_threshold, + "QUEUE_OPEN_TIME": self.QUEUE_OPEN_TIME, + "JOINING_TIME": self.JOINING_TIME, + "SUB_RANGE_MMR_ALLOWANCE": self.SUB_RANGE_MMR_ALLOWANCE, + "MATCHMAKING_BOTTOM_MMR": common.CONFIG["MATCHMAKING_BOTTOM_MMR"], + "MATCHMAKING_TOP_MMR": common.CONFIG["MATCHMAKING_TOP_MMR"], + "FIRST_EVENT_TIME": common.CONFIG["FIRST_EVENT_TIME"], + "PLACEMENT_PLAYER_MMR": self.PLACEMENT_PLAYER_MMR} try: with open(STAFF_SETTINGS_PKL, 'wb') as f: dill.dump(settings_dict, f) - except: + except BaseException: print(traceback.format_exc()) - @commands.Cog.listener() async def on_ready(self): self.GUILD = self.bot.get_guild(self.bot.config["guild_id"]) @@ -281,20 +312,26 @@ async def on_ready(self): self.bot.config["queue_history_channel"]) self.GENERAL_CHANNEL = self.bot.get_channel( self.bot.config["queue_general_channel"]) + if self.bot.config["queue_schedule_channel"]: + self.SCHEDULE_CHANNEL = self.bot.get_channel( + self.bot.config["queue_schedule_channel"]) if common.SERVER is common.Server.MKW: await self.get_ladder_info() try: await self.LIST_CHANNEL.purge() - except: + except BaseException: print("Purging List channel failed", flush=True) print(traceback.format_exc()) try: await self.SUB_CHANNEL.purge() - except: + except BaseException: print("Purging Sub channel failed", flush=True) print(traceback.format_exc()) - settings_group = SettingsGroup(self, name="settings", description="Change bot configuration. Staff use only.") + settings_group = SettingsGroup( + self, + name="settings", + description="Change bot configuration. Staff use only.") self.bot.tree.add_command(settings_group) print(f"Server - {self.GUILD}", flush=True) print(f"Join Channel - {self.MOGI_CHANNEL}", flush=True) @@ -344,7 +381,8 @@ async def queue_or_send(self, ctx, msg, delay=0): # Support for both server to implement their own algorithm for a player list being allowed # For now, both servers have a simple rating range check that a list of players must meet, - # but each server could implement more complex checks on a given list of players. + # but each server could implement more complex checks on a given list of + # players. def allowed_players_check(self, players: List[Player]): if common.SERVER is common.Server.MKW: return mkw_players_allowed(players, self.room_mmr_threshold) @@ -360,26 +398,28 @@ async def send_queued_messages(self): channel_queue = self.msg_queue[channel] sentmsgs = [] msg = "" - for i in range(len(channel_queue)-1, -1, -1): + for i in range(len(channel_queue) - 1, -1, -1): msg = channel_queue.pop(i) + "\n" + msg if len(msg) > 1500: sentmsgs.append(msg) msg = "" if len(msg) > 0: sentmsgs.append(msg) - for i in range(len(sentmsgs)-1, -1, -1): + for i in range(len(sentmsgs) - 1, -1, -1): await channel.send(sentmsgs[i]) except Exception as e: print(traceback.format_exc()) - def get_mogi(self, ctx: discord.Interaction | discord.ext.commands.context.Context) -> Mogi | None: + def get_mogi(self, ctx: discord.Interaction | + discord.ext.commands.context.Context) -> Mogi | None: if self.ongoing_event is None: return None if self.ongoing_event.mogi_channel.id == ctx.channel.id: return self.ongoing_event def is_staff(self, member: discord.Member): - return any(member.get_role(staff_role) for staff_role in self.bot.config["admin_roles"]) + return any(member.get_role(staff_role) + for staff_role in self.bot.config["admin_roles"]) async def is_started(self, ctx, mogi: Mogi): if not mogi.started: @@ -401,7 +441,7 @@ async def extend(self, interaction: discord.Interaction, minutes: int): minutes: int The number of minutes to add to the extension time. Can be negative. - Staff use only. + Staff use only. """ mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: @@ -447,10 +487,12 @@ async def join_queue(self, interaction: discord.Interaction, host=False): # The player was queued as host, and they queued again as a host if original_host_status and host: await interaction.response.send_message(f"{interaction.user.mention} is already signed up as a host.") - # The player was queued as host, and they queued again as a non-host + # The player was queued as host, and they queued again as a + # non-host elif original_host_status and not host: await interaction.response.send_message(f"{interaction.user.mention} has changed to a non-host.") - # The player was not queued as host, but they are changing to a host + # The player was not queued as host, but they are changing to a + # host elif not original_host_status and host: await interaction.response.send_message(f"{interaction.user.mention} has changed to a host.") # The player was not queued as host and did not change to a host @@ -540,7 +582,8 @@ async def host(self, interaction: discord.Interaction): host_str = "No one in this room queued as host! Ask someone who the host is." await interaction.response.send_message(host_str) - def find_room_by_interaction(self, interaction: discord.Interaction) -> Tuple[Room | None, int]: + def find_room_by_interaction( + self, interaction: discord.Interaction) -> Tuple[Room | None, int]: """Given an interaction, returns the Room and room number associated with the interaction. Searches current and old, undeleted events. If the interaction is not associated with a Room, (None, 1) is returned.""" @@ -561,8 +604,10 @@ async def sub(self, interaction: discord.Interaction, races_left: app_commands.R """Sends out a request for a sub in the sub channel. Only works in thread channels for SQ rooms.""" current_time = time.time() lastCommandTime = cooldowns.get(interaction.user.id) - print(f"{interaction.user.name} requests a sub, previous sub command time: {lastCommandTime}", flush=True) - if lastCommandTime == None: + print( + f"{interaction.user.name} requests a sub, previous sub command time: {lastCommandTime}", + flush=True) + if lastCommandTime is None: lastCommandTime = 0 # Cooldown timer in seconds @@ -660,7 +705,7 @@ async def list_task(self): else: new_message = await self.LIST_CHANNEL.send(message) self.list_messages.append(new_message) - except: + except BaseException: await self.delete_list_messages(0) for i, message in enumerate(new_messages): new_message = await self.LIST_CHANNEL.send(message) @@ -680,7 +725,9 @@ async def delete_list_messages(self, new_list_size: int): @commands.Cog.listener() async def on_message(self, message: discord.Message): - if message.author.bot or not (message.content.isdecimal() and 12 <= int(message.content) <= 180): + if message.author.bot or not ( + message.content.isdecimal() and 12 <= int( + message.content) <= 180): return mogi = None if self.ongoing_event is not None: @@ -728,7 +775,7 @@ async def scoreboard(self, interaction: discord.Interaction): await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) return - format_ = round(12/len(room.teams)) + format_ = round(12 / len(room.teams)) msg = f"!submit {format_} {room.tier}\n" for team in room.teams: @@ -764,7 +811,6 @@ async def remove_player(self, interaction: discord.Interaction, member: discord. if event_status_launched: await self.launch_mogi() - @app_commands.command(name="ping_staff") @app_commands.guild_only() @app_commands.checks.cooldown(1, 300, key=lambda i: i.user.id) @@ -895,6 +941,22 @@ async def reset_bot(self, interaction: discord.Interaction): self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("All events have been deleted. Queue will restart shortly.") + @app_commands.command(name="schedule_sq_times") + @app_commands.guild_only() + async def schedule_sq_times(self, interaction: discord.Interaction, date: str): + """Schedule Squad Queue times. Staff use only.""" + await interaction.response.send_message(f"Testing date {date}") + + @schedule_sq_times.autocomplete("date") + async def date_autocomplete(ctx: discord.AutocompleteContext): + today = datetime.now().date() + options = [] + for i in range(25): + date = today + timedelta(days=i) + options.append(discord.OptionChoice( + name=date.isoformat(), value=date.isoformat())) + return options + @commands.command(name="schedule_sq_times") @commands.guild_only() async def schedule_sq_times(self, ctx, timestamps: commands.Greedy[int]): @@ -1000,7 +1062,8 @@ async def end_voting(mogi: Mogi): for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( - f"Skipping room {index} in function end_voting.", flush=True) + f"Skipping room {index} in function end_voting.", + flush=True) continue await room.view.find_winner() except Exception as e: @@ -1015,7 +1078,8 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( - f"Skipping room {index} in function write_history.", flush=True) + f"Skipping room {index} in function write_history.", + flush=True) continue msg = room.view.header_text if common.SERVER is common.Server.MKW: @@ -1045,7 +1109,8 @@ async def check_room_threads(self, mogi: Mogi): room_channel = await self.GENERAL_CHANNEL.create_thread(name=room_name, auto_archive_duration=60, invitable=False) - # Address race condition - race condition would result in making too many rooms + # Address race condition - race condition would result in + # making too many rooms if len(mogi.rooms) >= mogi.max_possible_rooms: await room_channel.delete() return @@ -1055,11 +1120,12 @@ async def check_room_threads(self, mogi: Mogi): err_msg = f"\nAn error has occurred while creating a room channel:\n{e}" await mogi.mogi_channel.send(err_msg) return - mogi.rooms.append(Room(None, i+1, room_channel, self.TIER_INFO)) + mogi.rooms.append(Room(None, i + 1, room_channel, self.TIER_INFO)) @staticmethod async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): - # We could have used asyncio.call_later(120, handle_voting_and_history) in the caller's code + # We could have used asyncio.call_later(120, handle_voting_and_history) + # in the caller's code await asyncio.sleep(120) await SquadQueue.end_voting(mogi) await SquadQueue.write_history(mogi, history_channel) @@ -1096,12 +1162,13 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic all_confirmed_players = mogi.players_on_confirmed_teams() first_late_player_index = ( - mogi.num_players//mogi.players_per_room) * mogi.players_per_room + mogi.num_players // mogi.players_per_room) * mogi.players_per_room regular_player_list = all_confirmed_players[:first_late_player_index] late_player_list = all_confirmed_players[first_late_player_index:] proposed_list = mogi.generate_proposed_list(self.allowed_players_check) await mogi.populate_host_fcs() - for room_number, room_players in enumerate(divide_chunks(proposed_list, mogi.players_per_room), 1): + for room_number, room_players in enumerate( + divide_chunks(proposed_list, mogi.players_per_room), 1): mogi_channel_msg = f"`Room {room_number} - Player List`\n" room_msg = mogi_channel_msg for player_num, player in enumerate(room_players, 1): @@ -1131,10 +1198,12 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic If you need staff's assistance, use the `/ping_staff` command in this channel.""" - # The below try/except clause around Room.prepare_room_channel only runs if the config's USE_THREADS is set to false + # The below try/except clause around Room.prepare_room_channel + # only runs if the config's USE_THREADS is set to false try: await curr_room.prepare_room_channel(self.GUILD, all_events=([self.ongoing_event] + self.old_events)) - # Non-fatal error, message already sent to room channel, continue with room creation + # Non-fatal error, message already sent to room channel, + # continue with room creation except mogi_objects.RoleAddFailure: pass # semi-critical failures, but the room can proceed @@ -1152,8 +1221,13 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic try: sent_msg = await curr_room.channel.send(room_msg) - view = VoteView(room_players, curr_room.channel, - mogi, curr_room, self.ROOM_JOIN_PENALTY_TIME, sent_msg.jump_url) + view = VoteView( + room_players, + curr_room.channel, + mogi, + curr_room, + self.ROOM_JOIN_PENALTY_TIME, + sent_msg.jump_url) curr_room.view = view await curr_room.channel.send(view=view) except discord.DiscordException: @@ -1166,7 +1240,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic await mogi.mogi_channel.send(mogi_channel_msg) except discord.DiscordException: print( - f"Mogi Channel message for room {room_number} has failed to send.", flush=True) + f"Mogi Channel message for room {room_number} has failed to send.", + flush=True) print(traceback.format_exc()) # Compute the list of "late" players that didn't get into any room @@ -1187,7 +1262,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic # We could have used asyncio.call_later(120, handle_voting_and_history) # and removed asyncio.sleep(120) in handle_voting_and_history if not common.CONFIG["USE_THREADS"]: - asyncio.create_task(self.ongoing_event.assign_roles(guild=self.GUILD)) + asyncio.create_task( + self.ongoing_event.assign_roles(guild=self.GUILD)) asyncio.create_task(SquadQueue.handle_voting_and_history( self.ongoing_event, self.HISTORY_CHANNEL)) self.old_events.append(self.ongoing_event) @@ -1203,7 +1279,8 @@ def check_close_event_change(self) -> Tuple[bool, bool]: # If the mogi is not gathering, don't run this # If the mogi has already made the rooms, don't run this. # This logic was taken from a much more complex bot. It could be greatly simplified since all events here - # are automated and follow a certain flow, but I am not going to change what isn't broken. + # are automated and follow a certain flow, but I am not going to + # change what isn't broken. if not mogi.is_automated or not mogi.started or mogi.making_rooms_run or not mogi.gathering: return False cur_time = datetime.now(timezone.utc) @@ -1214,11 +1291,16 @@ def check_close_event_change(self) -> Tuple[bool, bool]: self.cur_extension_message = None return True num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) - num_needed_teams = (12 // mogi.max_player_per_team) - num_leftover_teams - if common.SERVER is common.Server.MKW and (force_start_time - timedelta(minutes=1)) <= cur_time and num_leftover_teams != 0: + num_needed_teams = ( + 12 // mogi.max_player_per_team) - num_leftover_teams + if common.SERVER is common.Server.MKW and ( + force_start_time - + timedelta( + minutes=1)) <= cur_time and num_leftover_teams != 0: if not mogi.has_checked_auto_extend: mogi.has_checked_auto_extend = True - if (mogi.max_possible_rooms == 0 and num_needed_teams <= 2) or (num_needed_teams-1) <= mogi.max_possible_rooms: + if (mogi.max_possible_rooms == 0 and num_needed_teams <= 2) or ( + num_needed_teams - 1) <= mogi.max_possible_rooms: self.cur_extension_message = f"The extension criteria has been met, so queueing has been extended for 2 more minutes." mogi.additional_extension += timedelta(minutes=2) return False @@ -1233,16 +1315,20 @@ def check_close_event_change(self) -> Tuple[bool, bool]: return True # ...any of the rooms will be cancelled, set the error message else: - self.last_extension_message_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) - minutes_left = (force_start_time - cur_time).seconds // 60 + self.last_extension_message_timestamp = datetime.now( + timezone.utc).replace(second=0, microsecond=0) + minutes_left = (force_start_time - + cur_time).seconds // 60 self.cur_extension_message = f"One or more of the rooms will be cancelled, so the extension " \ f"will continue. Starting in {minutes_left + 1} minute(s) " \ f"regardless." else: - cur_extension_timestamp = datetime.now(timezone.utc).replace(second=0, microsecond=0) + cur_extension_timestamp = datetime.now( + timezone.utc).replace(second=0, microsecond=0) # At this point, we're in the extension time. So if the extension timestamp is in a different - # minute than the last one, we set the new extension message to be sent. + # minute than the last one, we set the new extension + # message to be sent. if cur_extension_timestamp != self.last_extension_message_timestamp: self.last_extension_message_timestamp = cur_extension_timestamp minutes_left = (force_start_time - @@ -1267,16 +1353,21 @@ async def check_send_extension_message(self): async def scheduler_mogi_start(self): cur_time = datetime.now(timezone.utc) next_mogi = self.next_event - if next_mogi is not None and (next_mogi.start_time - self.JOINING_TIME) < cur_time: - # We are trying to launch the event - fail or not, we set next event to None + if next_mogi is not None and ( + next_mogi.start_time - + self.JOINING_TIME) < cur_time: + # We are trying to launch the event - fail or not, we set next + # event to None self.next_event = None - # There is an ongoing event, but people are still queueing, so remove it and fail + # There is an ongoing event, but people are still queueing, so + # remove it and fail if self.ongoing_event is not None and self.ongoing_event.gathering: await next_mogi.mogi_channel.send( f"Because there is an ongoing event right now, the following event has been removed:\n{self.get_event_str(next_mogi)}\n") return # There is an ongoing event, but it has already started, so add it to the old events - # This is potentially an issue - we should be checking if make rooms has been run, not if it's started...? + # This is potentially an issue - we should be checking if make + # rooms has been run, not if it's started...? if self.ongoing_event is not None and self.ongoing_event.started: self.old_events.append(self.ongoing_event) @@ -1293,7 +1384,8 @@ async def sqscheduler(self): """Scheduler that checks if it should start mogis and close them""" # It may seem silly to do try/except Exception, but this coroutine **cannot** fail # This coroutine *silently* fails and stops if exceptions aren't caught - an annoying abtraction of asyncio - # This is unacceptable considering people are relying on these mogis to run, so we will not allow this routine to stop + # This is unacceptable considering people are relying on these mogis to + # run, so we will not allow this routine to stop try: if self.ongoing_event is None: await self.schedule_que_event() @@ -1327,10 +1419,12 @@ async def schedule_que_event(self): next_event_open_time = self.compute_next_event_open_time() next_event_start_time = next_event_open_time + self.JOINING_TIME next_event_display_time = next_event_start_time + self.DISPLAY_OFFSET_MINUTES - # We don't want to schedule the next event if it would open after it's joining period and during its extension period + # We don't want to schedule the next event if it would open after + # it's joining period and during its extension period if next_event_start_time < datetime.now(timezone.utc): return - if len(self.sq_times) > 0 and next_event_open_time == self.sq_times[0]: + if len( + self.sq_times) > 0 and next_event_open_time == self.sq_times[0]: self.sq_times.pop(0) self.next_event = None self.ongoing_event = None @@ -1364,7 +1458,8 @@ async def delete_old_mogis(self): curr_time - mogi_lifetime) > m.start_time] for mogi in delete_queue: print( - f"Deleting {mogi.start_time} Mogi at {curr_time}", flush=True) + f"Deleting {mogi.start_time} Mogi at {curr_time}", + flush=True) self.old_events.remove(mogi) except Exception as e: print(traceback.format_exc()) @@ -1373,17 +1468,23 @@ async def delete_old_mogis(self): async def maintain_roles(self): """Removes roles from people who are no longer supposed to see tier channels.""" try: - should_start_role_removal = (self.bot_startup_time + timedelta(minutes=self.MOGI_LIFETIME)) <= datetime.now(timezone.utc) + should_start_role_removal = ( + self.bot_startup_time + + timedelta( + minutes=self.MOGI_LIFETIME)) <= datetime.now( + timezone.utc) if should_start_role_removal: # Fetch all members (API call) for server to ensure role information is up-to-date - # WARNING: if your server is large, you need a higher loop time on the task + # WARNING: if your server is large, you need a higher loop time + # on the task members = [member async for member in self.GUILD.fetch_members(limit=None)] # Gather all discord.Role objects tier_roles = [] tier_role_data = common.CONFIG["TIER_CHANNELS"] for tier, tier_data in tier_role_data.items(): - tier_roles.append(self.GUILD.get_role(tier_data["tier_role_id"])) + tier_roles.append(self.GUILD.get_role( + tier_data["tier_role_id"])) # Create a list of all current and existing old events all_mogis = [ev for ev in self.old_events if ev is not None] @@ -1393,22 +1494,25 @@ async def maintain_roles(self): # Create a list of all Rooms who have a valid room role all_rooms = [] for mogi in all_mogis: - all_rooms.extend([r for r in mogi.rooms if r is not None and r.room_role is not None]) + all_rooms.extend( + [r for r in mogi.rooms if r is not None and r.room_role is not None]) # Go through each member in the server and, based on what roles they should have because of the # events they are in, remove any roles they shouldn't have for member in members: should_have_roles = set() for room in all_rooms: - if room.check_player(member) is not None or member.id in room.subs: + if room.check_player( + member) is not None or member.id in room.subs: should_have_roles.add(room.room_role) should_not_have_roles = set(tier_roles) - should_have_roles member_roles = set(member.roles) - roles_to_remove = should_not_have_roles.intersection(member_roles) + roles_to_remove = should_not_have_roles.intersection( + member_roles) if len(roles_to_remove) > 0: try: await member.remove_roles(*roles_to_remove) - except: + except BaseException: print(traceback.format_exc()) except Exception as e: @@ -1419,17 +1523,17 @@ async def check_room_threads_task(self): try: if self.ongoing_event is not None: await self.check_room_threads(self.ongoing_event) - except: + except BaseException: print(traceback.format_exc()) - @tasks.loop(hours=24) async def refresh_helper_roles(self): """Refreshes the helper staff role names for the /ping_staff command using the role IDs in the config""" try: helper_staff_role_ids = self.bot.config["helper_staff_roles"] # In my experience, large servers experience caching issues. - # This is a forced API call which guarantees the role information will be up-to-date. + # This is a forced API call which guarantees the role information + # will be up-to-date. all_roles = await self.GUILD.fetch_roles() updated_roles = {} for role_id in helper_staff_role_ids: @@ -1440,7 +1544,8 @@ async def refresh_helper_roles(self): # As a fail safe, if we didn't add any new roles, either due to a misconfiguration or an internal discord # issue, only update if we found one or more of the roles if len(updated_roles) > 0: - # We use clear and update to ensure any *references* to the original dictionary are updated + # We use clear and update to ensure any *references* to the + # original dictionary are updated self.helper_staff_roles.clear() self.helper_staff_roles.update(updated_roles) except Exception as e: @@ -1492,8 +1597,9 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): check_players.extend(members) players = self.ratings.get_rating(check_players) for i in range(0, 12): - player = Player( - players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) + player = Player(players[0].member, + f"{players[0].lounge_name}{i + 1}", + players[0].mmr + (10 * i)) player.confirmed = True squad = Team([player]) mogi.teams.append(squad) @@ -1548,8 +1654,9 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem check_players.extend(members) players = self.ratings.get_rating(check_players) for i in range(0, num_times): - player = Player( - players[0].member, f"{players[0].lounge_name}{i + 1}", players[0].mmr + (10 * i)) + player = Player(players[0].member, + f"{players[0].lounge_name}{i + 1}", + players[0].mmr + (10 * i)) player.confirmed = True squad = Team([player]) mogi.teams.append(squad) diff --git a/common.py b/common.py index 81225f7..002c13d 100644 --- a/common.py +++ b/common.py @@ -19,7 +19,8 @@ class Server(Enum): elif CONFIG["lounge"] == "MK8DX": SERVER = Server.MK8DX else: - raise ValueError(f"{CONFIG['lounge']} is not a valid option for the 'lounge' attribute in the config.") + raise ValueError( + f"{CONFIG['lounge']} is not a valid option for the 'lounge' attribute in the config.") def divide_chunks(list_: list, chunk_size: int): @@ -42,4 +43,5 @@ def is_int(var: str) -> bool: except ValueError: return False + config_checker.check(CONFIG) diff --git a/config_checker.py b/config_checker.py index bfd2a58..37d533c 100644 --- a/config_checker.py +++ b/config_checker.py @@ -45,6 +45,7 @@ ("USE_THREADS", bool) ] + def check(config: dict): for required_field_name, required_field_type in STRONGLY_REQUIRED_FIELDS: check_list_items = required_field_name.endswith(".list_items") @@ -52,9 +53,10 @@ def check(config: dict): required_field_name = required_field_name[:-len(".list_items")] if required_field_name not in config: - raise ValueError(f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") + raise ValueError( + f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") field_data = config[required_field_name] - if type(required_field_type) is not tuple: + if not isinstance(required_field_type, tuple): required_field_type = (required_field_type,) if check_list_items: for item in field_data: @@ -71,21 +73,25 @@ def check(config: dict): if not config["USE_THREADS"]: required_field_name = "TIER_CHANNELS" if required_field_name not in config: - raise ValueError(f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") + raise ValueError( + f"Did not find field '{required_field_name}' in config. Your config appears to be out of date.") tier_channel_data = config[required_field_name] - if type(tier_channel_data) is not dict: - raise TypeError(f"""For field '{required_field_name}', expected type dict, but config type is {type(tier_channel_data)}""") + if not isinstance(tier_channel_data, dict): + raise TypeError( + f"""For field '{required_field_name}', expected type dict, but config type is {type(tier_channel_data)}""") for k, v in tier_channel_data.items(): - if type(k) is not str: - raise TypeError(f"""For field '{required_field_name}', the keys must all be of type str, but a key of type {type(k)} was found.""") - needed_keys = [("tier_role_id", int), ("channel_ids", list), ("role_ids_can_see_already", list)] + if not isinstance(k, str): + raise TypeError( + f"""For field '{required_field_name}', the keys must all be of type str, but a key of type {type(k)} was found.""") + needed_keys = [("tier_role_id", int), ("channel_ids", + list), ("role_ids_can_see_already", list)] for sub_k, sub_type in needed_keys: if sub_k not in v: - raise ValueError(f"Did not find field '{sub_k}' in each item of TIER_CHANNELS in the config. Your config appears to be out of date.") + raise ValueError( + f"Did not find field '{sub_k}' in each item of TIER_CHANNELS in the config. Your config appears to be out of date.") sub_data = v[sub_k] - if type(sub_data) is not sub_type: - raise TypeError(f"""For field '{sub_k}' in TIER_CHANNELS data, found type {type(sub_data)}, expected {sub_type}.""") - + if not isinstance(sub_data, sub_type): + raise TypeError( + f"""For field '{sub_k}' in TIER_CHANNELS data, found type {type(sub_data)}, expected {sub_type}.""") print("Config file validated.") - diff --git a/host_fcs.py b/host_fcs.py index a107ab2..d713b24 100644 --- a/host_fcs.py +++ b/host_fcs.py @@ -26,7 +26,8 @@ async def _get_mkw_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: return {} results = await r.json() if "status" not in results or results["status"] != "success": - print(f"hostfc endpoint returned unsuccessful results {results}") + print( + f"hostfc endpoint returned unsuccessful results {results}") return {} host_mapping = {} for item in results["results"]: diff --git a/lounge.py b/lounge.py index c9dd60e..68c2f41 100644 --- a/lounge.py +++ b/lounge.py @@ -56,6 +56,7 @@ async def on_app_command_error(interaction: discord.Interaction, error: discord. else: raise error + @bot.event async def setup_hook(): for extension in initial_extensions: diff --git a/mmr.py b/mmr.py index c81c46c..e586cbe 100644 --- a/mmr.py +++ b/mmr.py @@ -45,12 +45,14 @@ async def update_ratings(self): status = await rating_func() # If we failed to pull ratings, wait 60 seconds and try again if not status: - print(f"Failed to pull ratings for {common.SERVER.name}. Waiting 60 seconds and trying again.") + print( + f"Failed to pull ratings for {common.SERVER.name}. Waiting 60 seconds and trying again.") await asyncio.sleep(60) status = await rating_func() # If we failed to pull ratings again, fail. if not status: - print(f"Failed to pull ratings for {common.SERVER.name} on 2nd attempt. Skipping.") + print( + f"Failed to pull ratings for {common.SERVER.name} on 2nd attempt. Skipping.") return self.first_run_complete = True @@ -74,7 +76,8 @@ async def _pull_ratings(url, parser, validator) -> bool: try: validator(data) except RatingRequestFailure: - print(f"{common.SERVER.name}'s data from API was formatted incorrectly.") + print( + f"{common.SERVER.name}'s data from API was formatted incorrectly.") print(traceback.format_exc()) return False parser(data) @@ -82,34 +85,38 @@ async def _pull_ratings(url, parser, validator) -> bool: return False # Didn't run request successfully, so we return it was a failure def _validate_mk8dx_response(self, results: dict): - if type(results) is not dict: + if not isinstance(results, dict): raise BadRatingData("Response is not a dictionary") all_players = results.get("players") if all_players is None: - raise BadRatingData("Key word 'players' not found in JSON response.") + raise BadRatingData( + "Key word 'players' not found in JSON response.") # required_player_amount = 10000 if len(all_players) < required_player_amount: raise BadPlayerDataLength( - f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") + f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response." + "") strongly_required_fields = [("name", str)] weakly_required_fields = [("discordId", str), ("mmr", int)] for player in all_players: - # Ensure all strongly required fields are in the player JSON and that the type is correct + # Ensure all strongly required fields are in the player JSON and + # that the type is correct for strongly_req_field_name, strongly_req_field_type in strongly_required_fields: if strongly_req_field_name not in player: raise BadPlayerData( f"Missing required field '{strongly_req_field_name}' in the following player: {player}") field_data = player[strongly_req_field_name] - if type(field_data) is not strongly_req_field_type: + if not isinstance(field_data, strongly_req_field_type): raise BadPlayerData( f"For field '{strongly_req_field_name}', expected type '{strongly_req_field_type}' received {type(field_data)} for player: {player}") - # Ensure that if the weakly required field is in the JSON, the type is correct + # Ensure that if the weakly required field is in the JSON, the type + # is correct for weakly_req_field_name, weakly_req_field_type in weakly_required_fields: if weakly_req_field_name in player: field_data = player[weakly_req_field_name] - if type(field_data) is not weakly_req_field_type: + if not isinstance(field_data, weakly_req_field_type): raise BadPlayerData( f"For field '{weakly_req_field_name}', expected type '{weakly_req_field_type}' received {type(field_data)} for player: {player}") @@ -120,44 +127,55 @@ def _parse_mk8dx_ratings(self, results: dict): discord_id = player.get("discordId") if discord_id is None: continue - rating = common.CONFIG["PLACEMENT_PLAYER_MMR"] if player.get("mmr") is None else player.get("mmr") + rating = common.CONFIG["PLACEMENT_PLAYER_MMR"] if player.get( + "mmr") is None else player.get("mmr") self.ratings[discord_id] = (rating, player["name"]) def _validate_mkw_response(self, results: dict): - if type(results) is not dict: + if not isinstance(results, dict): raise BadRatingData("Response is not a dictionary") mkw_status = results.get("status") if mkw_status is None: - raise BadRatingData("Key word 'status' not found in JSON response.") + raise BadRatingData( + "Key word 'status' not found in JSON response.") if mkw_status != "success": - raise BadRatingData(f"'status' had value of {mkw_status} in JSON response.") + raise BadRatingData( + f"'status' had value of {mkw_status} in JSON response.") all_players = results.get("results") if all_players is None: - raise BadRatingData("Key word 'results' not found in JSON response.") + raise BadRatingData( + "Key word 'results' not found in JSON response.") # required_player_amount = 1000 if len(all_players) < required_player_amount: raise BadPlayerDataLength( - f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response.""") + f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response." + "") for player in all_players: if "player_name" not in player: - raise BadPlayerData(f"Missing required field 'player_name' in the following player: {player}") + raise BadPlayerData( + f"Missing required field 'player_name' in the following player: {player}") if "discord_user_id" not in player: - raise BadPlayerData(f"Missing required field 'discord_user_id' in the following player: {player}") + raise BadPlayerData( + f"Missing required field 'discord_user_id' in the following player: {player}") if "current_mmr" not in player: - raise BadPlayerData(f"Missing required field 'current_mmr' in the following player: {player}") + raise BadPlayerData( + f"Missing required field 'current_mmr' in the following player: {player}") player_name = player.get("player_name") discord_user_id = player.get("discord_user_id") current_mmr = player.get("current_mmr") - if not (type(discord_user_id) is str or discord_user_id is None): + if not ( + isinstance( + discord_user_id, + str) or discord_user_id is None): raise BadPlayerData( f"For field 'discord_user_id', expected type 'str' or None, received {type(discord_user_id)} for player: {player}") - if type(current_mmr) is not int: + if not isinstance(current_mmr, int): raise BadPlayerData( f"For field 'current_mmr', expected type 'int' received {type(current_mmr)} for player: {player}") - if type(player_name) is not str: + if not isinstance(player_name, str): raise BadPlayerData( f"For field 'player_name', expected type 'str' received {type(player_name)} for player: {player}") @@ -167,7 +185,8 @@ def _parse_mkw_ratings(self, results: dict): for player in all_players: discord_user_id = player.get("discord_user_id") if discord_user_id is not None: - self.ratings[player["discord_user_id"]] = (player["current_mmr"], player["player_name"]) + self.ratings[player["discord_user_id"]] = ( + player["current_mmr"], player["player_name"]) def get_rating_from_discord_id(self, discord_id: int | str) -> int | None: discord_id = str(discord_id) @@ -179,12 +198,14 @@ def get_rating_from_discord_id(self, discord_id: int | str) -> int | None: else: return None - def get_rating(self, members: List[discord.User | discord.Member]) -> List[Player]: + def get_rating( + self, members: List[discord.User | discord.Member]) -> List[Player]: if not self.first_run_complete: raise RatingsNotReady("Ratings not pulled yet.") all_players = [] for member in members: - member_id = str(member.id) # Are discord member IDs already strings...? + # Are discord member IDs already strings...? + member_id = str(member.id) if member_id not in self.ratings: continue rating, name = self.ratings[member_id] diff --git a/mogi_objects.py b/mogi_objects.py index a2ee381..08f18cb 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -47,8 +47,16 @@ class Mogi: ALGORITHM_STATUS_SUCCESS_EMPTY = 4 ALGORITHM_STATUS_SUCCESS_SOME_INVALID = 5 - def __init__(self, sq_id: int, max_players_per_team: int, players_per_room: int, mogi_channel: discord.TextChannel, - is_automated=False, start_time=None, display_time=None, additional_extension_minutes=0): + def __init__( + self, + sq_id: int, + max_players_per_team: int, + players_per_room: int, + mogi_channel: discord.TextChannel, + is_automated=False, + start_time=None, + display_time=None, + additional_extension_minutes=0): self.started = False self.gathering = False self.making_rooms_run = False @@ -83,21 +91,28 @@ def max_possible_rooms(self) -> int: return self.num_players // self.players_per_room @staticmethod - def _minimize_range(players: List[Player], num_players: int) -> List[Player] | None: + def _minimize_range( + players: List[Player], + num_players: int) -> List[Player] | None: """Returns a collection of players (the number of players in the collection is the given num_players parameter) whose has the smallest mmr spread. If the number of players in the given list is smaller than the request num_players collection size, or the num_players is less than 2 (doesn't make sense), None is returned.""" - # The number of players we were given is less than the collection size we are supposed to return, so return None + # The number of players we were given is less than the collection size + # we are supposed to return, so return None if len(players) < num_players: return None if num_players <= 1: return None - # Sort the players so we easily know the player with the lowest rating and highest rating in any given collection + # Sort the players so we easily know the player with the lowest rating + # and highest rating in any given collection sorted_players = sorted(players) - # In the beginning, the best found collection of players is the first 12 + # In the beginning, the best found collection of players is the first + # 12 best_collection = sorted_players[0:num_players] - cur_min = best_collection[-1].adjusted_mmr - best_collection[0].adjusted_mmr + cur_min = best_collection[-1].adjusted_mmr - \ + best_collection[0].adjusted_mmr # Find the collection of players with the least rating spread - for lowest_player_index, highest_player in enumerate(sorted_players[num_players:], 1): + for lowest_player_index, highest_player in enumerate( + sorted_players[num_players:], 1): lowest_player = sorted_players[lowest_player_index] cur_range = highest_player.adjusted_mmr - lowest_player.adjusted_mmr if cur_range < cur_min: @@ -106,41 +121,56 @@ def _minimize_range(players: List[Player], num_players: int) -> List[Player] | N lowest_player_index + num_players] return best_collection - def _all_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[List[Player]], int]: + def _all_room_final_list_algorithm(self, valid_players_check: Callable[[ + List[Player]], bool]) -> Tuple[List[List[Player]], int]: if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS all_confirmed_players = self.players_on_confirmed_teams() - first_late_player_index = (self.num_players // self.players_per_room) * self.players_per_room - on_time_players = sorted(all_confirmed_players[:first_late_player_index], reverse=True) + first_late_player_index = ( + self.num_players // self.players_per_room) * self.players_per_room + on_time_players = sorted( + all_confirmed_players[:first_late_player_index], reverse=True) late_players = all_confirmed_players[first_late_player_index:] - player_rooms: List[List[Player]] = list(common.divide_chunks(on_time_players, self.players_per_room)) + player_rooms: List[List[Player]] = list( + common.divide_chunks(on_time_players, self.players_per_room)) any_invalid = False for pr_index, player_room in enumerate(player_rooms, 0): - for lp_index in range(len(late_players)+1): + for lp_index in range(len(late_players) + 1): current_late_check_players = late_players[0:lp_index] - best_collection = Mogi._minimize_range(player_room + current_late_check_players, self.players_per_room) + best_collection = Mogi._minimize_range( + player_room + current_late_check_players, self.players_per_room) if valid_players_check(best_collection): best_collection.sort(reverse=True) - # Get all the players who were swapped out of the room and put them at the front of the late player list - swapped_out_players = [p for p in player_room if p not in best_collection] - swapped_in_players = [p for p in best_collection if p not in player_room] - late_players = swapped_out_players + list(filter(lambda p: p not in swapped_in_players, late_players)) + # Get all the players who were swapped out of the room and + # put them at the front of the late player list + swapped_out_players = [ + p for p in player_room if p not in best_collection] + swapped_in_players = [ + p for p in best_collection if p not in player_room] + late_players = swapped_out_players + \ + list( + filter(lambda p: p not in swapped_in_players, late_players)) player_rooms[pr_index] = best_collection break else: - # After checking all late players, no room player list with a valid range could be found for this room + # After checking all late players, no room player list with a + # valid range could be found for this room any_invalid = True - return player_rooms, (Mogi.ALGORITHM_STATUS_SUCCESS_SOME_INVALID if any_invalid else Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + return player_rooms, ( + Mogi.ALGORITHM_STATUS_SUCCESS_SOME_INVALID if any_invalid else Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) - def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Player]], bool]) -> Tuple[List[Player], int]: + def _one_room_final_list_algorithm(self, valid_players_check: Callable[[ + List[Player]], bool]) -> Tuple[List[Player], int]: if self.max_possible_rooms == 0: return [], Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS if self.max_possible_rooms > 1: confirmed_players = self.players_on_confirmed_teams() - return sorted(confirmed_players[0:self.players_per_room * self.max_possible_rooms], reverse=True), Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS - # At this point, we can only make one possible room, so our algorithm will be used + return sorted(confirmed_players[0:self.players_per_room * + self.max_possible_rooms], reverse=True), Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS + # At this point, we can only make one possible room, so our algorithm + # will be used confirmed_players = self.players_on_confirmed_teams() cur_check_list = list(confirmed_players[0:self.players_per_room]) late_players = list(confirmed_players[self.players_per_room:]) @@ -149,7 +179,8 @@ def _one_room_final_list_algorithm(self, valid_players_check: Callable[[List[Pla best_collection = Mogi._minimize_range( cur_check_list, self.players_per_room) if valid_players_check(best_collection): - return sorted(best_collection, reverse=True), Mogi.ALGORITHM_STATUS_SUCCESS_FOUND + return sorted( + best_collection, reverse=True), Mogi.ALGORITHM_STATUS_SUCCESS_FOUND if len(late_players) == 0: break cur_check_list.append(late_players.pop(0)) @@ -160,15 +191,21 @@ def _mk8dx_generate_final_list(self) -> List[Player]: confirmed_players = self.players_on_confirmed_teams() if self.max_possible_rooms == 0: return confirmed_players - return sorted(confirmed_players[0:self.players_per_room * self.max_possible_rooms], reverse=True) + return sorted( + confirmed_players[0:self.players_per_room * self.max_possible_rooms], reverse=True) - def _mkw_generate_final_list(self, valid_players_check: Callable[[List[Player]], bool]) -> List[Player]: - result, status = self._all_room_final_list_algorithm(valid_players_check) + def _mkw_generate_final_list(self, valid_players_check: Callable[[ + List[Player]], bool]) -> List[Player]: + result, status = self._all_room_final_list_algorithm( + valid_players_check) if status is Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS: return self.players_on_confirmed_teams() - return sorted(common.flatten(result), reverse=True) # I do not want to formally prove that sorting here is correct + # I do not want to formally prove that sorting here is correct + return sorted(common.flatten(result), reverse=True) - def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], bool] = None) -> List[Player]: + def generate_proposed_list(self, + valid_players_check: Callable[[List[Player]], + bool] = None) -> List[Player]: """Algorithm that generates a proposed list of players that will play. This algorithm may differ between MK8DX and MKW. The algorithm is allowed to propose any list of players it wants to. Among several possibilities, this allows the algorithm to change the order of the players in the returned list, add or remove players, @@ -183,16 +220,26 @@ def generate_proposed_list(self, valid_players_check: Callable[[List[Player]], b else: raise ValueError(f"Unknown server in config: {common.Server}") - def _count_cancelled(self, player_list: List[Player], valid_players_check: Callable[[List[Player]], bool] = None) -> int: + def _count_cancelled(self, + player_list: List[Player], + valid_players_check: Callable[[List[Player]], + bool] = None) -> int: """Using the provided valid players check function and player list, returns the number of rooms that would be cancelled.""" if valid_players_check is None: return 0 - return sum(1 for room_players in common.divide_chunks(player_list, self.players_per_room) if not valid_players_check(room_players)) - - def _any_cancelled(self, player_list: List[Player], valid_players_check: Callable[[List[Player]], bool] = None) -> bool: + return sum( + 1 for room_players in common.divide_chunks( + player_list, + self.players_per_room) if not valid_players_check(room_players)) + + def _any_cancelled(self, + player_list: List[Player], + valid_players_check: Callable[[List[Player]], + bool] = None) -> bool: return self._count_cancelled(player_list, valid_players_check) > 0 - def any_room_cancelled(self, valid_players_check: Callable[[List[Player]], bool] = None) -> bool: + def any_room_cancelled(self, valid_players_check: Callable[[ + List[Player]], bool] = None) -> bool: """Using the provided valid players check function, returns if any of the rooms will be cancelled.""" if valid_players_check is None: return False @@ -216,7 +263,8 @@ def players_on_confirmed_teams(self) -> List[Player]: return flatten([team.players for team in self.confirmed_teams()]) def all_room_channel_ids(self) -> Set[int]: - return {r.channel.id for r in self.rooms if r is not None and r.channel is not None} + return { + r.channel.id for r in self.rooms if r is not None and r.channel is not None} def channel_id_in_rooms(self, channel_id: int): return channel_id in self.all_room_channel_ids() @@ -241,15 +289,20 @@ async def populate_host_fcs(self): async def assign_roles(self, guild: discord.Guild): for room in self.rooms: try: - to_skip = None if room.room_role is None else {room.room_role.id} + to_skip = None if room.room_role is None else { + room.room_role.id} await room.assign_roles(guild=guild, role_skip=to_skip) except RoleAddFailure: pass - class Room: - def __init__(self, teams, room_num: int, channel: discord.Thread | discord.TextChannel, tier_info): + def __init__( + self, + teams, + room_num: int, + channel: discord.Thread | discord.TextChannel, + tier_info): self.teams: List["Team"] = teams self.room_num = room_num self.channel = channel @@ -342,8 +395,9 @@ async def assign_member_room_role(self, member: discord.Member): raise RoleNotFound(f"Could not find role for tier {self.tier}") try: await member.add_roles(self.room_role) - except: - raise RoleAddFailure(f"Failed to add role {self.room_role.name} to {member}\n") + except BaseException: + raise RoleAddFailure( + f"Failed to add role {self.room_role.name} to {member}\n") async def assign_roles(self, guild: discord.Guild, role_skip=None): role_add_fail_text = "" @@ -367,7 +421,7 @@ async def assign_roles(self, guild: discord.Guild, role_skip=None): async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi | None]): if common.CONFIG["USE_THREADS"]: return - + # Find the available tier channels for the tier (collection) tier_collection = self.tier_collection tier_data = common.CONFIG["TIER_CHANNELS"][tier_collection] @@ -380,20 +434,24 @@ async def prepare_room_channel(self, guild: discord.Guild, all_events: List[Mogi free_tier_channel_ids.remove(channel_id) # If there are no available tier channels, raise an exception if len(free_tier_channel_ids) == 0: - raise NoFreeChannels(f"No free channels for tier {tier_collection}") - + raise NoFreeChannels( + f"No free channels for tier {tier_collection}") + # Get the discord.GuildChannel of the first available channel id found_channel = guild.get_channel(free_tier_channel_ids[0]) if not isinstance(found_channel, discord.TextChannel): - raise WrongChannelType(f"For tier {tier_collection}, channel id {channel_id} is of type {type(found_channel)}, expected discord.TextChannel") + raise WrongChannelType( + f"For tier {tier_collection}, channel id {channel_id} is of type {type(found_channel)}, expected discord.TextChannel") self.channel = found_channel - # Assign the role for the tier collection to all players in the room so they can see the tier + # Assign the role for the tier collection to all players in the room so + # they can see the tier tier_role_id = tier_data["tier_role_id"] self.room_role = guild.get_role(tier_role_id) if self.room_role is None: - raise RoleNotFound(f"Could not find role for role id {tier_role_id} for tier {tier_collection}") - await self.assign_roles(guild=guild, role_skip=tier_data["role_ids_can_see_already"]+[tier_role_id]) + raise RoleNotFound( + f"Could not find role for role id {tier_role_id} for tier {tier_collection}") + await self.assign_roles(guild=guild, role_skip=tier_data["role_ids_can_see_already"] + [tier_role_id]) class Team: @@ -445,7 +503,13 @@ def __str__(self): class Player: - def __init__(self, member: discord.Member, lounge_name: str, mmr: int, confirmed=False, host=False): + def __init__( + self, + member: discord.Member, + lounge_name: str, + mmr: int, + confirmed=False, + host=False): self.member = member self.lounge_name = lounge_name self.mmr = mmr @@ -485,7 +549,14 @@ def __lt__(self, other: Player): class VoteView(View): - def __init__(self, players, thread, mogi: Mogi, room: Room, penalty_time: int, room_start_msg_link): + def __init__( + self, + players, + thread, + mogi: Mogi, + room: Room, + penalty_time: int, + room_start_msg_link): super().__init__() self.players: List[Player] = players self.room_channel: discord.Thread | discord.TextChannel = thread @@ -542,7 +613,8 @@ async def make_teams(self, players_per_team: int, vote_str: str): teams = [] teams_per_room = 12 // players_per_team for j in range(teams_per_room): - team = Team(self.players[j * players_per_team:(j + 1) * players_per_team]) + team = Team( + self.players[j * players_per_team:(j + 1) * players_per_team]) teams.append(team) teams.sort(key=lambda team: team.avg_mmr, reverse=True) @@ -590,7 +662,8 @@ async def make_teams(self, players_per_team: int, vote_str: str): else: cur_time = datetime.now(timezone.utc) mkw_room_open_time = cur_time + timedelta(minutes=1) - pen_time = mkw_room_open_time + timedelta(minutes=self.penalty_time) + pen_time = mkw_room_open_time + \ + timedelta(minutes=self.penalty_time) msg += f"\nRoom open at :{mkw_room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" room.teams = teams @@ -598,7 +671,8 @@ async def make_teams(self, players_per_team: int, vote_str: str): self.found_winner = True await self.room_channel.send(msg) if common.CONFIG["USE_THREADS"]: - new_thread_name = self.room_channel.name + f" - {tier_text}{room.tier}" + new_thread_name = self.room_channel.name + \ + f" - {tier_text}{room.tier}" await self.room_channel.edit(name=new_thread_name) async def find_winner(self): @@ -613,7 +687,8 @@ async def find_winner(self): winners.append((3, "3v3")) if len(self.votes["4v4"]) == most_votes: winners.append((4, "4v4")) - if common.SERVER is common.Server.MKW and len(self.votes["6v6"]) == most_votes: + if common.SERVER is common.Server.MKW and len( + self.votes["6v6"]) == most_votes: winners.append((6, "6v6")) winner = random.choice(winners) @@ -655,7 +730,13 @@ async def general_vote_callback(self, interaction: discord.Interaction): class JoinView(View): - def __init__(self, room: Room, get_rating_from_discord_id, sub_range_mmr_allowance, bottom_room_num, is_restricted: Callable[[discord.User | discord.Member], bool] | None = None): + def __init__(self, + room: Room, + get_rating_from_discord_id, + sub_range_mmr_allowance, + bottom_room_num, + is_restricted: Callable[[discord.User | discord.Member], + bool] | None = None): super().__init__(timeout=1200) self.room = room self.get_rating_from_discord_id = get_rating_from_discord_id @@ -665,7 +746,8 @@ def __init__(self, room: Room, get_rating_from_discord_id, sub_range_mmr_allowan @discord.ui.button(label="Join Room") async def button_callback(self, interaction: discord.Interaction, button): - if self.is_restricted is not None and self.is_restricted(interaction.user): + if self.is_restricted is not None and self.is_restricted( + interaction.user): await interaction.response.send_message( "Players with the muted or restricted role cannot use the sub button.", ephemeral=True) return @@ -677,14 +759,15 @@ async def button_callback(self, interaction: discord.Interaction, button): user_mmr = self.get_rating_from_discord_id(interaction.user.id) if isinstance(user_mmr, int): user_mmr = Player(None, "", user_mmr).adjusted_mmr - except: + except BaseException: await interaction.response.send_message( "MMR lookup for player has failed, please try again.", ephemeral=True) return mmr_high = 999999 if self.room.room_num == 1 else self.room.mmr_high mmr_low = -999999 if self.room.room_num == self.bottom_room_num else self.room.mmr_low - if isinstance(user_mmr, int) and mmr_high + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: + if isinstance(user_mmr, int) and mmr_high + \ + self.sub_range_mmr_allowance > user_mmr > mmr_low - self.sub_range_mmr_allowance: self.room.subs.append(interaction.user.id) button.disabled = True await interaction.response.edit_message(view=self) diff --git a/requirements.txt b/requirements.txt index bfbed65..2792814 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,6 @@ aiohttp==3.8.4 discord.py==2.3.2 python-dateutil==2.8.2 dill~=0.3.8 +autopep8==1.6.0 +pycodestyle==2.8.0 + diff --git a/unit_testing.py b/unit_testing.py index 5c4f935..1435166 100644 --- a/unit_testing.py +++ b/unit_testing.py @@ -130,11 +130,13 @@ def test_correct_collection_6(self): p14 = Player(None, "14", 1501) p15 = Player(None, "15", 502) p16 = Player(None, "16", 100) - players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] + players = [p1, p2, p3, p4, p5, p6, p7, p8, + p9, p10, p11, p12, p13, p14, p15, p16] random.shuffle(players) num_players = 12 result = Mogi._minimize_range(players, num_players) - self.assertListEqual([p16, p11, p2, p9, p15, p10, p4, p5, p1, p3, p14, p12], result) + self.assertListEqual( + [p16, p11, p2, p9, p15, p10, p4, p5, p1, p3, p14, p12], result) def test_correct_collection_7(self): """Test #7 for large player collection where 12 players are given and 12 are requested""" @@ -164,11 +166,14 @@ def setUpClass(cls): cls.threshold = 65 cls.threshold_function = None if common.SERVER is common.Server.MKW: - cls.threshold_function = lambda players: SquadQueue.mkw_players_allowed(players, cls.threshold) + cls.threshold_function = lambda players: SquadQueue.mkw_players_allowed( + players, cls.threshold) elif common.SERVER is common.Server.MK8DX: - cls.threshold_function = lambda players: SquadQueue.mk8dx_players_allowed(players, cls.threshold) + cls.threshold_function = lambda players: SquadQueue.mk8dx_players_allowed( + players, cls.threshold) else: - raise Exception("Lounge parameter in config is invalid, unit tests will fail.") + raise Exception( + "Lounge parameter in config is invalid, unit tests will fail.") def _new_default_mogi(self) -> Mogi: """Returns a default mogi with 12 players per room.""" @@ -182,18 +187,24 @@ def _new_default_mogi(self) -> Mogi: def test_no_players(self): """Test when no players are queued""" mogi = self._new_default_mogi() - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertListEqual([], results) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) def test_one_player(self): """Test when one player is queued""" p1 = Player(None, "1", 1000, True) mogi = self._new_default_mogi() mogi.teams.append(Team([p1])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertListEqual([], results) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) def test_two_players(self): """Test when two players are queued""" @@ -202,9 +213,12 @@ def test_two_players(self): mogi = self._new_default_mogi() mogi.teams.append(Team([p1])) mogi.teams.append(Team([p2])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertListEqual([], results) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) def test_large_max_players_per_team(self): """This is to catch the Mogi class computes the number of confirmed players incorrectly""" @@ -212,9 +226,12 @@ def test_large_max_players_per_team(self): mogi = self._new_default_mogi() mogi.max_player_per_team = 24 mogi.teams.append(Team([p1])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertListEqual([], results) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_INSUFFICIENT_PLAYERS) def test_2_rooms_correctness_1(self): """Test if 24P for 12 player rooms generates 2 rooms with correct status code""" @@ -222,9 +239,12 @@ def test_2_rooms_correctness_1(self): mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertSetEqual(set(players), set(results)) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) def test_2_rooms_correctness_2(self): """Test if 25P for 12 player rooms generates 2 rooms with correct players and correct status code""" @@ -233,46 +253,68 @@ def test_2_rooms_correctness_2(self): mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertSetEqual(set(players[0:24]), set(results)) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) def test_2_rooms_correctness_3(self): """Test if 25P for 12 player rooms generates 2 rooms with correct players and correct status code even with extremely large range""" - players = [Player(None, f"{i*10000}", i*10000, True) for i in range(25)] + players = [ + Player( + None, + f"{i*10000}", + i * 10000, + True) for i in range(25)] random.shuffle(players) mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) self.assertSetEqual(set(players[0:24]), set(results)) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_2_OR_MORE_ROOMS) def test_1_room_correctness_1(self): """Test if 15P for 12 player rooms generates 1 rooms with correct players and correct status code""" assert self.threshold == 65 - players = [Player(None, f"{i}", abs(i-8)*(i-8), True) for i in range(17)] # -64, -49, -36, ... 0, 1, 4, ... 64 - players.pop(15) # Set 36 rating to not confirmed so only one possibility occurs - correct_result = players[2:14].copy() # Only one possibility that matches threshold + # -64, -49, -36, ... 0, 1, 4, ... 64 + players = [Player(None, f"{i}", abs( + i - 8) * (i - 8), True) for i in range(17)] + # Set 36 rating to not confirmed so only one possibility occurs + players.pop(15) + # Only one possibility that matches threshold + correct_result = players[2:14].copy() random.shuffle(players) mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) self.assertSetEqual(set(correct_result), set(results)) def test_2_room_correctness_2(self): """Test if 15P for 12 player rooms does not find any rooms when players are beyond range""" assert self.threshold == 65 - players = [Player(None, f"{i}", (i-8)**3, True) for i in range(17)] # -512, -343, ... 0, 1, 8, ... 512 + # -512, -343, ... 0, 1, 8, ... 512 + players = [Player(None, f"{i}", (i - 8)**3, True) for i in range(17)] correct_result = [] # No possible list of players random.shuffle(players) mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) self.assertSetEqual(set(correct_result), set(results)) def test_2_room_correctness_3(self): @@ -294,14 +336,19 @@ def test_2_room_correctness_3(self): p14 = Player(None, "14", 74, True) p15 = Player(None, "15", 9, True) p16 = Player(None, "16", 100, True) - # p4 should be removed, p10 should be removed, p13 should be added, p15 should be added - players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] + # p4 should be removed, p10 should be removed, p13 should be added, p15 + # should be added + players = [p1, p2, p3, p4, p5, p6, p7, p8, + p9, p10, p11, p12, p13, p14, p15, p16] correct_result = [p1, p2, p3, p5, p6, p7, p8, p9, p11, p12, p13, p15] mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_FOUND) self.assertSetEqual(set(correct_result), set(results)) def test_2_room_correctness_4(self): @@ -323,17 +370,22 @@ def test_2_room_correctness_4(self): p14 = Player(None, "14", 74, True) p15 = Player(None, "15", 9, False) p16 = Player(None, "16", 100, True) - players = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16] - correct_result = [] # Same as previous test, but since p15 is not confirmed, no result is found + players = [p1, p2, p3, p4, p5, p6, p7, p8, + p9, p10, p11, p12, p13, p14, p15, p16] + # Same as previous test, but since p15 is not confirmed, no result is + # found + correct_result = [] mogi = self._new_default_mogi() for p in players: mogi.teams.append(Team([p])) - results, status_code = mogi._one_room_final_list_algorithm(OneRoomAlgorithmTests.threshold_function) - self.assertEqual(status_code, SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) + results, status_code = mogi._one_room_final_list_algorithm( + OneRoomAlgorithmTests.threshold_function) + self.assertEqual( + status_code, + SquadQueue.Mogi.ALGORITHM_STATUS_SUCCESS_EMPTY) self.assertSetEqual(set(correct_result), set(results)) - if __name__ == "__main__": """If PyCharm, add --verbose flag to configuration. Other IDEs, run from shell: python -m unittest ./unit_testing.py --verbose""" From 0ebaa7bbe3f08f1727965e2a52071d3957d262c4 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 10 Aug 2024 13:37:06 -0700 Subject: [PATCH 253/292] Initial forced format feature commit --- cogs/SquadQueue.py | 383 +++++++++++++++++++++++++++++++++++++++------ mogi_objects.py | 6 +- requirements.txt | 1 + timezones.json | 162 +++++++++++++++---- 4 files changed, 465 insertions(+), 87 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index bd7956e..d307c99 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -5,6 +5,7 @@ from discord.ext import commands, tasks from discord import app_commands from datetime import datetime, timezone, timedelta +import pytz import time import json @@ -15,7 +16,7 @@ from mogi_objects import Mogi, Team, Player, Room, VoteView, JoinView import asyncio from collections import defaultdict -from typing import Dict, List, Tuple +from typing import Optional, Dict, List, Tuple import traceback import os import dill @@ -161,7 +162,9 @@ def __init__(self, bot): self.old_events: List[Mogi] = [] - self.sq_times = [] + self.sq_times: List[datetime] = [] + + self.forced_format_times: List[Tuple[datetime, str]] = [] # Parameters for tracking if we should send an extension message or not self.last_extension_message_timestamp = datetime.now( @@ -177,6 +180,8 @@ def __init__(self, bot): self.list_messages = [] + self.format_messages = [] + self.LAUNCH_NEW_EVENTS = True self.GUILD: discord.Guild = None @@ -312,9 +317,10 @@ async def on_ready(self): self.bot.config["queue_history_channel"]) self.GENERAL_CHANNEL = self.bot.get_channel( self.bot.config["queue_general_channel"]) - if self.bot.config["queue_schedule_channel"]: + schedule_channel_id = self.bot.config["queue_schedule_channel"] + if schedule_channel_id: self.SCHEDULE_CHANNEL = self.bot.get_channel( - self.bot.config["queue_schedule_channel"]) + schedule_channel_id) if common.SERVER is common.Server.MKW: await self.get_ladder_info() try: @@ -327,6 +333,13 @@ async def on_ready(self): except BaseException: print("Purging Sub channel failed", flush=True) print(traceback.format_exc()) + try: + if schedule_channel_id: + await self.SCHEDULE_CHANNEL.purge() + await self.update_forced_format_list() + except BaseException: + print("Purging Schedule channel failed", flush=True) + print(traceback.format_exc()) settings_group = SettingsGroup( self, @@ -339,6 +352,7 @@ async def on_ready(self): print(f"List Channel - {self.LIST_CHANNEL}", flush=True) print(f"History Channel - {self.HISTORY_CHANNEL}", flush=True) print(f"General Channel - {self.GENERAL_CHANNEL}", flush=True) + print(f"Schedule Channel - {self.SCHEDULE_CHANNEL}", flush=True) print("Ready!", flush=True) self.refresh_ratings.start() self.refresh_helper_roles.start() @@ -529,9 +543,11 @@ async def join_queue(self, interaction: discord.Interaction, host=False): squad = Team(players) mogi.teams.append(squad) host_str = " as a host " if host else " " - msg += f"{players[0].lounge_name} joined queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" - if players[0].is_matchmaking_mmr_adjusted: - msg += f"\nPlayer considered {players[0].adjusted_mmr} MMR for matchmaking purposes." + format_str = f"the {mogi.format} " if mogi.format else "" + msg += f"{players[0].lounge_name} joined {format_str}queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + if common.SERVER is common.Server.MKW: + if players[0].is_matchmaking_mmr_adjusted: + msg += f"\nPlayer considered {players[0].adjusted_mmr} MMR for matchmaking purposes." event_status_launched = self.check_close_event_change() try: @@ -648,7 +664,7 @@ async def sub(self, interaction: discord.Interaction, races_left: app_commands.R @tasks.loop(seconds=30) async def list_task(self): - """Continually display the list of confirmed players for a mogi in the history channel""" + """Continually display the list of confirmed players for a mogi in the list channel""" mogi = self.ongoing_event if mogi is not None: if not mogi.gathering: @@ -662,7 +678,8 @@ async def list_task(self): late_players = all_confirmed_players[first_late_player_index:] msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" - msg += "**Current Mogi List:**\n" + format_str = f"{mogi.format} " if mogi.format else "" + msg += f"**Current {format_str}Mogi List:**\n" if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" @@ -723,6 +740,66 @@ async def delete_list_messages(self, new_list_size: int): except Exception as e: print(traceback.format_exc()) + async def update_forced_format_list(self): + """Update the list of Mogis with scheduled formats""" + + if self.SCHEDULE_CHANNEL: + msg = "" + + if common.SERVER is common.Server.MKW: + msg += "**Daily Queue Times:**\n\n" + + curr_time = self.compute_next_event_open_time() + self.JOINING_TIME + \ + self.DISPLAY_OFFSET_MINUTES + cutoff_time = curr_time + timedelta(hours=24) + + while curr_time < cutoff_time: + msg += f"{discord.utils.format_dt(curr_time, style='t')}\n" + curr_time += self.QUEUE_OPEN_TIME + + msg += "\n" + + msg += "**List of Mogis with Scheduled Formats:**\n\n" + for index, event in enumerate(self.forced_format_times): + msg += f"{index + 1}) {discord.utils.format_dt(event[0])} - {event[1]}\n" + message = msg.split("\n") + + new_messages = [] + bulk_msg = "" + for i in range(len(message)): + if len(bulk_msg + message[i] + "\n") > 2000: + new_messages.append(bulk_msg) + bulk_msg = "" + bulk_msg += message[i] + "\n" + if len(bulk_msg) > 0 and bulk_msg != "\n": + new_messages.append(bulk_msg) + + await self.delete_format_messages(len(new_messages)) + + try: + for i, message in enumerate(new_messages): + if i < len(self.format_messages): + old_message = self.format_messages[i] + await old_message.edit(content=message) + else: + new_message = await self.SCHEDULE_CHANNEL.send(message) + self.format_messages.append(new_message) + except BaseException: + await self.delete_format_messages(0) + for i, message in enumerate(new_messages): + new_message = await self.SCHEDULE_CHANNEL.send(message) + self.format_messages.append(new_message) + + async def delete_format_messages(self, new_list_size: int): + try: + messages_to_delete = [] + while len(self.format_messages) > new_list_size: + messages_to_delete.append(self.format_messages.pop()) + if self.SCHEDULE_CHANNEL and len(messages_to_delete) > 0: + await self.SCHEDULE_CHANNEL.delete_messages(messages_to_delete) + except Exception as e: + print(traceback.format_exc()) + @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot or not ( @@ -941,57 +1018,217 @@ async def reset_bot(self, interaction: discord.Interaction): self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("All events have been deleted. Queue will restart shortly.") - @app_commands.command(name="schedule_sq_times") - @app_commands.guild_only() - async def schedule_sq_times(self, interaction: discord.Interaction, date: str): - """Schedule Squad Queue times. Staff use only.""" - await interaction.response.send_message(f"Testing date {date}") + async def timezone_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + current_upper = current.upper() + now = datetime.now(pytz.utc) + active_timezones = [] + + for tz, info in self.timezones.items(): + full_timezone_name = info['zone_name'] + tz_info = pytz.timezone(full_timezone_name) + local_now = now.astimezone(tz_info) - @schedule_sq_times.autocomplete("date") - async def date_autocomplete(ctx: discord.AutocompleteContext): - today = datetime.now().date() + if local_now.utcoffset().total_seconds() / 3600 == info['offset']: + if current_upper in tz.upper(): + active_timezones.append((tz, info['offset'])) + + active_timezones.sort(key=lambda x: (x[1], x[0])) + + choices = [ + app_commands.Choice(name=f"{tz} (UTC{offset:+})", value=tz) + for tz, offset in active_timezones + ] + + return choices[:25] + + async def date_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + timezone_choice = pytz.timezone("Etc/UTC") + + if 'options' in interaction.data: + for option in interaction.data['options']: + if option['name'] == 'tzone': + abbreviation = option['value'] + timezone_info = self.timezones.get(abbreviation, {}) + full_timezone_name = timezone_info.get( + "zone_name", "Etc/UTC") + timezone_choice = pytz.timezone(full_timezone_name) + break + + today = datetime.now(timezone_choice) + self.QUEUE_OPEN_TIME + today_date = today.date() options = [] for i in range(25): - date = today + timedelta(days=i) - options.append(discord.OptionChoice( - name=date.isoformat(), value=date.isoformat())) + date = today_date + timedelta(days=i) + name = date.strftime("%m/%d") + value = date.isoformat() + options.append(app_commands.Choice(name=name, value=value)) + return options - @commands.command(name="schedule_sq_times") - @commands.guild_only() - async def schedule_sq_times(self, ctx, timestamps: commands.Greedy[int]): - """Saves a list of sq times to skip over. Input a list of unix utc timestamps. Staff use only.""" - if not self.is_staff(ctx.author) and not await self.bot.is_owner(ctx.author): - await self.queue_or_send( - ctx, "You do not have permission to use this command.") - return + def compute_first_event_of_date_open_time(self, date): + time_elapsed = date - self.FIRST_EVENT_TIME + num_launched_events = time_elapsed // self.QUEUE_OPEN_TIME + next_event_open_time = self.FIRST_EVENT_TIME + \ + (self.QUEUE_OPEN_TIME * num_launched_events) + return next_event_open_time - msg = "The queue will skip events THAT START AT THE TIMESTAMP (not when the queue pops)\n" - msg += "List of new Dates:\n" + async def time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + date = None + timezone_choice = pytz.timezone("Etc/UTC") + timezone_offset = 0 + + if 'options' in interaction.data: + for option in interaction.data['options']: + if option['name'] == 'date': + date = option['value'] + if option['name'] == 'tzone': + abbreviation = option['value'] + timezone_info = self.timezones.get(abbreviation, {}) + full_timezone_name = timezone_info.get( + "zone_name", "Etc/UTC") + timezone_offset = timezone_info.get( + "offset", 0) + timezone_choice = pytz.timezone(full_timezone_name) + + time_options = [] + if date and timezone_choice: + try: + selected_date = datetime.fromisoformat(date) + selected_date = selected_date.astimezone( + timezone.utc) - timedelta(hours=timezone_offset) + + curr_time = datetime.now( + timezone.utc) + curr_date = self.compute_first_event_of_date_open_time( + selected_date) + cutoff_date = selected_date + timedelta(hours=24) + + while curr_date < cutoff_date and len(time_options) < 25: + if curr_date > curr_time: + adjusted_date = curr_date + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES + tz_adjusted_date = adjusted_date + \ + timedelta(hours=timezone_offset) + dt_str = adjusted_date.isoformat() + name = tz_adjusted_date.strftime( + f"%m/%d - %H:%M:%S") + time_options.append( + app_commands.Choice(name=name, value=dt_str)) + curr_date += self.QUEUE_OPEN_TIME + + except ValueError as e: + print(f"Error parsing date: {e}", flush=True) + + else: + time_options = [f"{hour:02d}:00" for hour in range(24)] + return [app_commands.Choice(name=time, value=time) for time in time_options] + + return time_options + + async def format_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + options = ["FFA", "2v2", "3v3", "4v4", "6v6"] + return [app_commands.Choice(name=option, value=option) for option in options] + + @app_commands.command(name="schedule_forced_format_times") + @app_commands.autocomplete(tzone=timezone_autocomplete) + @app_commands.autocomplete(date=date_autocomplete) + @app_commands.autocomplete(time=time_autocomplete) + @app_commands.autocomplete(format=format_autocomplete) + @app_commands.guild_only() + async def schedule_forced_format_times(self, interaction: discord.Interaction, tzone: str, date: str, time: str, format: str): + """Schedule Squad Queue times. Staff use only.""" curr_time = datetime.now(timezone.utc) - new_sq_dates = [] + dt = datetime.fromisoformat(time).replace(tzinfo=timezone.utc) + timestamp = dt.timestamp() + + if curr_time > dt: + msg = "" + msg += f"Timestamp {timestamp} represents {time} and is in the past, submit a future date.\n" + msg += "This timestamp has not been added." + await interaction.response.send_message(msg) + return - for timestamp in timestamps: - date = datetime.fromtimestamp(timestamp, timezone.utc) - truncated_date = date.replace( - minute=0, second=0, microsecond=0) + new_event = (dt, format) + event_dict = {event[0]: event for event in self.forced_format_times} + event_dict[new_event[0]] = new_event + updated_list = list(event_dict.values()) + self.forced_format_times = sorted(updated_list, key=lambda x: x[0]) - if curr_time > truncated_date: - msg = "" - msg += f"Timestamp {timestamp} represents {truncated_date} and is in the past, submit a future date.\n" - msg += "No timestamps from this usage of the command have been added." - await self.queue_or_send(ctx, msg) - return + if self.SCHEDULE_CHANNEL: + await self.update_forced_format_list() - new_sq_dates.append(truncated_date) - msg += f"{truncated_date}\n" + msg = f"Added new {format} mogi at {discord.utils.format_dt(dt)}" - self.sq_times += new_sq_dates - self.sq_times = list(set(self.sq_times)) + await interaction.response.send_message(msg) + + async def remove_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + choices = [ + app_commands.Choice( + name=f"{dt.strftime('%Y-%m-%d %H:%M:%S')} - {fmt}", value=dt.isoformat() + ) + for dt, fmt in self.forced_format_times[:25] + ] + return choices + + @app_commands.command(name="remove_forced_format_time") + @app_commands.autocomplete(time=remove_time_autocomplete) + async def remove_forced_format_time(self, interaction: discord.Interaction, time: str): + """Remove a specific forced format time by datetime. Staff use only.""" + try: + dt_to_remove = datetime.fromisoformat(time) + + original_count = len(self.forced_format_times) + self.forced_format_times = [ + entry for entry in self.forced_format_times if entry[0] != dt_to_remove + ] + + if len(self.forced_format_times) < original_count: + await interaction.response.send_message(f"Removed {discord.utils.format_dt(dt_to_remove, 'f')} from the Forced Format times.") + else: + await interaction.response.send_message(f"No entry found for {time}.") + + if self.SCHEDULE_CHANNEL: + await self.update_forced_format_list() + except ValueError: + await interaction.response.send_message("Invalid datetime format provided.") + @app_commands.command(name="clear_forced_format_times") + @app_commands.guild_only() + async def clear_forced_format_times(self, interaction: discord.Interaction): + """Clears current list of forced format times. Staff use only.""" + self.forced_format_times = [] + if self.SCHEDULE_CHANNEL: + await self.update_forced_format_list() + + await interaction.response.send_message("Cleared list of Forced Format Times.") + + @app_commands.command(name="schedule_sq_times") + @app_commands.autocomplete(tzone=timezone_autocomplete) + @app_commands.autocomplete(date=date_autocomplete) + @app_commands.autocomplete(time=time_autocomplete) + @app_commands.guild_only() + async def schedule_sq_times(self, interaction: discord.Interaction, tzone: str, date: str, time: str): + """Schedule Squad Queue times. Staff use only.""" + curr_time = datetime.now(timezone.utc) + dt = datetime.fromisoformat(time).replace(tzinfo=timezone.utc) + timestamp = dt.timestamp() + + if curr_time > dt: + msg = "" + msg += f"Timestamp {timestamp} represents {time} and is in the past, submit a future date.\n" + msg += "This timestamp has not been added." + await interaction.response.send_message(msg) + return + + self.sq_times.append(dt) + self.sq_times = list(set(self.sq_times)) list.sort(self.sq_times) - await self.queue_or_send(ctx, msg) + msg = "List of Squad Queue Times:\n" + + for index, date in enumerate(self.sq_times): + msg += f"{index + 1}) {discord.utils.format_dt(date)}\n" + + await interaction.response.send_message(msg) @app_commands.command(name="peek_sq_times") @app_commands.guild_only() @@ -1004,6 +1241,26 @@ async def peek_sq_times(self, interaction: discord.Interaction): await interaction.response.send_message(msg) + async def sq_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + choices = [ + app_commands.Choice(name=dt.strftime( + "%Y-%m-%d %H:%M:%S"), value=dt.isoformat()) + for dt in self.sq_times[:25] + ] + return choices + + @app_commands.command(name="remove_sq_time") + @app_commands.autocomplete(time=sq_time_autocomplete) + async def remove_sq_time(self, interaction: discord.Interaction, time: str): + """Remove a specific Squad Queue time. Staff use only.""" + dt_to_remove = datetime.fromisoformat(time) + + if dt_to_remove in self.sq_times: + self.sq_times.remove(dt_to_remove) + await interaction.response.send_message(f"Removed {discord.utils.format_dt(dt_to_remove, 'f')} from the Squad Queue times.") + else: + await interaction.response.send_message("Specified time not found in the Squad Queue times.", ephemeral=True) + @app_commands.command(name="clear_sq_times") @app_commands.guild_only() async def clear_sq_times(self, interaction: discord.Interaction): @@ -1065,7 +1322,17 @@ async def end_voting(mogi: Mogi): f"Skipping room {index} in function end_voting.", flush=True) continue - await room.view.find_winner() + if mogi.format == "FFA": + await room.view.make_teams(1, "FFA") + elif mogi.format == "2v2": + await room.view.make_teams(2, "2v2") + elif mogi.format == "3v3": + await room.view.make_teams(3, "3v3") + elif mogi.format == "4v4": + await room.view.make_teams(4, "4v4") + else: + await room.view.find_winner() + except Exception as e: print(traceback.format_exc()) @@ -1074,7 +1341,8 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): """Writes the teams, tier and average of each room per hour.""" try: if mogi is not None: - await history_channel.send(f"{discord.utils.format_dt(mogi.display_time)} Rooms") + format_str = f"{mogi.format} " if mogi.format else "" + await history_channel.send(f"{discord.utils.format_dt(mogi.display_time)} {format_str}Rooms") for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( @@ -1126,7 +1394,8 @@ async def check_room_threads(self, mogi: Mogi): async def handle_voting_and_history(mogi: Mogi, history_channel: discord.TextChannel): # We could have used asyncio.call_later(120, handle_voting_and_history) # in the caller's code - await asyncio.sleep(120) + if not mogi.format: + await asyncio.sleep(120) await SquadQueue.end_voting(mogi) await SquadQueue.write_history(mogi, history_channel) @@ -1266,6 +1535,9 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic self.ongoing_event.assign_roles(guild=self.GUILD)) asyncio.create_task(SquadQueue.handle_voting_and_history( self.ongoing_event, self.HISTORY_CHANNEL)) + if mogi.format: + if self.SCHEDULE_CHANNEL: + await self.update_forced_format_list() self.old_events.append(self.ongoing_event) self.ongoing_event = None @@ -1376,8 +1648,9 @@ async def scheduler_mogi_start(self): self.ongoing_event.started = True self.ongoing_event.gathering = True await self.unlockdown(self.ongoing_event.mogi_channel) + format_str = f"{self.ongoing_event.format} " if self.ongoing_event.format else "" await self.ongoing_event.mogi_channel.send( - f"A queue is gathering for the mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") + f"A queue is gathering for the {format_str}mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") @tasks.loop(seconds=20.0) async def sqscheduler(self): @@ -1423,8 +1696,13 @@ async def schedule_que_event(self): # it's joining period and during its extension period if next_event_start_time < datetime.now(timezone.utc): return + format = None + if len( + self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: + format = self.forced_format_times[0][1] + self.forced_format_times.pop(0) if len( - self.sq_times) > 0 and next_event_open_time == self.sq_times[0]: + self.sq_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.sq_times[0]: self.sq_times.pop(0) self.next_event = None self.ongoing_event = None @@ -1444,7 +1722,8 @@ async def schedule_que_event(self): mogi_channel=self.MOGI_CHANNEL, is_automated=True, start_time=next_event_start_time, - display_time=next_event_display_time) + display_time=next_event_display_time, + format=format) print(f"Started Queue for {next_event_start_time}", flush=True) diff --git a/mogi_objects.py b/mogi_objects.py index 08f18cb..35d150c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -56,7 +56,8 @@ def __init__( is_automated=False, start_time=None, display_time=None, - additional_extension_minutes=0): + additional_extension_minutes=0, + format=None): self.started = False self.gathering = False self.making_rooms_run = False @@ -72,6 +73,7 @@ def __init__( self.display_time = display_time if is_automated else None self.additional_extension = timedelta( minutes=additional_extension_minutes) + self.format = format self.has_checked_auto_extend = False @property @@ -632,7 +634,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): if common.SERVER is common.Server.MK8DX: msg += f"\nTable: `/scoreboard`\n" - msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n\n" + msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n" elif common.SERVER is common.Server.MKW and vote_str == "6v6": captain_1 = teams[0].players[0] captain_2 = teams[1].players[0] diff --git a/requirements.txt b/requirements.txt index 2792814..9cb79ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ aiohttp==3.8.4 discord.py==2.3.2 python-dateutil==2.8.2 dill~=0.3.8 +pytz==2024.1 autopep8==1.6.0 pycodestyle==2.8.0 diff --git a/timezones.json b/timezones.json index 3ae77d6..5a94039 100644 --- a/timezones.json +++ b/timezones.json @@ -1,34 +1,130 @@ { - "ACDT": 10.5, - "ACST": 9.5, - "ADT": -3, - "AEDT": 11, - "AEST": 10, - "AKDT": -8, - "AKST": -9, - "AST": -4, - "AWDT": 9, - "AWST": 8, - "BST": 1, - "CDT": -5, - "CEST": 2, - "CET": 1, - "CLST": -3, - "CLT": -4, - "CST": -6, - "EDT": -4, - "EST": -5, - "GMT": 0, - "HDT": -9, - "HST": -8, - "IST": 5.5, - "JST": 9, - "KST": 9, - "MDT": -6, - "MST": -7, - "NDT": -2.5, - "NST": -3.5, - "PDT": -7, - "PST": -8, - "UTC": 0 -} \ No newline at end of file + "ACDT": { + "offset": 10.5, + "zone_name": "Australia/Adelaide" + }, + "ACST": { + "offset": 9.5, + "zone_name": "Australia/Adelaide" + }, + "ADT": { + "offset": -3, + "zone_name": "America/Halifax" + }, + "AEDT": { + "offset": 11, + "zone_name": "Australia/Sydney" + }, + "AEST": { + "offset": 10, + "zone_name": "Australia/Sydney" + }, + "AKDT": { + "offset": -8, + "zone_name": "America/Anchorage" + }, + "AKST": { + "offset": -9, + "zone_name": "America/Anchorage" + }, + "AST": { + "offset": -4, + "zone_name": "America/Anchorage" + }, + "AWDT": { + "offset": 9, + "zone_name": "Australia/Perth" + }, + "AWST": { + "offset": 8, + "zone_name": "Australia/Perth" + }, + "BST": { + "offset": 1, + "zone_name": "Europe/London" + }, + "CDT": { + "offset": -5, + "zone_name": "America/Chicago" + }, + "CEST": { + "offset": 2, + "zone_name": "Europe/Berlin" + }, + "CET": { + "offset": 1, + "zone_name": "Europe/Berlin" + }, + "CLST": { + "offset": -3, + "zone_name": "Chile/Continental" + }, + "CLT": { + "offset": -4, + "zone_name": "Chile/Continental" + }, + "CST": { + "offset": -6, + "zone_name": "America/Chicago" + }, + "EDT": { + "offset": -4, + "zone_name": "America/New_York" + }, + "EST": { + "offset": -5, + "zone_name": "America/New_York" + }, + "GMT": { + "offset": 0, + "zone_name": "Etc/GMT" + }, + "HDT": { + "offset": -9, + "zone_name": "Pacific/Honolulu" + }, + "HST": { + "offset": -8, + "zone_name": "Pacific/Honolulu" + }, + "IST": { + "offset": 5.5, + "zone_name": "Asia/Kolkata" + }, + "JST": { + "offset": 9, + "zone_name": "Asia/Tokyo" + }, + "KST": { + "offset": 9, + "zone_name": "Asia/Seoul" + }, + "MDT": { + "offset": -6, + "zone_name": "America/Denver" + }, + "MST": { + "offset": -7, + "zone_name": "America/Denver" + }, + "NDT": { + "offset": -2.5, + "zone_name": "Canada/Newfoundland" + }, + "NST": { + "offset": -3.5, + "zone_name": "Canada/Newfoundland" + }, + "PDT": { + "offset": -7, + "zone_name": "America/Los_Angeles" + }, + "PST": { + "offset": -8, + "zone_name": "America/Los_Angeles" + }, + "UTC": { + "offset": 0, + "zone_name": "Etc/UTC" + } +} From 698908e9db95a4fb2c8cd1bdfe7ec824f8ade7c9 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 10 Aug 2024 13:37:28 -0700 Subject: [PATCH 254/292] fixed redeploy files for linux --- redeploy.sh | 2 +- redeploy_CT.sh | 2 +- redeploy_MK8DX.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/redeploy.sh b/redeploy.sh index 882142e..6467a0b 100644 --- a/redeploy.sh +++ b/redeploy.sh @@ -4,4 +4,4 @@ docker rm mkwii-mogi-queuebot docker container prune docker build --tag mkwii-mogi-queuebot . docker image prune -docker run -d --name mkwii-mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped mkwii-mogi-queuebot +docker run -d --name mkwii-mogi-queuebot -v $(pwd)/settings_data:/app/settings_data --restart unless-stopped mkwii-mogi-queuebot diff --git a/redeploy_CT.sh b/redeploy_CT.sh index b3a14b9..12a1d25 100644 --- a/redeploy_CT.sh +++ b/redeploy_CT.sh @@ -4,4 +4,4 @@ docker rm ct-mkwii-mogi-queuebot docker container prune docker build --tag ct-mkwii-mogi-queuebot . docker image prune -docker run -d --name ct-mkwii-mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped ct-mkwii-mogi-queuebot +docker run -d --name ct-mkwii-mogi-queuebot -v $(pwd)/settings_data:/app/settings_data --restart unless-stopped ct-mkwii-mogi-queuebot diff --git a/redeploy_MK8DX.sh b/redeploy_MK8DX.sh index 21c56fd..4450a13 100644 --- a/redeploy_MK8DX.sh +++ b/redeploy_MK8DX.sh @@ -4,4 +4,4 @@ docker rm mogi-queuebot docker container prune docker build --tag mogi-queuebot . docker image prune -docker run -d --name mogi-queuebot -v ${pwd}\settings_data:/app/settings_data --restart unless-stopped mogi-queuebot \ No newline at end of file +docker run -d --name mogi-queuebot -v $(pwd)/settings_data:/app/settings_data --restart unless-stopped mogi-queuebot \ No newline at end of file From 6f789c653ddfaba5b26c127425751f4f7ec53877 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 10 Aug 2024 13:38:51 -0700 Subject: [PATCH 255/292] adjusted example config files to match current production and the schedule channel --- example_configs/prod/mk8dx_prod_config.json | 3 ++- example_configs/prod/mkw_ct_prod_config.json | 7 ++++--- example_configs/prod/mkw_rt_prod_config.json | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index c9e1568..b51e1f3 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -9,6 +9,7 @@ "queue_list_channel": 1221665101904478340, "queue_history_channel": 1194440106266865684, "queue_general_channel": 1186158671525318719, + "queue_schedule_channel": null, "admin_roles": [ 464610815299944448, 910528103225389107 @@ -39,7 +40,7 @@ "JOINING_TIME": 55, "DISPLAY_OFFSET_MINUTES": 5, "EXTENSION_TIME": 3, - "ROOM_JOIN_PENALTY_TIME": 5, + "ROOM_JOIN_PENALTY_TIME": 6, "MOGI_LIFETIME": 180, "SUB_RANGE_MMR_ALLOWANCE": 500, "SUB_MESSAGE_LIFETIME_SECONDS": 1200, diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index 5e5038a..a05c7b2 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -9,6 +9,7 @@ "queue_list_channel": 1253109466027655338, "queue_history_channel": 1253109800196243618, "queue_general_channel": 1253109398193045534, + "queue_schedule_channel": null, "admin_roles": [ 387347888935534593, 877629421811540016 @@ -44,10 +45,10 @@ "password": "password", "url": "https://www.mkwlounge.gg", "track_type": "ct", - "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME": 5, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", - "QUEUE_OPEN_TIME": 90, - "JOINING_TIME": 85, + "QUEUE_OPEN_TIME": 80, + "JOINING_TIME": 75, "DISPLAY_OFFSET_MINUTES": 0, "EXTENSION_TIME": 5, "ROOM_JOIN_PENALTY_TIME": 8, diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 0555720..95bf045 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -9,6 +9,7 @@ "queue_list_channel": 1215174057453162496, "queue_history_channel": 1225709377747685477, "queue_general_channel": 1241842559429513361, + "queue_schedule_channel": null, "admin_roles": [ 387347888935534593, 877629421811540016 @@ -44,7 +45,7 @@ "password": "password", "url": "https://www.mkwlounge.gg", "track_type": "rt", - "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME": -55, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, From 2d9535b7c1a4c9fdbeae32275a0c946adaabeb0a Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Tue, 13 Aug 2024 14:09:11 -0700 Subject: [PATCH 256/292] added capability to force format 6v6 --- cogs/SquadQueue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d307c99..020aa03 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1330,6 +1330,8 @@ async def end_voting(mogi: Mogi): await room.view.make_teams(3, "3v3") elif mogi.format == "4v4": await room.view.make_teams(4, "4v4") + elif mogi.format == "6v6": + await room.view.make_teams(6, "6v6") else: await room.view.find_winner() From 0aa418e4f56ecf3dba08df4cbbc2119dc2fe474e Mon Sep 17 00:00:00 2001 From: cyndaquilx Date: Wed, 14 Aug 2024 01:19:49 -0400 Subject: [PATCH 257/292] update mogi list formatting, update purge criteria --- cogs/SquadQueue.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 020aa03..39c61f1 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -323,19 +323,23 @@ async def on_ready(self): schedule_channel_id) if common.SERVER is common.Server.MKW: await self.get_ladder_info() + + def is_queuebot(m: discord.Message): + return m.author.id == self.bot.user.id + purge_after = datetime.now(timezone.utc) - timedelta(days=1) try: - await self.LIST_CHANNEL.purge() + await self.LIST_CHANNEL.purge(check=is_queuebot, after=purge_after) except BaseException: print("Purging List channel failed", flush=True) print(traceback.format_exc()) try: - await self.SUB_CHANNEL.purge() + await self.SUB_CHANNEL.purge(check=is_queuebot, after=purge_after) except BaseException: print("Purging Sub channel failed", flush=True) print(traceback.format_exc()) try: if schedule_channel_id: - await self.SCHEDULE_CHANNEL.purge() + await self.SCHEDULE_CHANNEL.purge(check=is_queuebot, after=purge_after) await self.update_forced_format_list() except BaseException: print("Purging Schedule channel failed", flush=True) @@ -683,7 +687,7 @@ async def list_task(self): if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" + msg += f"`{i}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" if len(on_time_players) == 0: # Text looks better this way @@ -691,12 +695,12 @@ async def list_task(self): msg += "**Late Players:**\n" for i, player in enumerate(late_players, 1): adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - msg += f"{i}) {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" + msg += f"`{i}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" elif common.SERVER is common.Server.MK8DX: all_confirmed_players.sort(reverse=True) for i, player in enumerate(all_confirmed_players, 1): - late_str = " (late)" if player in late_players else "" - msg += f"{i}) {player.lounge_name} ({player.mmr} MMR){late_str}\n" + late_str = " `*`" if player in late_players else "" + msg += f"`{i}.` {player.lounge_name} ({player.mmr} MMR){late_str}\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" From 36254a3e6bb54025aa85f996b14d7eb08a8bc127 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 14 Aug 2024 15:01:11 -0700 Subject: [PATCH 258/292] Fixed description of schedule_forced_format_times --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 020aa03..c68b82f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1135,7 +1135,7 @@ async def format_autocomplete(self, interaction: discord.Interaction, current: s @app_commands.autocomplete(format=format_autocomplete) @app_commands.guild_only() async def schedule_forced_format_times(self, interaction: discord.Interaction, tzone: str, date: str, time: str, format: str): - """Schedule Squad Queue times. Staff use only.""" + """Schedule mogis to have all rooms be specific format. Staff use only.""" curr_time = datetime.now(timezone.utc) dt = datetime.fromisoformat(time).replace(tzinfo=timezone.utc) timestamp = dt.timestamp() From f6813d92d05bf58975cebc5c5e30732e7e13ccbf Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Thu, 15 Aug 2024 07:11:11 -0700 Subject: [PATCH 259/292] refactored voteview to prevent voting on forced format mogis while players are put into rooms --- cogs/SquadQueue.py | 17 +++-------------- mogi_objects.py | 45 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c68b82f..afff51b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1315,25 +1315,14 @@ async def get_ladder_info(self): async def end_voting(mogi: Mogi): """Ends voting in all rooms with ongoing votes.""" try: - if mogi is not None: + if mogi and not mogi.format: for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( f"Skipping room {index} in function end_voting.", flush=True) continue - if mogi.format == "FFA": - await room.view.make_teams(1, "FFA") - elif mogi.format == "2v2": - await room.view.make_teams(2, "2v2") - elif mogi.format == "3v3": - await room.view.make_teams(3, "3v3") - elif mogi.format == "4v4": - await room.view.make_teams(4, "4v4") - elif mogi.format == "6v6": - await room.view.make_teams(6, "6v6") - else: - await room.view.find_winner() + await room.view.find_winner() except Exception as e: print(traceback.format_exc()) @@ -1500,7 +1489,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic self.ROOM_JOIN_PENALTY_TIME, sent_msg.jump_url) curr_room.view = view - await curr_room.channel.send(view=view) + await view.create_message() except discord.DiscordException: err_msg = f"\nAn error has occurred while creating the room channel; please contact your opponents in DM or another channel\n" err_msg += player_mentions + extra_member_mentions diff --git a/mogi_objects.py b/mogi_objects.py index 35d150c..37ee8ca 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -576,13 +576,35 @@ def __init__( "6v6": [] } - self.add_button("FFA", self.general_vote_callback) - self.add_button("2v2", self.general_vote_callback) - self.add_button("3v3", self.general_vote_callback) - self.add_button("4v4", self.general_vote_callback) + async def create_message(self): + if self.mogi.format: + await self.room_channel.send( + content=f"The format **{self.mogi.format}** has been selected!", + view=self + ) + + if self.mogi.format == "FFA": + await self.make_teams(1, "FFA") + elif self.mogi.format == "2v2": + await self.make_teams(2, "2v2") + elif self.mogi.format == "3v3": + await self.make_teams(3, "3v3") + elif self.mogi.format == "4v4": + await self.make_teams(4, "4v4") + elif self.mogi.format == "6v6": + await self.make_teams(6, "6v6") + else: + self.add_button("FFA", self.general_vote_callback) + self.add_button("2v2", self.general_vote_callback) + self.add_button("3v3", self.general_vote_callback) + self.add_button("4v4", self.general_vote_callback) - if common.SERVER is common.Server.MKW: - self.add_button("6v6", self.general_vote_callback) + if common.SERVER is common.Server.MKW: + self.add_button("6v6", self.general_vote_callback) + + await self.room_channel.send( + view=self + ) async def make_teams(self, players_per_team: int, vote_str: str): if common.SERVER is common.Server.MKW: @@ -596,16 +618,19 @@ async def make_teams(self, players_per_team: int, vote_str: str): room = self.mogi.get_room_from_channel_id(self.room_channel.id) - msg = f"""**Poll Ended!** + msg = "ㅤ\n" + + if not self.mogi.format: + msg = f"""**Poll Ended!** 1) FFA - {len(self.votes['FFA'])} 2) 2v2 - {len(self.votes['2v2'])} 3) 3v3 - {len(self.votes['3v3'])} 4) 4v4 - {len(self.votes['4v4'])} """ - if common.SERVER is common.Server.MKW: - msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" - msg += f"Winner: {vote_str}\n\n" + if common.SERVER is common.Server.MKW: + msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" + msg += f"Winner: {vote_str}\n\n" self.header_text = "" tier_text = "Tier " if common.SERVER is common.Server.MK8DX else "T" From 2038770f88cdfa54c18b4777eab9fd5a34c51494 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Fri, 16 Aug 2024 13:57:16 -0700 Subject: [PATCH 260/292] fixed 6v6 draft text --- mogi_objects.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 37ee8ca..f9c53a6 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -671,8 +671,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): 3. **{captain_1.lounge_name}** picks 2 players 4. **{captain_2.lounge_name}** picks 2 players 5. **{captain_1.lounge_name}** picks 2 players -6. **{captain_2.lounge_name}** picks 2 players -7. **{captain_1.lounge_name}** picks 1 player\n""" +6. **{captain_2.lounge_name}** picks 1 players\n""" penalty_time = self.mogi.making_rooms_run_time + \ timedelta(minutes=self.penalty_time) From 994904d56b517071ff54e948133da07b7b3f8883 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 21 Aug 2024 18:28:11 -0700 Subject: [PATCH 261/292] Added the functionality to autoschedule ff mogis --- cogs/SquadQueue.py | 62 +++++++++++++++++--- config_checker.py | 6 ++ example_configs/prod/mk8dx_prod_config.json | 7 ++- example_configs/prod/mkw_ct_prod_config.json | 5 ++ example_configs/prod/mkw_rt_prod_config.json | 5 ++ 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 19dcf1a..c7fb7a9 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -166,6 +166,11 @@ def __init__(self, bot): self.forced_format_times: List[Tuple[datetime, str]] = [] + self.forced_format_order: List[str] = bot.config["FORCED_FORMAT_ORDER"] + + self.queues_between_forced_format_queue: int | None = bot.config[ + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE"] + # Parameters for tracking if we should send an extension message or not self.last_extension_message_timestamp = datetime.now( timezone.utc).replace(second=0, microsecond=0) @@ -239,6 +244,14 @@ def __init__(self, bot): self.FIRST_EVENT_TIME = datetime.combine(datetime.utcnow().date(), datetime.min.time( ), tzinfo=timezone.utc) + timedelta(minutes=bot.config["FIRST_EVENT_TIME"]) + self.FORCED_FORMAT_FIRST_EVENT = datetime( + 2024, 8, 1, 0, 0, 0, tzinfo=timezone.utc) + timedelta(minutes=bot.config["FIRST_EVENT_TIME"]) \ + + timedelta(hours=bot.config["FORCED_FORMAT_HOURLY_OFFSET"]) + + self.FORCED_FORMAT_ORDER_OFFSET = bot.config["FORCED_FORMAT_ORDER_OFFSET"] + + self.FORCED_FORMAT_AUTOSCHEDULE_AMOUNT = bot.config["FORCED_FORMAT_AUTOSCHEDULE_AMOUNT"] + # If we're testing, set the time to the current time so we start a new # event immediately if not self.is_production: @@ -340,7 +353,8 @@ def is_queuebot(m: discord.Message): try: if schedule_channel_id: await self.SCHEDULE_CHANNEL.purge(check=is_queuebot, after=purge_after) - await self.update_forced_format_list() + if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: + await self.autoschedule_forced_format_times() except BaseException: print("Purging Schedule channel failed", flush=True) print(traceback.format_exc()) @@ -1164,6 +1178,36 @@ async def schedule_forced_format_times(self, interaction: discord.Interaction, t await interaction.response.send_message(msg) + async def autoschedule_forced_format_times(self): + """Schedule the next batch of forced format mogis automatically.""" + event_dict = {event[0]: event for event in self.forced_format_times} + time_between_ff_queues = self.queues_between_forced_format_queue * self.QUEUE_OPEN_TIME + + last_event = self.FORCED_FORMAT_FIRST_EVENT + start_index = 0 + while last_event < datetime.now(timezone.utc): + last_event += time_between_ff_queues + start_index += 1 + + total_iterations = self.FORCED_FORMAT_AUTOSCHEDULE_AMOUNT * \ + len(self.forced_format_order) + + for i in range(total_iterations): + current_index = ( + start_index + i + self.FORCED_FORMAT_ORDER_OFFSET) % len(self.forced_format_order) + format = self.forced_format_order[current_index] + + new_event_time = last_event + i * time_between_ff_queues + \ + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES + new_event = (new_event_time, format) + event_dict[new_event[0]] = new_event + + updated_list = list(event_dict.values()) + self.forced_format_times = sorted(updated_list, key=lambda x: x[0]) + + if self.SCHEDULE_CHANNEL: + await self.update_forced_format_list() + async def remove_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: choices = [ app_commands.Choice( @@ -1453,11 +1497,13 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic potential_host_str = curr_room.get_host_str() if potential_host_str == "": potential_host_str += f"No one in the room queued as host. Decide a host amongst yourselves." - vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" - room_msg += f""" -{potential_host_str} + room_msg += f"{potential_host_str}\n" -Vote for format {vote_formats}. + if not mogi.format: + vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" + room_msg += f"Vote for format {vote_formats}.\n" + + room_msg += f""" {player_mentions} {extra_member_mentions} If you need staff's assistance, use the `/ping_staff` command in this channel.""" @@ -1694,8 +1740,10 @@ async def schedule_que_event(self): format = None if len( self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: - format = self.forced_format_times[0][1] - self.forced_format_times.pop(0) + last_event = self.forced_format_times.pop(0) + format = last_event[1] + if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: + await self.autoschedule_forced_format_times() if len( self.sq_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.sq_times[0]: self.sq_times.pop(0) diff --git a/config_checker.py b/config_checker.py index 37d533c..0e03128 100644 --- a/config_checker.py +++ b/config_checker.py @@ -10,6 +10,7 @@ ("queue_list_channel", int), ("queue_history_channel", int), ("queue_general_channel", int), + ("queue_schedule_channel", (int, type(None))), ("admin_roles", list), ("admin_roles.list_items", int), ("helper_staff_roles", list), @@ -31,6 +32,11 @@ ("track_type", str), ("FIRST_EVENT_TIME", int), ("FIRST_EVENT_TIME_DESCRIPTION", str), + ("FORCED_FORMAT_ORDER", list), + ("FORCED_FORMAT_ORDER_OFFSET", int), + ("FORCED_FORMAT_HOURLY_OFFSET", int), + ("FORCED_FORMAT_AUTOSCHEDULE_AMOUNT", int), + ("QUEUES_BETWEEN_FORCED_FORMAT_QUEUE", (int, type(None))), ("JOINING_TIME", int), ("QUEUE_OPEN_TIME", int), ("DISPLAY_OFFSET_MINUTES", int), diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index b51e1f3..9b4636f 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -9,7 +9,7 @@ "queue_list_channel": 1221665101904478340, "queue_history_channel": 1194440106266865684, "queue_general_channel": 1186158671525318719, - "queue_schedule_channel": null, + "queue_schedule_channel": 1274492585917812807, "admin_roles": [ 464610815299944448, 910528103225389107 @@ -36,6 +36,11 @@ "track_type": "rt", "FIRST_EVENT_TIME": 0, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", + "FORCED_FORMAT_ORDER": ["FFA", "3v3", "FFA", "4v4"], + "FORCED_FORMAT_ORDER_OFFSET": 3, + "FORCED_FORMAT_HOURLY_OFFSET": 0, + "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 1, + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": 9, "QUEUE_OPEN_TIME": 60, "JOINING_TIME": 55, "DISPLAY_OFFSET_MINUTES": 5, diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index a05c7b2..da52f0b 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -47,6 +47,11 @@ "track_type": "ct", "FIRST_EVENT_TIME": 5, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", + "FORCED_FORMAT_ORDER": [], + "FORCED_FORMAT_ORDER_OFFSET": 0, + "FORCED_FORMAT_HOURLY_OFFSET": 0, + "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 1, + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": null, "QUEUE_OPEN_TIME": 80, "JOINING_TIME": 75, "DISPLAY_OFFSET_MINUTES": 0, diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 95bf045..9e6ee2e 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -47,6 +47,11 @@ "track_type": "rt", "FIRST_EVENT_TIME": -55, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", + "FORCED_FORMAT_ORDER": [], + "FORCED_FORMAT_ORDER_OFFSET": 0, + "FORCED_FORMAT_HOURLY_OFFSET": 0, + "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 1, + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": null, "QUEUE_OPEN_TIME": 72, "JOINING_TIME": 67, "DISPLAY_OFFSET_MINUTES": 0, From 61507526d80be9b33940a82e45e04bafbea82534 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 24 Aug 2024 05:26:41 -0700 Subject: [PATCH 262/292] added better error handling --- cogs/SquadQueue.py | 7 ++++++- mogi_objects.py | 31 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c7fb7a9..57adcff 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1395,7 +1395,12 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): msg += f"{room.channel.jump_url}\n" msg += room.view.teams_text msg += "ㅤ" - await history_channel.send(msg) + try: + await history_channel.send(msg) + except discord.HTTPException as e: + print( + f"HTTP error while writing room {index} in function write_history, skipping to next room.", + flush=True) except Exception as e: print(traceback.format_exc()) diff --git a/mogi_objects.py b/mogi_objects.py index f9c53a6..e941345 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -565,6 +565,7 @@ def __init__( self.mogi = mogi self.room = room self.header_text = "" + self.message = None self.room_start_msg_link = room_start_msg_link self.teams_text = "" self.found_winner = False @@ -602,7 +603,7 @@ async def create_message(self): if common.SERVER is common.Server.MKW: self.add_button("6v6", self.general_vote_callback) - await self.room_channel.send( + self.message = await self.room_channel.send( view=self ) @@ -695,11 +696,16 @@ async def make_teams(self, players_per_team: int, vote_str: str): room.teams = teams self.found_winner = True - await self.room_channel.send(msg) - if common.CONFIG["USE_THREADS"]: - new_thread_name = self.room_channel.name + \ - f" - {tier_text}{room.tier}" - await self.room_channel.edit(name=new_thread_name) + try: + await self.room_channel.send(msg) + if common.CONFIG["USE_THREADS"]: + new_thread_name = self.room_channel.name + \ + f" - {tier_text}{room.tier}" + await self.room_channel.edit(name=new_thread_name) + except discord.HTTPException as e: + print( + f"HTTP error while ending voting for room {self.room.room_num} in function end_voting, skipping to next room.", + flush=True) async def find_winner(self): if not self.found_winner: @@ -722,6 +728,13 @@ async def find_winner(self): for curr_button in self.children: curr_button.disabled = True + if self.message: + try: + await self.message.edit(view=self) + except discord.errors.NotFound: + print( + f"Failed to edit message for interaction: {self.message.id}. Message is no longer valid.", flush=True) + await self.make_teams(*winner) def add_button(self, label, callback): @@ -752,7 +765,11 @@ async def general_vote_callback(self, interaction: discord.Interaction): curr_button.label = f"{curr_button.custom_id}: {len(self.votes[curr_button.custom_id])}" if self.found_winner: curr_button.disabled = True - await interaction.response.edit_message(view=self) + try: + await interaction.response.edit_message(view=self) + except discord.errors.NotFound: + print( + f"Failed to edit message for interaction: {interaction.id}. Interaction is no longer valid.", flush=True) class JoinView(View): From 708182a7287aa14a1879d669a17fe2ba3622b1f4 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 24 Aug 2024 05:31:17 -0700 Subject: [PATCH 263/292] added host to both messages instead of only top message --- cogs/SquadQueue.py | 2 +- mogi_objects.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 57adcff..54b71cf 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1502,7 +1502,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic potential_host_str = curr_room.get_host_str() if potential_host_str == "": potential_host_str += f"No one in the room queued as host. Decide a host amongst yourselves." - room_msg += f"{potential_host_str}\n" + room_msg += f"\n{potential_host_str}\n" if not mogi.format: vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" diff --git a/mogi_objects.py b/mogi_objects.py index e941345..2213f4f 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -685,6 +685,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): msg += f"\nPenalty is {self.penalty_time} minutes after the room opens. Good luck!" else: if common.SERVER is common.Server.MK8DX: + msg += f"\n{potential_host_str}" msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: cur_time = datetime.now(timezone.utc) From 2ea3d1d3b28a8bccad2252bddfad76696c1224aa Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 28 Aug 2024 20:48:31 -0700 Subject: [PATCH 264/292] changed schedule channel to purge posts for longer periods of time in production --- cogs/SquadQueue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 54b71cf..77bf61d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -339,7 +339,9 @@ async def on_ready(self): def is_queuebot(m: discord.Message): return m.author.id == self.bot.user.id - purge_after = datetime.now(timezone.utc) - timedelta(days=1) + purge_after_amt = 365 if self.is_production else 1 + purge_after = datetime.now(timezone.utc) - \ + timedelta(days=purge_after_amt) try: await self.LIST_CHANNEL.purge(check=is_queuebot, after=purge_after) except BaseException: From b4e2cac4a216dc8fe0b3be577c290169c624daf5 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 28 Aug 2024 20:48:56 -0700 Subject: [PATCH 265/292] Edited write_history header to include number of rooms --- cogs/SquadQueue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 77bf61d..39af28c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1383,7 +1383,8 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): try: if mogi is not None: format_str = f"{mogi.format} " if mogi.format else "" - await history_channel.send(f"{discord.utils.format_dt(mogi.display_time)} {format_str}Rooms") + rooms_str = "Room" if len(mogi.rooms) == 1 else "Rooms" + await history_channel.send(f"{discord.utils.format_dt(mogi.display_time)} - {len(mogi.rooms)} {format_str}{rooms_str}") for index, room in enumerate(mogi.rooms, 1): if not room or not room.view: print( From 640f30df103840cc24257dc1fe6715500786d933 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Fri, 30 Aug 2024 15:17:21 -0700 Subject: [PATCH 266/292] Fixed odd names screwing up discord markdown --- cogs/SquadQueue.py | 8 ++++---- mogi_objects.py | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 39af28c..3231e00 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -703,7 +703,7 @@ async def list_task(self): if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - msg += f"`{i}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR)\n" + msg += f"`{i}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} {adjusted_mmr_text}MMR)\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" if len(on_time_players) == 0: # Text looks better this way @@ -716,7 +716,7 @@ async def list_task(self): all_confirmed_players.sort(reverse=True) for i, player in enumerate(all_confirmed_players, 1): late_str = " `*`" if player in late_players else "" - msg += f"`{i}.` {player.lounge_name} ({player.mmr} MMR){late_str}\n" + msg += f"`{i}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} MMR){late_str}\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" @@ -1490,8 +1490,8 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic for player_num, player in enumerate(room_players, 1): added_str = ": **Added from late players**" if player in late_player_list else "" adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" - mogi_channel_msg += f"`{player_num}.` {player.lounge_name} ({player.mmr} {adjusted_mmr_text}MMR){added_str}\n" - room_msg += f"`{player_num}.` {player.lounge_name} ({player.mmr} MMR)\n" + mogi_channel_msg += f"`{player_num}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} {adjusted_mmr_text}MMR){added_str}\n" + room_msg += f"`{player_num}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} MMR)\n" if not self.allowed_players_check(room_players): mogi_channel_msg += f"\nThe mmr gap in the room is higher than the allowed threshold of {self.room_mmr_threshold} MMR, this room has been cancelled." else: diff --git a/mogi_objects.py b/mogi_objects.py index 2213f4f..1398e2c 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -651,8 +651,11 @@ async def make_teams(self, players_per_team: int, vote_str: str): for j in range(teams_per_room): team_text = f"`{j + 1}.` " - team_names = ", ".join([p.lounge_name for p in teams[j].players]) - scoreboard_text.append(team_names) + team_names = ", ".join([discord.utils.escape_markdown( + p.lounge_name) for p in teams[j].players]) + raw_team_names = ", ".join( + [p.lounge_name for p in teams[j].players]) + scoreboard_text.append(raw_team_names) team_text += team_names team_text += f" ({int(teams[j].avg_mmr)} MMR)\n" msg += team_text From 983166bf0263839ea759b1087ede158a0b361bd5 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 8 Sep 2024 16:38:02 -0700 Subject: [PATCH 267/292] changed the autoscheduler display --- cogs/SquadQueue.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 3231e00..d44895d 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -779,9 +779,22 @@ async def update_forced_format_list(self): msg += "\n" + current_rotation = ', '.join(self.forced_format_order) + + msg += f"**Current Rotation** - {current_rotation}\n" + + time_between_ff_queues = self.queues_between_forced_format_queue * self.QUEUE_OPEN_TIME + time_between_ff_queues_hours = time_between_ff_queues.total_seconds() / 3600 + + if time_between_ff_queues_hours.is_integer(): + msg += f"**Current Interval** - {int(time_between_ff_queues_hours)} hours\n\n" + else: + time_between_ff_queues_minutes = time_between_ff_queues.total_seconds() / 60 + msg += f"**Current Interval** - {int(time_between_ff_queues_minutes)} minutes\n\n" + msg += "**List of Mogis with Scheduled Formats:**\n\n" for index, event in enumerate(self.forced_format_times): - msg += f"{index + 1}) {discord.utils.format_dt(event[0])} - {event[1]}\n" + msg += f"`#{index + 1}` **{event[1]}:** {discord.utils.format_dt(event[0])} - {discord.utils.format_dt(event[0], style='R')}\n" message = msg.split("\n") new_messages = [] From 2160a34aed6398bd2be5313eb7b1799ebe5b6a19 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Fri, 20 Sep 2024 16:32:31 -0700 Subject: [PATCH 268/292] Hard-coded MMR Ranges + redeploy change Removed Lounge API calls for ladder information and added quotes to the directory in the last line of the redeploy --- cogs/SquadQueue.py | 47 ++-------------------------------------------- mogi_objects.py | 30 +++++++++++++++++++++++------ redeploy.sh | 2 +- 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index d44895d..f35dd07 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -219,7 +219,7 @@ def __init__(self, bot): self.TRACK_TYPE = bot.config["track_type"] - self.TIER_INFO = [] + # self.TIER_INFO = [] # Time between each event queue opening self.QUEUE_OPEN_TIME = timedelta(minutes=bot.config["QUEUE_OPEN_TIME"]) @@ -334,8 +334,6 @@ async def on_ready(self): if schedule_channel_id: self.SCHEDULE_CHANNEL = self.bot.get_channel( schedule_channel_id) - if common.SERVER is common.Server.MKW: - await self.get_ladder_info() def is_queuebot(m: discord.Message): return m.author.id == self.bot.user.id @@ -1332,48 +1330,7 @@ async def clear_sq_times(self, interaction: discord.Interaction): await interaction.response.send_message("Cleared list of Squad Queue Times.") - @app_commands.command(name="update_tier_info") - @app_commands.guild_only() - async def update_tier_info(self, interaction: discord.Interaction): - """Updates the mmr divisions that denote each tier. Staff use only.""" - msg = await self.get_ladder_info() - await interaction.response.send_message(msg) - - async def get_ladder_info(self): - timeout = aiohttp.ClientTimeout(total=10) - url = "https://mkwlounge.gg/api/ladderclass.php?ladder_type=" + self.TRACK_TYPE - msg = "" - try: - async with aiohttp.ClientSession( - timeout=timeout, - auth=aiohttp.BasicAuth( - "username", "password")) as session: - async with session.get(url, headers=headers) as resp: - if resp.status != 200: - raise Exception( - "Fetch for tier info has failed, bad status code") - result = await resp.json() - if result['status'] != "success": - raise Exception( - "Fetch for tier info has failed, Status: Failure") - self.TIER_INFO = result["results"] - msg += "Fetch for Tier Info Successful.\n" - for tier in self.TIER_INFO: - boundary = "" - if tier["minimum_mmr"]: - boundary += f"{tier['minimum_mmr']}-" - else: - boundary += "<" - if tier["maximum_mmr"]: - boundary += f"{tier['maximum_mmr']}" - else: - boundary = f">{tier['minimum_mmr']}" - msg += f"T{tier['ladder_order']} Boundary: {boundary}\n" - print(msg, flush=True) - return msg - except Exception as e: - print(traceback.format_exc()) - + @staticmethod async def end_voting(mogi: Mogi): """Ends voting in all rooms with ongoing votes.""" diff --git a/mogi_objects.py b/mogi_objects.py index 1398e2c..33c8dd9 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -320,7 +320,7 @@ def tier(self) -> str: if common.SERVER is common.Server.MK8DX: return get_tier_mk8dx(round(self.avg_mmr) - 500) if common.SERVER is common.Server.MKW: - return str(get_tier_mkw(self.avg_mmr, self.tier_info)) + return str(get_tier_mkw(self.avg_mmr)) @property def tier_collection(self) -> str: @@ -830,11 +830,29 @@ async def button_callback(self, interaction: discord.Interaction, button): "You do not meet room requirements", ephemeral=True) -def get_tier_mkw(mmr: int, tier_info): - for tier in tier_info: - if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( - tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): - return tier["ladder_order"] +def get_tier_mkw(mmr: int): + if mmr > 10999: + return '8' + if mmr > 9499: + return '7' + if mmr > 7999: + return '6' + if mmr > 6499: + return '5' + if mmr > 4999: + return '4' + if mmr > 3499: + return '3' + if mmr > 1999: + return '2' + if mmr > 499: + return '1' + else: + return '0' + #for tier in tier_info: + #if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( + #tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): + #return tier["ladder_order"] def get_tier_mk8dx(mmr: int): diff --git a/redeploy.sh b/redeploy.sh index 6467a0b..42b05d0 100644 --- a/redeploy.sh +++ b/redeploy.sh @@ -4,4 +4,4 @@ docker rm mkwii-mogi-queuebot docker container prune docker build --tag mkwii-mogi-queuebot . docker image prune -docker run -d --name mkwii-mogi-queuebot -v $(pwd)/settings_data:/app/settings_data --restart unless-stopped mkwii-mogi-queuebot +docker run -d --name mkwii-mogi-queuebot -v "$(pwd)/settings_data:/app/settings_data" --restart unless-stopped mkwii-mogi-queuebot From 44a5df44f8c8de820829263dd899c0a1621dea2a Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Fri, 20 Sep 2024 17:08:22 -0700 Subject: [PATCH 269/292] Re-added self.TIER_INFO in initialization Removed the comment notation from the initialization of self.TIER_INFO variable --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index f35dd07..c2ff71b 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -219,7 +219,7 @@ def __init__(self, bot): self.TRACK_TYPE = bot.config["track_type"] - # self.TIER_INFO = [] + self.TIER_INFO = [] # Time between each event queue opening self.QUEUE_OPEN_TIME = timedelta(minutes=bot.config["QUEUE_OPEN_TIME"]) From 35a4361a0baa3e16a68a822da0f9190c2c34f251 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Tue, 14 Jan 2025 17:10:30 -0800 Subject: [PATCH 270/292] enforce specific team selection in forced format 6v6 mk8dx --- mogi_objects.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 33c8dd9..6cf29c6 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -615,7 +615,13 @@ async def make_teams(self, players_per_team: int, vote_str: str): elif common.SERVER is common.Server.MK8DX: self.mogi.making_rooms_run_time = self.mogi.start_time + \ timedelta(minutes=5) - random.shuffle(self.players) + if common.SERVER is common.Server.MK8DX and vote_str == "6v6": + pop_to_end = [8, 7, 4, 3, 0] + for i in pop_to_end: + player = self.players.pop(i) + self.players.append(player) + else: + random.shuffle(self.players) room = self.mogi.get_room_from_channel_id(self.room_channel.id) From c1c5da1526753a060333fe6f229182c9843ac001 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 19 Jan 2025 10:07:46 -0800 Subject: [PATCH 271/292] sort players within each team by mmr --- mogi_objects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index 6cf29c6..7c0db4a 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -647,8 +647,9 @@ async def make_teams(self, players_per_team: int, vote_str: str): teams = [] teams_per_room = 12 // players_per_team for j in range(teams_per_room): - team = Team( - self.players[j * players_per_team:(j + 1) * players_per_team]) + players = sorted(self.players[j * players_per_team:(j + 1) * players_per_team], + key=lambda player: player.mmr, reverse=True) + team = Team(players) teams.append(team) teams.sort(key=lambda team: team.avg_mmr, reverse=True) From 5ac69e75e8038c7305829de5925abfd0bc0360ef Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Mon, 10 Feb 2025 11:07:16 -0800 Subject: [PATCH 272/292] get_tier_mkw function updated Changed the MMR Threshold for T7 in MKW Lounge --- mogi_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mogi_objects.py b/mogi_objects.py index 7c0db4a..be523cf 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -840,7 +840,7 @@ async def button_callback(self, interaction: discord.Interaction, button): def get_tier_mkw(mmr: int): if mmr > 10999: return '8' - if mmr > 9499: + if mmr > 9999: return '7' if mmr > 7999: return '6' From 0c43abc5b25365978df2ff30db7e8fa2a18db508 Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Wed, 26 Mar 2025 15:55:20 -0700 Subject: [PATCH 273/292] MKW Tier Calculation Modification Modified get_tier_mkw function to account for MMR thresholds. Defined player_threshold_check function for t7 and t8 calculation. https://github.com/emilp-595/QueueBot/issues/55 --- mogi_objects.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index be523cf..f3da56b 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -320,7 +320,7 @@ def tier(self) -> str: if common.SERVER is common.Server.MK8DX: return get_tier_mk8dx(round(self.avg_mmr) - 500) if common.SERVER is common.Server.MKW: - return str(get_tier_mkw(self.avg_mmr)) + return str(get_tier_mkw(self.avg_mmr, self.players)) @property def tier_collection(self) -> str: @@ -837,10 +837,10 @@ async def button_callback(self, interaction: discord.Interaction, button): "You do not meet room requirements", ephemeral=True) -def get_tier_mkw(mmr: int): - if mmr > 10999: +def get_tier_mkw(mmr: int, players: List[Player]): + if mmr > 10499 and player_threshold_check(10500, players): return '8' - if mmr > 9999: + if mmr > 8999 and player_threshold_check(9000, players): return '7' if mmr > 7999: return '6' @@ -856,11 +856,17 @@ def get_tier_mkw(mmr: int): return '1' else: return '0' - #for tier in tier_info: + #for tier in tier_info: #if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( #tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): #return tier["ladder_order"] + +def player_threshold_check(mmr: int, players: List[Player]): + for player in players: + if player.mmr < mmr: + return False + return True def get_tier_mk8dx(mmr: int): if mmr > 14000: From 664dd0230255216b085df8295f0f981777900f8f Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Thu, 27 Mar 2025 09:52:34 -0700 Subject: [PATCH 274/292] MKW Queue Quick Start Added a check to start mogis if there are more than 8 players needed for a new room at the time of the queue popping. https://github.com/emilp-595/QueueBot/issues/56 --- cogs/SquadQueue.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index c2ff71b..b0c47d5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -567,7 +567,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): if players[0].is_matchmaking_mmr_adjusted: msg += f"\nPlayer considered {players[0].adjusted_mmr} MMR for matchmaking purposes." - event_status_launched = self.check_close_event_change() + event_status_launched = await self.check_close_event_change() try: await interaction.response.send_message(msg) finally: @@ -594,7 +594,7 @@ async def drop(self, interaction: discord.Interaction): msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" msg += f", `[{mogi.count_registered()} players]`" - event_status_launched = self.check_close_event_change() + event_status_launched = await self.check_close_event_change() try: await interaction.response.send_message(msg) finally: @@ -912,7 +912,7 @@ async def remove_player(self, interaction: discord.Interaction, member: discord. msg += f" from the mogi {discord.utils.format_dt(mogi.start_time, style='R')}" msg += f", `[{mogi.count_registered()} players]`" - event_status_launched = self.check_close_event_change() + event_status_launched = await self.check_close_event_change() try: await interaction.response.send_message(msg) finally: @@ -1560,7 +1560,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic self.old_events.append(self.ongoing_event) self.ongoing_event = None - def check_close_event_change(self) -> Tuple[bool, bool]: + async def check_close_event_change(self) -> Tuple[bool, bool]: """Returns a bool indicating if the event was gathering but this function then closed the mogi depending on the time and number of players or other logic""" mogi = self.ongoing_event @@ -1615,6 +1615,11 @@ def check_close_event_change(self) -> Tuple[bool, bool]: f"regardless." else: + if not mogi.any_room_cancelled(self.allowed_players_check) and common.SERVER is common.Server.MKW and num_needed_teams > 8: + mogi.gathering = False + self.cur_extension_message = None + await mogi.mogi_channel.send(f"New room requires too many people to start ({num_needed_teams}), starting rooms now.") + return True cur_extension_timestamp = datetime.now( timezone.utc).replace(second=0, microsecond=0) # At this point, we're in the extension time. So if the extension timestamp is in a different @@ -1688,7 +1693,7 @@ async def sqscheduler(self): except Exception as e: print(traceback.format_exc()) try: - if self.check_close_event_change(): + if await self.check_close_event_change(): await self.launch_mogi() await self.check_send_extension_message() except Exception as e: @@ -1905,7 +1910,7 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): mogi.teams.append(squad) msg = f"{players[0].lounge_name} added 12 times." await self.queue_or_send(ctx, msg) - if self.check_close_event_change(): + if await self.check_close_event_change(): await self.launch_mogi() @commands.command(name="debug_add_many_ratings") @@ -1930,7 +1935,7 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): mogi.teams.append(Team([player])) msg = f"Players added with the following ratings: {' '.join(['`' + r + '`' for r in ratings])}" await self.queue_or_send(ctx, msg) - if self.check_close_event_change(): + if await self.check_close_event_change(): await self.launch_mogi() @commands.command(name="debug_add_many_players") @@ -1962,7 +1967,7 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem mogi.teams.append(squad) msg = f"{players[0].lounge_name} added {num_times} times." await self.queue_or_send(ctx, msg) - if self.check_close_event_change(): + if await self.check_close_event_change(): await self.launch_mogi() @commands.command(name="debug_start_rooms") From c1747b8e69016474d3c1bb48ce58a7d83a067c9a Mon Sep 17 00:00:00 2001 From: emilp-595 Date: Fri, 28 Mar 2025 15:49:46 -0700 Subject: [PATCH 275/292] Fixed Queue Schedule Message Changed the conditions for the forced formats to allow queue schedule messages to post without a pre-defined order. --- cogs/SquadQueue.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b0c47d5..789977f 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -353,7 +353,8 @@ def is_queuebot(m: discord.Message): try: if schedule_channel_id: await self.SCHEDULE_CHANNEL.purge(check=is_queuebot, after=purge_after) - if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: + #if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: + if self.queues_between_forced_format_queue: await self.autoschedule_forced_format_times() except BaseException: print("Purging Schedule channel failed", flush=True) @@ -779,12 +780,14 @@ async def update_forced_format_list(self): current_rotation = ', '.join(self.forced_format_order) - msg += f"**Current Rotation** - {current_rotation}\n" + msg += f"**Current Rotation** - {current_rotation or 'N/A'}\n" time_between_ff_queues = self.queues_between_forced_format_queue * self.QUEUE_OPEN_TIME time_between_ff_queues_hours = time_between_ff_queues.total_seconds() / 3600 - if time_between_ff_queues_hours.is_integer(): + if not current_rotation: + msg+= f"**Current Interval** - N/A\n\n" + elif time_between_ff_queues_hours.is_integer(): msg += f"**Current Interval** - {int(time_between_ff_queues_hours)} hours\n\n" else: time_between_ff_queues_minutes = time_between_ff_queues.total_seconds() / 60 From df44f03d7d4cfae63f54ca23ae2d2fd9d72e9d16 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 12 May 2025 17:50:44 -0700 Subject: [PATCH 276/292] initial commit --- cogs/SquadQueue.py | 40 +++++++++++-- common.py | 3 + example_configs/prod/mk8dx_prod_config.json | 2 +- example_configs/prod/mkw_ct_prod_config.json | 2 +- example_configs/prod/mkw_rt_prod_config.json | 2 +- example_configs/prod/mkworld_prod_config.json | 56 +++++++++++++++++++ host_fcs.py | 6 ++ mmr.py | 6 ++ mogi_objects.py | 46 ++++++++++----- redeploy_MKWorld.sh | 7 +++ 10 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 example_configs/prod/mkworld_prod_config.json create mode 100644 redeploy_MKWorld.sh diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 789977f..3f010f0 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -56,6 +56,11 @@ def mk8dx_players_allowed(players: List[Player], threshold: int) -> bool: return basic_threshold_players_allowed(players, threshold) +def mkworld_players_allowed(players: List[Player], threshold: int) -> bool: + """Returns true if the given list of players would be allowed to play together""" + return basic_threshold_players_allowed(players, threshold) + + @app_commands.guild_only() class SettingsGroup(app_commands.Group): def __init__(self, squad_queue: SquadQueue, *args, **kwargs): @@ -205,6 +210,8 @@ def __init__(self, bot): self.URL = bot.config["url"] + self.PLAYERS_PER_ROOM: int = self.bot.config["PLAYERS_PER_ROOM"] or 12 + self.PLACEMENT_PLAYER_MMR = self.bot.config["PLACEMENT_PLAYER_MMR"] self.ROOM_JOIN_PENALTY_TIME = self.bot.config["ROOM_JOIN_PENALTY_TIME"] @@ -353,7 +360,7 @@ def is_queuebot(m: discord.Message): try: if schedule_channel_id: await self.SCHEDULE_CHANNEL.purge(check=is_queuebot, after=purge_after) - #if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: + # if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: if self.queues_between_forced_format_queue: await self.autoschedule_forced_format_times() except BaseException: @@ -421,6 +428,8 @@ def allowed_players_check(self, players: List[Player]): return mkw_players_allowed(players, self.room_mmr_threshold) elif common.SERVER is common.Server.MK8DX: return mk8dx_players_allowed(players, self.room_mmr_threshold) + elif common.SERVER is common.Server.MKWorld: + return mkworld_players_allowed(players, self.room_mmr_threshold) # goes thru the msg queue for each channel and combines them # into as few messsages as possible, then sends them @@ -504,6 +513,9 @@ async def join_queue(self, interaction: discord.Interaction, host=False): elif common.SERVER is common.Server.MKW: # member = await self.bot.fetch_user(82862780591378432) pass + if common.SERVER is common.Server.MKWorld: + # is actually a user and not a member + member = await self.bot.fetch_user(318637887597969419) mogi = self.get_mogi(interaction) if mogi is None or not mogi.started or not mogi.gathering: await interaction.response.send_message("Queue has not started yet.") @@ -718,6 +730,13 @@ async def list_task(self): msg += f"`{i}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} MMR){late_str}\n" if i % mogi.players_per_room == 0: msg += "ㅤ\n" + elif common.SERVER is common.Server.MKWorld: + all_confirmed_players.sort(reverse=True) + for i, player in enumerate(all_confirmed_players, 1): + late_str = " `*`" if player in late_players else "" + msg += f"`{i}.` {discord.utils.escape_markdown(player.lounge_name)} ({player.mmr} MMR){late_str}\n" + if i % mogi.players_per_room == 0: + msg += "ㅤ\n" msg += f"\n**Last Updated:** {discord.utils.format_dt(datetime.now(timezone.utc), style='R')}" message = msg.split("\n") @@ -765,6 +784,7 @@ async def update_forced_format_list(self): if self.SCHEDULE_CHANNEL: msg = "" + # maybe for mkworld if not 1 hour blocks for mogis if common.SERVER is common.Server.MKW: msg += "**Daily Queue Times:**\n\n" @@ -786,7 +806,7 @@ async def update_forced_format_list(self): time_between_ff_queues_hours = time_between_ff_queues.total_seconds() / 3600 if not current_rotation: - msg+= f"**Current Interval** - N/A\n\n" + msg += f"**Current Interval** - N/A\n\n" elif time_between_ff_queues_hours.is_integer(): msg += f"**Current Interval** - {int(time_between_ff_queues_hours)} hours\n\n" else: @@ -865,9 +885,9 @@ async def on_message(self, message: discord.Message): @app_commands.command(name="scoreboard") @app_commands.guild_only() async def scoreboard(self, interaction: discord.Interaction): - """Displays the scoreboard of the room. Only works in thread channels for SQ rooms.""" - if common.SERVER is not common.Server.MK8DX: - await interaction.response.send_message(f"Command is only usable for MK8DX.", ephemeral=True) + """Displays the scoreboard of the room. Only works in thread channels for queue rooms.""" + if common.SERVER is common.Server.MKW: + await interaction.response.send_message(f"Command is only usable for MK8DX and MKWorld.", ephemeral=True) return if not isinstance(interaction.channel, discord.Thread): @@ -1333,7 +1353,6 @@ async def clear_sq_times(self, interaction: discord.Interaction): await interaction.response.send_message("Cleared list of Squad Queue Times.") - @staticmethod async def end_voting(mogi: Mogi): """Ends voting in all rooms with ongoing votes.""" @@ -1369,6 +1388,8 @@ async def write_history(mogi: Mogi, history_channel: discord.TextChannel): msg += f"{room.view.room_start_msg_link}\n" elif common.SERVER is common.Server.MK8DX: msg += f"{room.channel.jump_url}\n" + elif common.SERVER is common.Server.MKWorld: + msg += f"{room.channel.jump_url}\n" msg += room.view.teams_text msg += "ㅤ" try: @@ -1480,6 +1501,7 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic potential_host_str += f"No one in the room queued as host. Decide a host amongst yourselves." room_msg += f"\n{potential_host_str}\n" + # make it work for variable amount of people in room, mkworld if not mogi.format: vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" room_msg += f"Vote for format {vote_formats}.\n" @@ -1901,6 +1923,8 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(82862780591378432) + elif common.SERVER is common.Server.MKWorld: + member = await self.bot.fetch_user(318637887597969419) check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) @@ -1931,6 +1955,8 @@ async def debug_add_many_ratings(self, ctx, *ratings: str): member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: member = await self.GUILD.fetch_member(1114699357179088917) + elif common.SERVER is common.Server.MKWorld: + member = await self.bot.fetch_user(318637887597969419) for i, rating in enumerate(ratings, 1): if common.is_int(rating): player = Player(member, f"{member.name} {i}", int( @@ -1958,6 +1984,8 @@ async def debug_add_many_players(self, ctx, members: commands.Greedy[discord.Mem member = await self.bot.fetch_user(318637887597969419) elif common.SERVER is common.Server.MKW: member = await self.bot.fetch_user(314861232693706752) + elif common.SERVER is common.Server.MKWorld: + member = await self.bot.fetch_user(318637887597969419) check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) diff --git a/common.py b/common.py index 002c13d..70aa362 100644 --- a/common.py +++ b/common.py @@ -12,12 +12,15 @@ class Server(Enum): MKW = auto() MK8DX = auto() + MKWorld = auto() if CONFIG["lounge"] == "MKW": SERVER = Server.MKW elif CONFIG["lounge"] == "MK8DX": SERVER = Server.MK8DX +elif CONFIG["lounge"] == "MKWorld": + SERVER = Server.MKWorld else: raise ValueError( f"{CONFIG['lounge']} is not a valid option for the 'lounge' attribute in the config.") diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index 9b4636f..7a1bbf7 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -2,7 +2,7 @@ "token": "", "is_production": true, "lounge": "MK8DX", - "lounge_description": "Valid options are MKW or MK8DX", + "lounge_description": "Valid options are MKW, MKWorld, or MK8DX", "guild_id": 445404006177570829, "queue_join_channel": 1186158671525318719, "queue_sub_channel": 1186158767029620766, diff --git a/example_configs/prod/mkw_ct_prod_config.json b/example_configs/prod/mkw_ct_prod_config.json index da52f0b..bee8bf9 100644 --- a/example_configs/prod/mkw_ct_prod_config.json +++ b/example_configs/prod/mkw_ct_prod_config.json @@ -2,7 +2,7 @@ "token": "", "is_production": true, "lounge": "MKW", - "lounge_description": "Valid options are MKW or MK8DX", + "lounge_description": "Valid options are MKW, MKWorld, or MK8DX", "guild_id": 387347467332485122, "queue_join_channel": 1253109700107567155, "queue_sub_channel": 1253109752418930779, diff --git a/example_configs/prod/mkw_rt_prod_config.json b/example_configs/prod/mkw_rt_prod_config.json index 9e6ee2e..338f7d3 100644 --- a/example_configs/prod/mkw_rt_prod_config.json +++ b/example_configs/prod/mkw_rt_prod_config.json @@ -2,7 +2,7 @@ "token": "", "is_production": true, "lounge": "MKW", - "lounge_description": "Valid options are MKW or MK8DX", + "lounge_description": "Valid options are MKW, MKWorld, or MK8DX", "guild_id": 387347467332485122, "queue_join_channel": 1215174119767932979, "queue_sub_channel": 1215174160234315806, diff --git a/example_configs/prod/mkworld_prod_config.json b/example_configs/prod/mkworld_prod_config.json new file mode 100644 index 0000000..ac7d242 --- /dev/null +++ b/example_configs/prod/mkworld_prod_config.json @@ -0,0 +1,56 @@ +{ + "token": "", + "is_production": true, + "lounge": "MKWorld", + "lounge_description": "Valid options are MKW, MKWorld, or MK8DX", + "guild_id": 1326380310233092157, + "queue_join_channel": 1186158671525318719, + "queue_sub_channel": 1186158767029620766, + "queue_list_channel": 1221665101904478340, + "queue_history_channel": 1194440106266865684, + "queue_general_channel": 1186158671525318719, + "queue_schedule_channel": 1274492585917812807, + "admin_roles": [ + 464610815299944448, + 910528103225389107 + ], + "helper_staff_roles": [ + 910528103225389107 + ], + "members_for_channels": [ + 934740707934081035, + 743532508343304242, + 735910635082219570 + ], + "roles_for_channels": [], + "PLACEMENT_PLAYER_MMR": 1500, + "placement_role_id": 730980761322389504, + "frequently_tagged_role_id": 682445864400453739, + "restricted_role_id": 600495108999086090, + "muted_role_id": 600495108999086090, + "queue_messages": true, + "sec_between_queue_msgs": 2, + "username": "username", + "password": "password", + "url": "https://www.mk8dx-lounge.com", + "track_type": "rt", + "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", + "FORCED_FORMAT_ORDER": ["FFA", "3v3", "FFA", "4v4"], + "FORCED_FORMAT_ORDER_OFFSET": 3, + "FORCED_FORMAT_HOURLY_OFFSET": 0, + "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 1, + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": 9, + "QUEUE_OPEN_TIME": 60, + "JOINING_TIME": 55, + "DISPLAY_OFFSET_MINUTES": 5, + "EXTENSION_TIME": 3, + "ROOM_JOIN_PENALTY_TIME": 6, + "MOGI_LIFETIME": 180, + "SUB_RANGE_MMR_ALLOWANCE": 500, + "SUB_MESSAGE_LIFETIME_SECONDS": 1200, + "ROOM_MMR_THRESHOLD": 9999999, + "MATCHMAKING_BOTTOM_MMR": null, + "MATCHMAKING_TOP_MMR": null, + "USE_THREADS": true +} \ No newline at end of file diff --git a/host_fcs.py b/host_fcs.py index d713b24..18a243b 100644 --- a/host_fcs.py +++ b/host_fcs.py @@ -17,6 +17,10 @@ async def _get_mk8dx_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: return {} +async def _get_mkworld_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: + return {} + + async def _get_mkw_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: request_url = MKW_HOST_API_URL + ",".join(discord_ids) async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5)) as session: @@ -40,5 +44,7 @@ async def get_hosts(discord_ids: Iterable[str]) -> Dict[str, str]: return await _get_mkw_hosts(discord_ids) elif common.SERVER is common.Server.MK8DX: return await _get_mk8dx_hosts(discord_ids) + elif common.SERVER is common.Server.MKWorld: + return await _get_mkworld_hosts(discord_ids) else: raise ValueError("Bad config settings for server.") diff --git a/mmr.py b/mmr.py index e586cbe..44d1cbb 100644 --- a/mmr.py +++ b/mmr.py @@ -39,6 +39,8 @@ async def update_ratings(self): rating_func = self._pull_mk8dx_ratings elif common.SERVER is common.Server.MKW: rating_func = self._pull_mkw_ratings + elif common.SERVER is common.Server.MKWorld: + rating_func = self._pull_mkworld_ratings else: raise Exception("Unreachable code.") @@ -64,6 +66,10 @@ async def _pull_mkw_ratings(self) -> bool: url = f"""{common.CONFIG["url"]}/api/ladderplayer.php?ladder_type={common.CONFIG["track_type"]}&all&fields=discord_user_id,current_mmr,player_name""" return await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) + async def _pull_mkworld_ratings(self) -> bool: + url = f"""{common.CONFIG["url"]}/api/player/list""" + return await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) + @staticmethod async def _pull_ratings(url, parser, validator) -> bool: """Returns True if the ratings were pulled and successfully stored.""" diff --git a/mogi_objects.py b/mogi_objects.py index f3da56b..72fba87 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -219,6 +219,8 @@ def generate_proposed_list(self, return self._mk8dx_generate_final_list() elif common.SERVER is common.Server.MKW: return self._mkw_generate_final_list(valid_players_check) + elif common.SERVER is common.Server.MKWorld: + return self._mk8dx_generate_final_list() else: raise ValueError(f"Unknown server in config: {common.Server}") @@ -319,14 +321,16 @@ def __init__( def tier(self) -> str: if common.SERVER is common.Server.MK8DX: return get_tier_mk8dx(round(self.avg_mmr) - 500) - if common.SERVER is common.Server.MKW: + elif common.SERVER is common.Server.MKW: return str(get_tier_mkw(self.avg_mmr, self.players)) + elif common.SERVER is common.Server.MKWorld: + return get_tier_mk8dx(round(self.avg_mmr) - 500) @property def tier_collection(self) -> str: if common.SERVER is common.Server.MK8DX: return self.tier - if common.SERVER is common.Server.MKW: + elif common.SERVER is common.Server.MKW: tier_number = self.tier if not common.is_int(tier_number): return "HT" @@ -336,6 +340,8 @@ def tier_collection(self) -> str: if tier_number >= 5: return "HT" return "MT" + elif common.SERVER is common.Server.MKWorld: + return self.tier @property def mmr_high(self) -> int: @@ -615,6 +621,9 @@ async def make_teams(self, players_per_team: int, vote_str: str): elif common.SERVER is common.Server.MK8DX: self.mogi.making_rooms_run_time = self.mogi.start_time + \ timedelta(minutes=5) + elif common.SERVER is common.Server.MKWorld: + self.mogi.making_rooms_run_time = self.mogi.start_time + \ + timedelta(minutes=5) if common.SERVER is common.Server.MK8DX and vote_str == "6v6": pop_to_end = [8, 7, 4, 3, 0] for i in pop_to_end: @@ -640,7 +649,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): msg += f"Winner: {vote_str}\n\n" self.header_text = "" - tier_text = "Tier " if common.SERVER is common.Server.MK8DX else "T" + tier_text = "Tier " if common.SERVER is common.Server.MK8DX or common.SERVER is common.Server.MKWorld else "T" self.header_text += f"**Room {room.room_num} MMR: {room.avg_mmr} - {tier_text}{room.tier}** " msg += self.header_text + "\n" @@ -683,6 +692,9 @@ async def make_teams(self, players_per_team: int, vote_str: str): 4. **{captain_2.lounge_name}** picks 2 players 5. **{captain_1.lounge_name}** picks 2 players 6. **{captain_2.lounge_name}** picks 1 players\n""" + elif common.SERVER is common.Server.MKWorld: + msg += f"\nTable: `/scoreboard`\n" + msg += f"RandomBot Scoreboard: `/scoreboard {teams_per_room} {', '.join(scoreboard_text)}`\n" penalty_time = self.mogi.making_rooms_run_time + \ timedelta(minutes=self.penalty_time) @@ -691,18 +703,23 @@ async def make_teams(self, players_per_team: int, vote_str: str): if potential_host_str == "": if common.SERVER is common.Server.MK8DX: msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" - elif common.SERVER.MKW: + elif common.SERVER is common.Server.MKW: msg += f"\nPenalty is {self.penalty_time} minutes after the room opens. Good luck!" + elif common.SERVER is common.Server.MKWorld: + msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" else: if common.SERVER is common.Server.MK8DX: msg += f"\n{potential_host_str}" msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" - else: + elif common.SERVER is common.Server.MKW: cur_time = datetime.now(timezone.utc) mkw_room_open_time = cur_time + timedelta(minutes=1) pen_time = mkw_room_open_time + \ timedelta(minutes=self.penalty_time) msg += f"\nRoom open at :{mkw_room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" + elif common.SERVER is common.Server.MK8DX: + msg += f"\n{potential_host_str}" + msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" room.teams = teams @@ -856,17 +873,18 @@ def get_tier_mkw(mmr: int, players: List[Player]): return '1' else: return '0' - #for tier in tier_info: - #if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( - #tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): - #return tier["ladder_order"] + # for tier in tier_info: + # if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( + # tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): + # return tier["ladder_order"] + - def player_threshold_check(mmr: int, players: List[Player]): - for player in players: - if player.mmr < mmr: - return False - return True + for player in players: + if player.mmr < mmr: + return False + return True + def get_tier_mk8dx(mmr: int): if mmr > 14000: diff --git a/redeploy_MKWorld.sh b/redeploy_MKWorld.sh new file mode 100644 index 0000000..05be999 --- /dev/null +++ b/redeploy_MKWorld.sh @@ -0,0 +1,7 @@ +#!/bin/bash +docker stop mkworld-mogi-queuebot +docker rm mkworld-mogi-queuebot +docker container prune +docker build --tag mkworld-mogi-queuebot . +docker image prune +docker run -d --name mkworld-mogi-queuebot -v $(pwd)/settings_data:/app/settings_data --restart unless-stopped mkworld-mogi-queuebot \ No newline at end of file From 77a927c57e46d0296035e80b9e97cf6c1ea2b594 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 12 May 2025 19:53:35 -0700 Subject: [PATCH 277/292] Created commands for modifying available vote buttons --- cogs/SquadQueue.py | 109 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 3f010f0..a37e057 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -4,6 +4,7 @@ import discord from discord.ext import commands, tasks from discord import app_commands +from discord.app_commands import Range from datetime import datetime, timezone, timedelta import pytz import time @@ -210,7 +211,10 @@ def __init__(self, bot): self.URL = bot.config["url"] - self.PLAYERS_PER_ROOM: int = self.bot.config["PLAYERS_PER_ROOM"] or 12 + self.PLAYERS_PER_ROOM: int = self.bot.config.get( + "PLAYERS_PER_ROOM", 12) + + self.ALLOWED_VOTE_BUTTONS = [] self.PLACEMENT_PLAYER_MMR = self.bot.config["PLACEMENT_PLAYER_MMR"] @@ -342,6 +346,8 @@ async def on_ready(self): self.SCHEDULE_CHANNEL = self.bot.get_channel( schedule_channel_id) + self.generate_vote_buttons() + def is_queuebot(m: discord.Message): return m.author.id == self.bot.user.id purge_after_amt = 365 if self.is_production else 1 @@ -1895,6 +1901,107 @@ def get_event_str(self, mogi: Mogi): mogi.start_time, style="R") return f"`#{mogi.sq_id}` **{mogi.max_player_per_team}v{mogi.max_player_per_team}:** {mogi_time} - {mogi_time_relative}" + async def print_vote_button_info(self, interaction): + str = "" + + str += f"The new amount of players per room is {self.PLAYERS_PER_ROOM}\n" + str += f"The new vote buttons for this amount of players per room are " + + format_names = [btn["format_name"] + for btn in self.ALLOWED_VOTE_BUTTONS] + str += ", ".join(format_names) + + await interaction.response.send_message(str) + + @app_commands.command(name="update_players_per_room") + @app_commands.guild_only() + async def update_players_per_room(self, interaction: discord.Interaction, new_amount: Range[int, 2]): + """Sets the new amount of player per room for queue. Staff use only.""" + self.PLAYERS_PER_ROOM = new_amount + self.generate_vote_buttons() + + await self.print_vote_button_info(interaction) + + def generate_vote_buttons(self): + new_vote_button_list = [] + iterator = iter(range(1, self.PLAYERS_PER_ROOM)) + + for i in iterator: + if self.PLAYERS_PER_ROOM % i == 0: + format_name = "FFA" if i == 1 else f"{i}v{i}" + new_vote_button_list.append({ + "num_players_per_team": i, + "format_name": format_name + }) + + self.ALLOWED_VOTE_BUTTONS = new_vote_button_list + + async def add_vote_button_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + total = self.PLAYERS_PER_ROOM + + existing_formats = {btn["format_name"] + for btn in self.ALLOWED_VOTE_BUTTONS} + + new_formats = [] + for i in range(1, total): + if total % i != 0: + continue + fmt = "FFA" if i == 1 else f"{i}v{i}" + if fmt not in existing_formats and current in fmt: + new_formats.append(fmt) + + return [ + app_commands.Choice(name=fmt, value=fmt) + for fmt in new_formats[:25] + ] + + @app_commands.command(name="add_vote_button") + @app_commands.autocomplete(vote_string=add_vote_button_autocomplete) + @app_commands.guild_only() + async def add_vote_button(self, interaction: discord.Interaction, vote_string: str): + """Adds a vote button from list of possible buttons. Staff use only.""" + if vote_string == "FFA": + num_players_per_team = 1 + else: + try: + num_players_per_team = int(vote_string.split("v")[0]) + except (ValueError, IndexError): + await interaction.response.send_message("Invalid format name.", ephemeral=True) + return + + self.ALLOWED_VOTE_BUTTONS.append({ + "num_players_per_team": num_players_per_team, + "format_name": vote_string + }) + + self.ALLOWED_VOTE_BUTTONS.sort(key=lambda x: x["num_players_per_team"]) + + await self.print_vote_button_info(interaction) + + async def remove_vote_button_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + choices = [ + app_commands.Choice( + name=btn["format_name"], value=btn["format_name"]) + for btn in self.ALLOWED_VOTE_BUTTONS + if current in btn["format_name"] + ] + return choices[:25] + + @app_commands.command(name="remove_vote_button") + @app_commands.autocomplete(vote_string=remove_vote_button_autocomplete) + @app_commands.guild_only() + async def remove_vote_button(self, interaction: discord.Interaction, vote_string: str): + """Removes a vote button from list of possible buttons. Staff use only.""" + if len(self.ALLOWED_VOTE_BUTTONS) <= 1: + await interaction.response.send_message("You cannnot remove last possible button.") + return + + self.ALLOWED_VOTE_BUTTONS = [ + btn for btn in self.ALLOWED_VOTE_BUTTONS if btn["format_name"] != vote_string + ] + + await self.print_vote_button_info(interaction) + @commands.command(name="sync") @commands.is_owner() async def sync(self, ctx): From 2d10dc86921cbadd7944f943e4f383fe9ce70a9f Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 11 Jun 2025 00:54:36 -0700 Subject: [PATCH 278/292] 1st good working version of mkworld bot --- cogs/SquadQueue.py | 69 +++++++++++++---- example_configs/prod/mk8dx_prod_config.json | 2 +- example_configs/prod/mkworld_prod_config.json | 39 +++++----- mmr.py | 42 +++++++++- mogi_objects.py | 76 +++++++------------ unit_testing.py | 3 + 6 files changed, 143 insertions(+), 88 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a37e057..de4ba12 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -309,6 +309,11 @@ def load_staff_settings(self, view_only=False): if "PLACEMENT_PLAYER_MMR" in settings_dict: self.PLACEMENT_PLAYER_MMR = settings_dict["PLACEMENT_PLAYER_MMR"] common.CONFIG["PLACEMENT_PLAYER_MMR"] = settings_dict["PLACEMENT_PLAYER_MMR"] + if "PLAYERS_PER_ROOM" in settings_dict: + self.PLAYERS_PER_ROOM = settings_dict["PLAYERS_PER_ROOM"] + common.CONFIG["PLAYERS_PER_ROOM"] = settings_dict["PLAYERS_PER_ROOM"] + if "ALLOWED_VOTE_BUTTONS" in settings_dict and len(settings_dict["ALLOWED_VOTE_BUTTONS"]) > 0: + self.ALLOWED_VOTE_BUTTONS = settings_dict["ALLOWED_VOTE_BUTTONS"] except BaseException: print(traceback.format_exc()) @@ -321,7 +326,10 @@ def dump_staff_settings(self): "MATCHMAKING_BOTTOM_MMR": common.CONFIG["MATCHMAKING_BOTTOM_MMR"], "MATCHMAKING_TOP_MMR": common.CONFIG["MATCHMAKING_TOP_MMR"], "FIRST_EVENT_TIME": common.CONFIG["FIRST_EVENT_TIME"], - "PLACEMENT_PLAYER_MMR": self.PLACEMENT_PLAYER_MMR} + "PLACEMENT_PLAYER_MMR": self.PLACEMENT_PLAYER_MMR, + "PLAYERS_PER_ROOM": self.PLAYERS_PER_ROOM, + "ALLOWED_VOTE_BUTTONS": self.ALLOWED_VOTE_BUTTONS + } try: with open(STAFF_SETTINGS_PKL, 'wb') as f: dill.dump(settings_dict, f) @@ -346,7 +354,8 @@ async def on_ready(self): self.SCHEDULE_CHANNEL = self.bot.get_channel( schedule_channel_id) - self.generate_vote_buttons() + if not self.ALLOWED_VOTE_BUTTONS: + self.generate_vote_buttons() def is_queuebot(m: discord.Message): return m.author.id == self.bot.user.id @@ -912,7 +921,7 @@ async def scoreboard(self, interaction: discord.Interaction): await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) return - format_ = round(12 / len(room.teams)) + format_ = round(self.ongoing_event.players_per_room / len(room.teams)) msg = f"!submit {format_} {room.tier}\n" for team in room.teams: @@ -1507,10 +1516,10 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic potential_host_str += f"No one in the room queued as host. Decide a host amongst yourselves." room_msg += f"\n{potential_host_str}\n" - # make it work for variable amount of people in room, mkworld if not mogi.format: - vote_formats = "FFA, 2v2, 3v3, 4v4" if common.SERVER is common.Server.MK8DX else "FFA, 2v2, 3v3, 4v4, 6v6" - room_msg += f"Vote for format {vote_formats}.\n" + vote_formats = [b["format_name"] for b in mogi.buttons] + vote_formats_str = ", ".join(vote_formats) + room_msg += f"Vote for format {vote_formats_str}.\n" room_msg += f""" {player_mentions} {extra_member_mentions} @@ -1612,9 +1621,10 @@ async def check_close_event_change(self) -> Tuple[bool, bool]: mogi.gathering = False self.cur_extension_message = None return True - num_leftover_teams = mogi.count_registered() % (12 // mogi.max_player_per_team) + num_leftover_teams = mogi.count_registered() % ( + mogi.players_per_room // mogi.max_player_per_team) num_needed_teams = ( - 12 // mogi.max_player_per_team) - num_leftover_teams + mogi.players_per_room // mogi.max_player_per_team) - num_leftover_teams if common.SERVER is common.Server.MKW and ( force_start_time - timedelta( @@ -1703,7 +1713,7 @@ async def scheduler_mogi_start(self): self.ongoing_event.started = True self.ongoing_event.gathering = True await self.unlockdown(self.ongoing_event.mogi_channel) - format_str = f"{self.ongoing_event.format} " if self.ongoing_event.format else "" + format_str = f"**{self.ongoing_event.format}** " if self.ongoing_event.format else "" await self.ongoing_event.mogi_channel.send( f"A queue is gathering for the {format_str}mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") @@ -1751,13 +1761,21 @@ async def schedule_que_event(self): # it's joining period and during its extension period if next_event_start_time < datetime.now(timezone.utc): return + format = None + while len( + self.forced_format_times) > 0 and datetime.now(timezone.utc) > self.forced_format_times[0][0]: + self.forced_format_times.pop(0) if len( self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: last_event = self.forced_format_times.pop(0) format = last_event[1] if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: await self.autoschedule_forced_format_times() + + while len( + self.sq_times) > 0 and datetime.now(timezone.utc) > self.sq_times[0]: + self.sq_times.pop(0) if len( self.sq_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.sq_times[0]: self.sq_times.pop(0) @@ -1773,9 +1791,11 @@ async def schedule_que_event(self): await self.MOGI_CHANNEL.send( "Squad Queue is currently going on! The queue will remain closed.") return + self.next_event = Mogi(sq_id=1, max_players_per_team=1, - players_per_room=12, + players_per_room=self.PLAYERS_PER_ROOM, + buttons=self.ALLOWED_VOTE_BUTTONS, mogi_channel=self.MOGI_CHANNEL, is_automated=True, start_time=next_event_start_time, @@ -1911,6 +1931,8 @@ async def print_vote_button_info(self, interaction): for btn in self.ALLOWED_VOTE_BUTTONS] str += ", ".join(format_names) + self.dump_staff_settings() + await interaction.response.send_message(str) @app_commands.command(name="update_players_per_room") @@ -1920,6 +1942,8 @@ async def update_players_per_room(self, interaction: discord.Interaction, new_am self.PLAYERS_PER_ROOM = new_amount self.generate_vote_buttons() + common.CONFIG["PLAYERS_PER_ROOM"] = new_amount + await self.print_vote_button_info(interaction) def generate_vote_buttons(self): @@ -1969,6 +1993,13 @@ async def add_vote_button(self, interaction: discord.Interaction, vote_string: s await interaction.response.send_message("Invalid format name.", ephemeral=True) return + if self.PLAYERS_PER_ROOM % num_players_per_team != 0: + await interaction.response.send_message( + f"Invalid team size. `{num_players_per_team}` must divide evenly into `{self.PLAYERS_PER_ROOM}`.", + ephemeral=True + ) + return + self.ALLOWED_VOTE_BUTTONS.append({ "num_players_per_team": num_players_per_team, "format_name": vote_string @@ -2035,14 +2066,22 @@ async def debug_add_team(self, ctx, members: commands.Greedy[discord.Member]): check_players = [member] check_players.extend(members) players = self.ratings.get_rating(check_players) - for i in range(0, 12): - player = Player(players[0].member, - f"{players[0].lounge_name}{i + 1}", - players[0].mmr + (10 * i)) + player_member = member + player_name = member.display_name + player_mmr = 5000 + if len(players) > 0 and players[0]: + player_member = players[0].member + player_name = players[0].lounge_name + player_mmr = players[0].mmr + + for i in range(0, self.PLAYERS_PER_ROOM): + player = Player(player_member, + f"{player_name}{i + 1}", + player_mmr + (10 * i)) player.confirmed = True squad = Team([player]) mogi.teams.append(squad) - msg = f"{players[0].lounge_name} added 12 times." + msg = f"{player_name} added {self.PLAYERS_PER_ROOM} times." await self.queue_or_send(ctx, msg) if await self.check_close_event_change(): await self.launch_mogi() diff --git a/example_configs/prod/mk8dx_prod_config.json b/example_configs/prod/mk8dx_prod_config.json index 7a1bbf7..8d5c918 100644 --- a/example_configs/prod/mk8dx_prod_config.json +++ b/example_configs/prod/mk8dx_prod_config.json @@ -32,7 +32,7 @@ "sec_between_queue_msgs": 2, "username": "username", "password": "password", - "url": "https://www.mk8dx-lounge.com", + "url": "https://lounge.mkcentral.com/", "track_type": "rt", "FIRST_EVENT_TIME": 0, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", diff --git a/example_configs/prod/mkworld_prod_config.json b/example_configs/prod/mkworld_prod_config.json index ac7d242..ccdf02d 100644 --- a/example_configs/prod/mkworld_prod_config.json +++ b/example_configs/prod/mkworld_prod_config.json @@ -4,18 +4,18 @@ "lounge": "MKWorld", "lounge_description": "Valid options are MKW, MKWorld, or MK8DX", "guild_id": 1326380310233092157, - "queue_join_channel": 1186158671525318719, - "queue_sub_channel": 1186158767029620766, - "queue_list_channel": 1221665101904478340, - "queue_history_channel": 1194440106266865684, - "queue_general_channel": 1186158671525318719, - "queue_schedule_channel": 1274492585917812807, + "queue_join_channel": 1349069297955242038, + "queue_sub_channel": 1349069321942339664, + "queue_list_channel": 1349069264157671564, + "queue_history_channel": 1349069358554677380, + "queue_general_channel": 1349069297955242038, + "queue_schedule_channel": 1349069338237206589, "admin_roles": [ - 464610815299944448, - 910528103225389107 + 1326381661558014086, + 1326382705969205341 ], "helper_staff_roles": [ - 910528103225389107 + 1326381688900812800 ], "members_for_channels": [ 934740707934081035, @@ -23,24 +23,25 @@ 735910635082219570 ], "roles_for_channels": [], - "PLACEMENT_PLAYER_MMR": 1500, - "placement_role_id": 730980761322389504, - "frequently_tagged_role_id": 682445864400453739, - "restricted_role_id": 600495108999086090, - "muted_role_id": 600495108999086090, + "PLAYERS_PER_ROOM": 24, + "PLACEMENT_PLAYER_MMR": 5000, + "placement_role_id": 1381068401908252833, + "frequently_tagged_role_id": 1382218727445102653, + "restricted_role_id": 1381068109359616050, + "muted_role_id": 1349068927489278074, "queue_messages": true, "sec_between_queue_msgs": 2, "username": "username", "password": "password", - "url": "https://www.mk8dx-lounge.com", + "url": "https://lounge.mkcentral.com/", "track_type": "rt", "FIRST_EVENT_TIME": 0, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", - "FORCED_FORMAT_ORDER": ["FFA", "3v3", "FFA", "4v4"], - "FORCED_FORMAT_ORDER_OFFSET": 3, + "FORCED_FORMAT_ORDER": ["FFA"], + "FORCED_FORMAT_ORDER_OFFSET": 0, "FORCED_FORMAT_HOURLY_OFFSET": 0, - "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 1, - "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": 9, + "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 24, + "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": 1, "QUEUE_OPEN_TIME": 60, "JOINING_TIME": 55, "DISPLAY_OFFSET_MINUTES": 5, diff --git a/mmr.py b/mmr.py index 44d1cbb..bd7a1b1 100644 --- a/mmr.py +++ b/mmr.py @@ -59,7 +59,7 @@ async def update_ratings(self): self.first_run_complete = True async def _pull_mk8dx_ratings(self) -> bool: - url = f"""{common.CONFIG["url"]}/api/player/list""" + url = f"""{common.CONFIG["url"]}/api/player/list?game=mk8dx""" return await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) async def _pull_mkw_ratings(self) -> bool: @@ -67,8 +67,8 @@ async def _pull_mkw_ratings(self) -> bool: return await Ratings._pull_ratings(url, self._parse_mkw_ratings, self._validate_mkw_response) async def _pull_mkworld_ratings(self) -> bool: - url = f"""{common.CONFIG["url"]}/api/player/list""" - return await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mk8dx_response) + url = f"""{common.CONFIG["url"]}/api/player/list?game=mkworld""" + return await Ratings._pull_ratings(url, self._parse_mk8dx_ratings, self._validate_mkworld_response) @staticmethod async def _pull_ratings(url, parser, validator) -> bool: @@ -137,6 +137,42 @@ def _parse_mk8dx_ratings(self, results: dict): "mmr") is None else player.get("mmr") self.ratings[discord_id] = (rating, player["name"]) + def _validate_mkworld_response(self, results: dict): + if not isinstance(results, dict): + raise BadRatingData("Response is not a dictionary") + all_players = results.get("players") + if all_players is None: + raise BadRatingData( + "Key word 'players' not found in JSON response.") + # + required_player_amount = 100 + if len(all_players) < required_player_amount: + raise BadPlayerDataLength( + f"Not enough players found in the JSON response. Required {required_player_amount} players in JSON response, only found {len(all_players)} players in JSON response." + "") + + strongly_required_fields = [("name", str)] + weakly_required_fields = [("discordId", str), ("mmr", int)] + for player in all_players: + # Ensure all strongly required fields are in the player JSON and + # that the type is correct + for strongly_req_field_name, strongly_req_field_type in strongly_required_fields: + if strongly_req_field_name not in player: + raise BadPlayerData( + f"Missing required field '{strongly_req_field_name}' in the following player: {player}") + field_data = player[strongly_req_field_name] + if not isinstance(field_data, strongly_req_field_type): + raise BadPlayerData( + f"For field '{strongly_req_field_name}', expected type '{strongly_req_field_type}' received {type(field_data)} for player: {player}") + # Ensure that if the weakly required field is in the JSON, the type + # is correct + for weakly_req_field_name, weakly_req_field_type in weakly_required_fields: + if weakly_req_field_name in player: + field_data = player[weakly_req_field_name] + if not isinstance(field_data, weakly_req_field_type): + raise BadPlayerData( + f"For field '{weakly_req_field_name}', expected type '{weakly_req_field_type}' received {type(field_data)} for player: {player}") + def _validate_mkw_response(self, results: dict): if not isinstance(results, dict): raise BadRatingData("Response is not a dictionary") diff --git a/mogi_objects.py b/mogi_objects.py index 72fba87..b3429ce 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -52,6 +52,7 @@ def __init__( sq_id: int, max_players_per_team: int, players_per_room: int, + buttons, mogi_channel: discord.TextChannel, is_automated=False, start_time=None, @@ -65,6 +66,7 @@ def __init__( self.sq_id = sq_id self.max_player_per_team = max_players_per_team self.players_per_room = players_per_room + self.buttons = buttons self.mogi_channel = mogi_channel self.teams: List[Team] = [] self.rooms: List[Room] = [] @@ -576,12 +578,7 @@ def __init__( self.teams_text = "" self.found_winner = False self.penalty_time = penalty_time - self.votes = {"FFA": [], - "2v2": [], - "3v3": [], - "4v4": [], - "6v6": [] - } + self.votes = {button["format_name"]: [] for button in mogi.buttons} async def create_message(self): if self.mogi.format: @@ -590,24 +587,14 @@ async def create_message(self): view=self ) - if self.mogi.format == "FFA": - await self.make_teams(1, "FFA") - elif self.mogi.format == "2v2": - await self.make_teams(2, "2v2") - elif self.mogi.format == "3v3": - await self.make_teams(3, "3v3") - elif self.mogi.format == "4v4": - await self.make_teams(4, "4v4") - elif self.mogi.format == "6v6": - await self.make_teams(6, "6v6") - else: - self.add_button("FFA", self.general_vote_callback) - self.add_button("2v2", self.general_vote_callback) - self.add_button("3v3", self.general_vote_callback) - self.add_button("4v4", self.general_vote_callback) + players_per_team = 1 if self.mogi.format == "FFA" else int( + self.mogi.format.split('v')[0]) - if common.SERVER is common.Server.MKW: - self.add_button("6v6", self.general_vote_callback) + await self.make_teams(players_per_team, self.mogi.format) + else: + for button in self.mogi.buttons: + format_name = button["format_name"] + self.add_button(format_name, self.general_vote_callback) self.message = await self.room_channel.send( view=self @@ -637,15 +624,11 @@ async def make_teams(self, players_per_team: int, vote_str: str): msg = "ㅤ\n" if not self.mogi.format: - msg = f"""**Poll Ended!** - -1) FFA - {len(self.votes['FFA'])} -2) 2v2 - {len(self.votes['2v2'])} -3) 3v3 - {len(self.votes['3v3'])} -4) 4v4 - {len(self.votes['4v4'])} -""" - if common.SERVER is common.Server.MKW: - msg += f"5) 6v6 - {len(self.votes['6v6'])}\n" + msg = "**Poll Ended!**\n\n" + + for i, (format_name, votes) in enumerate(self.votes.items(), start=1): + msg += f"{i}) {format_name} - {len(votes)}\n" + msg += f"Winner: {vote_str}\n\n" self.header_text = "" @@ -654,7 +637,7 @@ async def make_teams(self, players_per_team: int, vote_str: str): msg += self.header_text + "\n" teams = [] - teams_per_room = 12 // players_per_team + teams_per_room = self.mogi.players_per_room // players_per_team for j in range(teams_per_room): players = sorted(self.players[j * players_per_team:(j + 1) * players_per_team], key=lambda player: player.mmr, reverse=True) @@ -717,13 +700,12 @@ async def make_teams(self, players_per_team: int, vote_str: str): pen_time = mkw_room_open_time + \ timedelta(minutes=self.penalty_time) msg += f"\nRoom open at :{mkw_room_open_time.minute:02}, penalty at :{pen_time.minute:02}. Good luck!" - elif common.SERVER is common.Server.MK8DX: + elif common.SERVER is common.Server.MKWorld: msg += f"\n{potential_host_str}" msg += f"\nRoom open at :{room_open_time.minute:02}, penalty at :{penalty_time.minute:02}. Good luck!" room.teams = teams - self.found_winner = True try: await self.room_channel.send(msg) if common.CONFIG["USE_THREADS"]: @@ -737,19 +719,14 @@ async def make_teams(self, players_per_team: int, vote_str: str): async def find_winner(self): if not self.found_winner: + self.found_winner = True most_votes = len(max(self.votes.values(), key=len)) winners = [] - if len(self.votes["FFA"]) == most_votes: - winners.append((1, "FFA")) - if len(self.votes["2v2"]) == most_votes: - winners.append((2, "2v2")) - if len(self.votes["3v3"]) == most_votes: - winners.append((3, "3v3")) - if len(self.votes["4v4"]) == most_votes: - winners.append((4, "4v4")) - if common.SERVER is common.Server.MKW and len( - self.votes["6v6"]) == most_votes: - winners.append((6, "6v6")) + for format_name, votes in self.votes.items(): + if len(votes) == most_votes: + players_per_team = 1 if format_name == "FFA" else int( + format_name.split('v')[0]) + winners.append((players_per_team, format_name)) winner = random.choice(winners) @@ -776,9 +753,8 @@ async def general_vote_callback(self, interaction: discord.Interaction): await interaction.response.send_message("You are not a player in this event.", ephemeral=True) return vote = interaction.data['custom_id'] - players_per_team = 1 - if vote != "FFA": - players_per_team = int(vote[0]) + players_per_team = 1 if vote == "FFA" else int( + vote.split('v')[0]) original_vote = None for vote_option, voter_ids in self.votes.items(): if interaction.user.id in voter_ids: @@ -786,7 +762,7 @@ async def general_vote_callback(self, interaction: discord.Interaction): voter_ids.remove(interaction.user.id) if original_vote != vote: # They changed their vote or are a new voter self.votes[vote].append(interaction.user.id) - if len(self.votes[vote]) == 6: + if len(self.votes[vote]) >= self.mogi.players_per_room / 2: self.found_winner = True # This fixes a race condition await self.make_teams(players_per_team, vote) for curr_button in self.children: diff --git a/unit_testing.py b/unit_testing.py index 1435166..9e84976 100644 --- a/unit_testing.py +++ b/unit_testing.py @@ -171,6 +171,9 @@ def setUpClass(cls): elif common.SERVER is common.Server.MK8DX: cls.threshold_function = lambda players: SquadQueue.mk8dx_players_allowed( players, cls.threshold) + elif common.SERVER is common.Server.MKWorld: + cls.threshold_function = lambda players: SquadQueue.mkworld_players_allowed( + players, cls.threshold) else: raise Exception( "Lounge parameter in config is invalid, unit tests will fail.") From ecd114d69075184c9cfd316683968d8a9a52b599 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 11 Jun 2025 01:50:49 -0700 Subject: [PATCH 279/292] created command to change format of ongoing queue --- cogs/SquadQueue.py | 64 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index de4ba12..a8a418c 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -589,7 +589,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): squad = Team(players) mogi.teams.append(squad) host_str = " as a host " if host else " " - format_str = f"the {mogi.format} " if mogi.format else "" + format_str = f"the __**{mogi.format}**__ " if mogi.format else "" msg += f"{players[0].lounge_name} joined {format_str}queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" if common.SERVER is common.Server.MKW: if players[0].is_matchmaking_mmr_adjusted: @@ -1087,6 +1087,54 @@ async def reset_bot(self, interaction: discord.Interaction): self.LAUNCH_NEW_EVENTS = True await interaction.response.send_message("All events have been deleted. Queue will restart shortly.") + async def update_current_event_autocomplete( + self, interaction: discord.Interaction, current: str + ) -> List[app_commands.Choice[str]]: + total = self.PLAYERS_PER_ROOM + choices = [] + + for i in range(1, total): + if total % i != 0: + continue + fmt = "FFA" if i == 1 else f"{i}v{i}" + if current.lower() in fmt.lower(): + choices.append(app_commands.Choice(name=fmt, value=fmt)) + + if "voting".startswith(current.lower()): + choices.append(app_commands.Choice(name="Voting", value="Voting")) + + return choices[:25] + + @app_commands.command(name="update_current_event_format") + @app_commands.autocomplete(format_string=update_current_event_autocomplete) + @app_commands.guild_only() + async def update_current_event_format(self, interaction: discord.Interaction, format_string: str): + """Update the currently gathering mogi's format. Staff use only.""" + + if format_string == "Voting": + self.ongoing_event.format = None + elif format_string == "FFA": + self.ongoing_event.format = "FFA" + else: + try: + num_players_per_team = int(format_string.split("v")[0]) + except (ValueError, IndexError): + await interaction.response.send_message( + f"Invalid format string: `{format_string}`", ephemeral=True + ) + return + + if self.PLAYERS_PER_ROOM % num_players_per_team != 0: + await interaction.response.send_message( + f"`{format_string}` is not valid — {num_players_per_team} does not divide evenly into {self.PLAYERS_PER_ROOM}.", + ephemeral=True + ) + return + + self.ongoing_event.format = format_string + + await interaction.response.send_message(f"The currently gathering event's format is now {format_string}") + async def timezone_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: current_upper = current.upper() now = datetime.now(pytz.utc) @@ -1770,6 +1818,20 @@ async def schedule_que_event(self): self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: last_event = self.forced_format_times.pop(0) format = last_event[1] + + if format != "FFA": + try: + num_players_per_team = int(format.split("v")[0]) + except (ValueError, IndexError): + print( + f"Invalid format string encountered: {format}", flush=True) + format = None + + if self.PLAYERS_PER_ROOM % num_players_per_team != 0: + print( + f"Format {format} is invalid: {num_players_per_team} does not divide evenly into {self.PLAYERS_PER_ROOM}") + format = None + if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: await self.autoschedule_forced_format_times() From fa17d53361d85becdc8945ff4d358c42a5bda811 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 11 Jun 2025 02:01:23 -0700 Subject: [PATCH 280/292] added tiers unique to mkworld --- mogi_objects.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mogi_objects.py b/mogi_objects.py index b3429ce..3bc22e0 100644 --- a/mogi_objects.py +++ b/mogi_objects.py @@ -326,7 +326,7 @@ def tier(self) -> str: elif common.SERVER is common.Server.MKW: return str(get_tier_mkw(self.avg_mmr, self.players)) elif common.SERVER is common.Server.MKWorld: - return get_tier_mk8dx(round(self.avg_mmr) - 500) + return get_tier_mkworld(round(self.avg_mmr) - 500) @property def tier_collection(self) -> str: @@ -849,10 +849,6 @@ def get_tier_mkw(mmr: int, players: List[Player]): return '1' else: return '0' - # for tier in tier_info: - # if (tier["minimum_mmr"] is None or mmr >= tier["minimum_mmr"]) and ( - # tier["maximum_mmr"] is None or mmr <= tier["maximum_mmr"]): - # return tier["ladder_order"] def player_threshold_check(mmr: int, players: List[Player]): @@ -893,3 +889,7 @@ def get_tier_mk8dx(mmr: int): return 'FG' else: return 'G' + + +def get_tier_mkworld(mmr: int): + return 'Q' From 223e0e2c7f524c7ab5780705ad195600eda1e9bc Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Wed, 11 Jun 2025 18:02:17 -0700 Subject: [PATCH 281/292] create new forced format if applicable and empty --- cogs/SquadQueue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index a8a418c..4acd6bf 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1814,6 +1814,8 @@ async def schedule_que_event(self): while len( self.forced_format_times) > 0 and datetime.now(timezone.utc) > self.forced_format_times[0][0]: self.forced_format_times.pop(0) + if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: + await self.autoschedule_forced_format_times() if len( self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: last_event = self.forced_format_times.pop(0) From 31b73bdd8874e7b3b0b5f8180381844ad4cf604f Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 15 Jun 2025 21:10:48 -0700 Subject: [PATCH 282/292] updating players per room/buttons updates current mogi as well --- cogs/SquadQueue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 4acd6bf..5208aae 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -590,7 +590,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): mogi.teams.append(squad) host_str = " as a host " if host else " " format_str = f"the __**{mogi.format}**__ " if mogi.format else "" - msg += f"{players[0].lounge_name} joined {format_str}queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" + msg += f"{players[0].lounge_name} joined the {self.ongoing_event.players_per_room} player {format_str}queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" if common.SERVER is common.Server.MKW: if players[0].is_matchmaking_mmr_adjusted: msg += f"\nPlayer considered {players[0].adjusted_mmr} MMR for matchmaking purposes." @@ -725,7 +725,7 @@ async def list_task(self): msg = f"**Queue closing: {discord.utils.format_dt(mogi.start_time)}**\n\n" format_str = f"{mogi.format} " if mogi.format else "" - msg += f"**Current {format_str}Mogi List:**\n" + msg += f"**Current {mogi.players_per_room} Player {format_str}Mogi List:**\n" if common.SERVER is common.Server.MKW: for i, player in enumerate(on_time_players, 1): adjusted_mmr_text = f"MMR -> {player.adjusted_mmr} " if player.is_matchmaking_mmr_adjusted else "" @@ -1763,7 +1763,7 @@ async def scheduler_mogi_start(self): await self.unlockdown(self.ongoing_event.mogi_channel) format_str = f"**{self.ongoing_event.format}** " if self.ongoing_event.format else "" await self.ongoing_event.mogi_channel.send( - f"A queue is gathering for the {format_str}mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") + f"A queue is gathering for the {self.ongoing_event.players_per_room} player {format_str}mogi {discord.utils.format_dt(self.ongoing_event.start_time, style='R')} - Type `/c` to join, `/ch` to join and volunteer to host, and `/d` to drop.") @tasks.loop(seconds=20.0) async def sqscheduler(self): @@ -1996,6 +1996,7 @@ async def print_vote_button_info(self, interaction): str += ", ".join(format_names) self.dump_staff_settings() + self.ongoing_event.buttons = self.ALLOWED_VOTE_BUTTONS await interaction.response.send_message(str) @@ -2008,6 +2009,7 @@ async def update_players_per_room(self, interaction: discord.Interaction, new_am common.CONFIG["PLAYERS_PER_ROOM"] = new_amount + self.ongoing_event.players_per_room = new_amount await self.print_vote_button_info(interaction) def generate_vote_buttons(self): From 64c7f7ed23304c524a6e15640147e30f21f44d94 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Thu, 19 Jun 2025 02:59:11 -0700 Subject: [PATCH 283/292] created way to schedule future mogis with specific amounts of players --- cogs/SquadQueue.py | 131 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 16 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5208aae..dcddde0 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -170,6 +170,9 @@ def __init__(self, bot): self.sq_times: List[datetime] = [] + self.specific_player_amount_queue_times: List[Tuple[datetime, int]] = [ + ] + self.forced_format_times: List[Tuple[datetime, str]] = [] self.forced_format_order: List[str] = bot.config["FORCED_FORMAT_ORDER"] @@ -355,7 +358,8 @@ async def on_ready(self): schedule_channel_id) if not self.ALLOWED_VOTE_BUTTONS: - self.generate_vote_buttons() + self.ALLOWED_VOTE_BUTTONS = SquadQueue.generate_vote_buttons( + self.PLAYERS_PER_ROOM) def is_queuebot(m: discord.Message): return m.author.id == self.bot.user.id @@ -793,8 +797,8 @@ async def delete_list_messages(self, new_list_size: int): except Exception as e: print(traceback.format_exc()) - async def update_forced_format_list(self): - """Update the list of Mogis with scheduled formats""" + async def update_schedule_channel(self): + """Update the Schedule Channel""" if self.SCHEDULE_CHANNEL: msg = "" @@ -813,6 +817,14 @@ async def update_forced_format_list(self): msg += "\n" + if len(self.specific_player_amount_queue_times) > 0: + msg += "**List of Mogis with Set Amount of Players:**\n\n" + + for index, event in enumerate(self.specific_player_amount_queue_times): + msg += f"`#{index + 1}` **{event[1]} Players:** {discord.utils.format_dt(event[0])} - {discord.utils.format_dt(event[0], style='R')}\n" + + msg += "\n" + current_rotation = ', '.join(self.forced_format_order) msg += f"**Current Rotation** - {current_rotation or 'N/A'}\n" @@ -1271,7 +1283,7 @@ async def schedule_forced_format_times(self, interaction: discord.Interaction, t self.forced_format_times = sorted(updated_list, key=lambda x: x[0]) if self.SCHEDULE_CHANNEL: - await self.update_forced_format_list() + await self.update_schedule_channel() msg = f"Added new {format} mogi at {discord.utils.format_dt(dt)}" @@ -1305,7 +1317,7 @@ async def autoschedule_forced_format_times(self): self.forced_format_times = sorted(updated_list, key=lambda x: x[0]) if self.SCHEDULE_CHANNEL: - await self.update_forced_format_list() + await self.update_schedule_channel() async def remove_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: choices = [ @@ -1334,7 +1346,7 @@ async def remove_forced_format_time(self, interaction: discord.Interaction, time await interaction.response.send_message(f"No entry found for {time}.") if self.SCHEDULE_CHANNEL: - await self.update_forced_format_list() + await self.update_schedule_channel() except ValueError: await interaction.response.send_message("Invalid datetime format provided.") @@ -1344,10 +1356,84 @@ async def clear_forced_format_times(self, interaction: discord.Interaction): """Clears current list of forced format times. Staff use only.""" self.forced_format_times = [] if self.SCHEDULE_CHANNEL: - await self.update_forced_format_list() + await self.update_schedule_channel() await interaction.response.send_message("Cleared list of Forced Format Times.") + @app_commands.command(name="schedule_player_amount_times") + @app_commands.autocomplete(tzone=timezone_autocomplete) + @app_commands.autocomplete(date=date_autocomplete) + @app_commands.autocomplete(time=time_autocomplete) + @app_commands.guild_only() + async def schedule_player_amount_times(self, interaction: discord.Interaction, tzone: str, date: str, time: str, player_amount: Range[int, 2]): + """Schedule mogis to be rooms of certain sizes. Staff use only.""" + curr_time = datetime.now(timezone.utc) + dt = datetime.fromisoformat(time).replace(tzinfo=timezone.utc) + timestamp = dt.timestamp() + + if curr_time > dt: + msg = "" + msg += f"Timestamp {timestamp} represents {time} and is in the past, submit a future date.\n" + msg += "This timestamp has not been added." + await interaction.response.send_message(msg) + return + + new_event = (dt, player_amount) + event_dict = { + event[0]: event for event in self.specific_player_amount_queue_times} + event_dict[new_event[0]] = new_event + updated_list = list(event_dict.values()) + self.specific_player_amount_queue_times = sorted( + updated_list, key=lambda x: x[0]) + + if self.SCHEDULE_CHANNEL: + await self.update_schedule_channel() + + msg = f"Added new {player_amount} player mogi at {discord.utils.format_dt(dt)}" + + await interaction.response.send_message(msg) + + async def remove_player_amount_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + choices = [ + app_commands.Choice( + name=f"{dt.strftime('%Y-%m-%d %H:%M:%S')} - {fmt}", value=dt.isoformat() + ) + for dt, fmt in self.specific_player_amount_queue_times[:25] + ] + return choices + + @app_commands.command(name="remove_player_amount_time") + @app_commands.autocomplete(time=remove_player_amount_time_autocomplete) + async def remove_player_amount_time(self, interaction: discord.Interaction, time: str): + """Remove a specific set player amount queue time by datetime. Staff use only.""" + try: + dt_to_remove = datetime.fromisoformat(time) + + original_count = len(self.specific_player_amount_queue_times) + self.specific_player_amount_queue_times = [ + entry for entry in self.specific_player_amount_queue_times if entry[0] != dt_to_remove + ] + + if len(self.specific_player_amount_queue_times) < original_count: + await interaction.response.send_message(f"Removed {discord.utils.format_dt(dt_to_remove, 'f')} from the Set Player Amount times.") + else: + await interaction.response.send_message(f"No entry found for {time}.") + + if self.SCHEDULE_CHANNEL: + await self.update_schedule_channel() + except ValueError: + await interaction.response.send_message("Invalid datetime format provided.") + + @app_commands.command(name="clear_player_amount_times") + @app_commands.guild_only() + async def clear_player_amount_times(self, interaction: discord.Interaction): + """Clears current list of Set Player Amount times. Staff use only.""" + self.specific_player_amount_queue_times = [] + if self.SCHEDULE_CHANNEL: + await self.update_schedule_channel() + + await interaction.response.send_message("Cleared list of Set Player Amount Times.") + @app_commands.command(name="schedule_sq_times") @app_commands.autocomplete(tzone=timezone_autocomplete) @app_commands.autocomplete(date=date_autocomplete) @@ -1642,9 +1728,9 @@ async def add_teams_to_rooms(self, mogi: Mogi, open_time: int, started_automatic self.ongoing_event.assign_roles(guild=self.GUILD)) asyncio.create_task(SquadQueue.handle_voting_and_history( self.ongoing_event, self.HISTORY_CHANNEL)) - if mogi.format: + if mogi.format or mogi.players_per_room != self.PLAYERS_PER_ROOM: if self.SCHEDULE_CHANNEL: - await self.update_forced_format_list() + await self.update_schedule_channel() self.old_events.append(self.ongoing_event) self.ongoing_event = None @@ -1837,6 +1923,17 @@ async def schedule_que_event(self): if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: await self.autoschedule_forced_format_times() + player_amount = self.PLAYERS_PER_ROOM + buttons = self.ALLOWED_VOTE_BUTTONS + while len( + self.specific_player_amount_queue_times) > 0 and datetime.now(timezone.utc) > self.specific_player_amount_queue_times[0][0]: + self.specific_player_amount_queue_times.pop(0) + if len( + self.specific_player_amount_queue_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.specific_player_amount_queue_times[0][0]: + last_event = self.specific_player_amount_queue_times.pop(0) + player_amount = last_event[1] + buttons = SquadQueue.generate_vote_buttons(player_amount) + while len( self.sq_times) > 0 and datetime.now(timezone.utc) > self.sq_times[0]: self.sq_times.pop(0) @@ -1858,8 +1955,8 @@ async def schedule_que_event(self): self.next_event = Mogi(sq_id=1, max_players_per_team=1, - players_per_room=self.PLAYERS_PER_ROOM, - buttons=self.ALLOWED_VOTE_BUTTONS, + players_per_room=player_amount, + buttons=buttons, mogi_channel=self.MOGI_CHANNEL, is_automated=True, start_time=next_event_start_time, @@ -2005,26 +2102,28 @@ async def print_vote_button_info(self, interaction): async def update_players_per_room(self, interaction: discord.Interaction, new_amount: Range[int, 2]): """Sets the new amount of player per room for queue. Staff use only.""" self.PLAYERS_PER_ROOM = new_amount - self.generate_vote_buttons() + self.ALLOWED_VOTE_BUTTONS = SquadQueue.generate_vote_buttons( + self.PLAYERS_PER_ROOM) common.CONFIG["PLAYERS_PER_ROOM"] = new_amount self.ongoing_event.players_per_room = new_amount await self.print_vote_button_info(interaction) - def generate_vote_buttons(self): + @staticmethod + def generate_vote_buttons(player_amount): new_vote_button_list = [] - iterator = iter(range(1, self.PLAYERS_PER_ROOM)) + iterator = iter(range(1, player_amount)) for i in iterator: - if self.PLAYERS_PER_ROOM % i == 0: + if player_amount % i == 0: format_name = "FFA" if i == 1 else f"{i}v{i}" new_vote_button_list.append({ "num_players_per_team": i, "format_name": format_name }) - self.ALLOWED_VOTE_BUTTONS = new_vote_button_list + return new_vote_button_list async def add_vote_button_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: total = self.PLAYERS_PER_ROOM From e4c0f95d6bd666be6332134c9410dfb654bec6e3 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Thu, 19 Jun 2025 17:07:45 -0700 Subject: [PATCH 284/292] see daily queue schedule for mkworld --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index dcddde0..9013808 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -804,7 +804,7 @@ async def update_schedule_channel(self): msg = "" # maybe for mkworld if not 1 hour blocks for mogis - if common.SERVER is common.Server.MKW: + if common.SERVER is common.Server.MKW or common.SERVER is common.Server.MKWorld: msg += "**Daily Queue Times:**\n\n" curr_time = self.compute_next_event_open_time() + self.JOINING_TIME + \ From 46bc5845e9deac3b337ea5f0f5a611575234607d Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sat, 21 Jun 2025 17:06:02 -0700 Subject: [PATCH 285/292] fixed join text --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 9013808..48abd54 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -593,7 +593,7 @@ async def join_queue(self, interaction: discord.Interaction, host=False): squad = Team(players) mogi.teams.append(squad) host_str = " as a host " if host else " " - format_str = f"the __**{mogi.format}**__ " if mogi.format else "" + format_str = f"__**{mogi.format}**__ " if mogi.format else "" msg += f"{players[0].lounge_name} joined the {self.ongoing_event.players_per_room} player {format_str}queue{host_str}closing at {discord.utils.format_dt(mogi.start_time)}, `[{mogi.count_registered()} players]`" if common.SERVER is common.Server.MKW: if players[0].is_matchmaking_mmr_adjusted: From a0bffddab9e2b57087236bee649abd08350fa575 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 22 Jun 2025 21:08:30 -0700 Subject: [PATCH 286/292] lowered polling of mmr to 30 minutes --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 48abd54..92020a9 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -2068,7 +2068,7 @@ async def refresh_helper_roles(self): except Exception as e: print(traceback.format_exc()) - @tasks.loop(minutes=10) + @tasks.loop(minutes=30) async def refresh_ratings(self): """Refreshes the ratings""" try: From 0a531d89dcb9f1dd7b32d829a6adc9af9e967ce1 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 22 Jun 2025 21:09:18 -0700 Subject: [PATCH 287/292] fixed security issue commands usable in dms --- cogs/SquadQueue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 92020a9..b04e1b2 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1330,6 +1330,7 @@ async def remove_time_autocomplete(self, interaction: discord.Interaction, curre @app_commands.command(name="remove_forced_format_time") @app_commands.autocomplete(time=remove_time_autocomplete) + @app_commands.guild_only() async def remove_forced_format_time(self, interaction: discord.Interaction, time: str): """Remove a specific forced format time by datetime. Staff use only.""" try: @@ -1404,6 +1405,7 @@ async def remove_player_amount_time_autocomplete(self, interaction: discord.Inte @app_commands.command(name="remove_player_amount_time") @app_commands.autocomplete(time=remove_player_amount_time_autocomplete) + @app_commands.guild_only() async def remove_player_amount_time(self, interaction: discord.Interaction, time: str): """Remove a specific set player amount queue time by datetime. Staff use only.""" try: @@ -1484,6 +1486,7 @@ async def sq_time_autocomplete(self, interaction: discord.Interaction, current: @app_commands.command(name="remove_sq_time") @app_commands.autocomplete(time=sq_time_autocomplete) + @app_commands.guild_only() async def remove_sq_time(self, interaction: discord.Interaction, time: str): """Remove a specific Squad Queue time. Staff use only.""" dt_to_remove = datetime.fromisoformat(time) From d00f8a491742fcb136b40a70a577448ed8a253bf Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 22 Jun 2025 21:10:00 -0700 Subject: [PATCH 288/292] queues that should be forced format when ff queue is emptied get scheduled --- cogs/SquadQueue.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index b04e1b2..cc90b70 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -380,8 +380,8 @@ def is_queuebot(m: discord.Message): if schedule_channel_id: await self.SCHEDULE_CHANNEL.purge(check=is_queuebot, after=purge_after) # if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue: - if self.queues_between_forced_format_queue: - await self.autoschedule_forced_format_times() + # if self.queues_between_forced_format_queue: + # await self.autoschedule_forced_format_times() except BaseException: print("Purging Schedule channel failed", flush=True) print(traceback.format_exc()) @@ -1295,11 +1295,35 @@ async def autoschedule_forced_format_times(self): time_between_ff_queues = self.queues_between_forced_format_queue * self.QUEUE_OPEN_TIME last_event = self.FORCED_FORMAT_FIRST_EVENT + next_event_open_time = self.compute_next_event_open_time() start_index = 0 - while last_event < datetime.now(timezone.utc): + while last_event < datetime.now(timezone.utc) - self.QUEUE_OPEN_TIME: last_event += time_between_ff_queues start_index += 1 + format = None + retroactive_time = last_event + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES + if retroactive_time == next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES: + current_index = ( + start_index + self.FORCED_FORMAT_ORDER_OFFSET) % len(self.forced_format_order) + format = self.forced_format_order[current_index] + + if format != "FFA": + try: + num_players_per_team = int(format.split("v")[0]) + except (ValueError, IndexError): + print( + f"Invalid format string encountered: {format}", flush=True) + format = None + + if self.PLAYERS_PER_ROOM % num_players_per_team != 0: + print( + f"Format {format} is invalid: {num_players_per_team} does not divide evenly into {self.PLAYERS_PER_ROOM}") + format = None + + last_event += time_between_ff_queues + start_index += 1 + total_iterations = self.FORCED_FORMAT_AUTOSCHEDULE_AMOUNT * \ len(self.forced_format_order) @@ -1319,6 +1343,8 @@ async def autoschedule_forced_format_times(self): if self.SCHEDULE_CHANNEL: await self.update_schedule_channel() + return format + async def remove_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: choices = [ app_commands.Choice( @@ -1904,7 +1930,7 @@ async def schedule_que_event(self): self.forced_format_times) > 0 and datetime.now(timezone.utc) > self.forced_format_times[0][0]: self.forced_format_times.pop(0) if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: - await self.autoschedule_forced_format_times() + format = await self.autoschedule_forced_format_times() if len( self.forced_format_times) > 0 and next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES == self.forced_format_times[0][0]: last_event = self.forced_format_times.pop(0) @@ -1924,7 +1950,7 @@ async def schedule_que_event(self): format = None if len(self.forced_format_order) > 0 and self.queues_between_forced_format_queue and len(self.forced_format_times) == 0: - await self.autoschedule_forced_format_times() + format = await self.autoschedule_forced_format_times() player_amount = self.PLAYERS_PER_ROOM buttons = self.ALLOWED_VOTE_BUTTONS From ff4e2c780ffb9c4352dc120a2b21b675d29a0331 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 22 Jun 2025 21:29:15 -0700 Subject: [PATCH 289/292] quick fix to scoreboard command --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index cc90b70..52b52a0 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -933,7 +933,7 @@ async def scoreboard(self, interaction: discord.Interaction): await interaction.response.send_message(f"The Thread object cannot be found.", ephemeral=True) return - format_ = round(self.ongoing_event.players_per_room / len(room.teams)) + format_ = round(mogi.players_per_room / len(room.teams)) msg = f"!submit {format_} {room.tier}\n" for team in room.teams: From beb60f6c5c84526f7ec9ea32c853d10e17629edc Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Sun, 22 Jun 2025 22:08:35 -0700 Subject: [PATCH 290/292] fixed bug in autoschedule --- cogs/SquadQueue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 52b52a0..5dc1a8a 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1297,7 +1297,7 @@ async def autoschedule_forced_format_times(self): last_event = self.FORCED_FORMAT_FIRST_EVENT next_event_open_time = self.compute_next_event_open_time() start_index = 0 - while last_event < datetime.now(timezone.utc) - self.QUEUE_OPEN_TIME: + while last_event < datetime.now(timezone.utc) - time_between_ff_queues: last_event += time_between_ff_queues start_index += 1 From 484b6befdd6102b71da7acaf444a66406cfdca22 Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 23 Jun 2025 00:19:04 -0700 Subject: [PATCH 291/292] fixed silly mistake with overwritten variable name --- cogs/SquadQueue.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cogs/SquadQueue.py b/cogs/SquadQueue.py index 5dc1a8a..7880ff5 100644 --- a/cogs/SquadQueue.py +++ b/cogs/SquadQueue.py @@ -1301,25 +1301,24 @@ async def autoschedule_forced_format_times(self): last_event += time_between_ff_queues start_index += 1 - format = None - retroactive_time = last_event + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES - if retroactive_time == next_event_open_time + self.JOINING_TIME + self.DISPLAY_OFFSET_MINUTES: + ongoing_format = None + if last_event == next_event_open_time: current_index = ( start_index + self.FORCED_FORMAT_ORDER_OFFSET) % len(self.forced_format_order) - format = self.forced_format_order[current_index] + ongoing_format = self.forced_format_order[current_index] - if format != "FFA": + if ongoing_format != "FFA": try: - num_players_per_team = int(format.split("v")[0]) + num_players_per_team = int(ongoing_format.split("v")[0]) except (ValueError, IndexError): print( - f"Invalid format string encountered: {format}", flush=True) - format = None + f"Invalid format string encountered: {ongoing_format}", flush=True) + ongoing_format = None if self.PLAYERS_PER_ROOM % num_players_per_team != 0: print( - f"Format {format} is invalid: {num_players_per_team} does not divide evenly into {self.PLAYERS_PER_ROOM}") - format = None + f"Format {ongoing_format} is invalid: {num_players_per_team} does not divide evenly into {self.PLAYERS_PER_ROOM}") + ongoing_format = None last_event += time_between_ff_queues start_index += 1 @@ -1343,7 +1342,7 @@ async def autoschedule_forced_format_times(self): if self.SCHEDULE_CHANNEL: await self.update_schedule_channel() - return format + return ongoing_format async def remove_time_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: choices = [ From 653ec75ef89a50cc16ceb11bb0f44c47accdde1b Mon Sep 17 00:00:00 2001 From: thomasgonda3 Date: Mon, 23 Jun 2025 00:19:19 -0700 Subject: [PATCH 292/292] updated default config values --- example_configs/prod/mkworld_prod_config.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_configs/prod/mkworld_prod_config.json b/example_configs/prod/mkworld_prod_config.json index ccdf02d..95580c4 100644 --- a/example_configs/prod/mkworld_prod_config.json +++ b/example_configs/prod/mkworld_prod_config.json @@ -35,15 +35,15 @@ "password": "password", "url": "https://lounge.mkcentral.com/", "track_type": "rt", - "FIRST_EVENT_TIME": 0, + "FIRST_EVENT_TIME": -12, "FIRST_EVENT_TIME_DESCRIPTION": "First event time is the number of minutes from 0:00 UTC that the bot would run the first event of the day. As an example, if this were set to 5, and the QUEUE_OPEN_TIME were set to 70, and the bot were to reboot at 2:00 UTC, the next event would immediately start and create rooms at 2:25 UTC.", "FORCED_FORMAT_ORDER": ["FFA"], "FORCED_FORMAT_ORDER_OFFSET": 0, "FORCED_FORMAT_HOURLY_OFFSET": 0, "FORCED_FORMAT_AUTOSCHEDULE_AMOUNT": 24, "QUEUES_BETWEEN_FORCED_FORMAT_QUEUE": 1, - "QUEUE_OPEN_TIME": 60, - "JOINING_TIME": 55, + "QUEUE_OPEN_TIME": 72, + "JOINING_TIME": 67, "DISPLAY_OFFSET_MINUTES": 5, "EXTENSION_TIME": 3, "ROOM_JOIN_PENALTY_TIME": 6,