diff --git a/export_assets.py b/export_assets.py index 2e6aa92..81a2af9 100644 --- a/export_assets.py +++ b/export_assets.py @@ -1,11 +1,9 @@ -import hashlib import shutil from pathlib import Path from d4dj_utils.manager.asset_manager import AssetManager -from d4dj_utils.master.master_asset import MasterAsset -from miyu_bot.commands.common.master_asset_manager import MasterFilterManager, hash_master +from miyu_bot.bot.master_asset_manager import hash_master def main(): diff --git a/main.py b/main.py index 7808760..3f3b63f 100644 --- a/main.py +++ b/main.py @@ -3,20 +3,19 @@ import logging import discord from d4dj_utils.manager.asset_manager import AssetManager -from discord.ext import commands -from miyu_bot.commands.common.master_asset_manager import MasterFilterManager +from miyu_bot.bot.bot import D4DJBot +from miyu_bot.bot.master_asset_manager import MasterFilterManager logging.basicConfig(level=logging.INFO) with open('config.json') as f: bot_token = json.load(f)['token'] -bot = commands.Bot(command_prefix='!', case_insensitive=True) asset_manager = AssetManager('assets') +bot = D4DJBot(asset_manager, MasterFilterManager(asset_manager), command_prefix='!', case_insensitive=True) -masters = MasterFilterManager(asset_manager) bot.load_extension('miyu_bot.commands.cogs.card') bot.load_extension('miyu_bot.commands.cogs.event') diff --git a/miyu_bot/commands/common/aliases/event.py b/miyu_bot/bot/aliases/event.py similarity index 100% rename from miyu_bot/commands/common/aliases/event.py rename to miyu_bot/bot/aliases/event.py diff --git a/miyu_bot/bot/bot.py b/miyu_bot/bot/bot.py new file mode 100644 index 0000000..4b03c91 --- /dev/null +++ b/miyu_bot/bot/bot.py @@ -0,0 +1,17 @@ +from d4dj_utils.manager.asset_manager import AssetManager +from discord.ext import commands + +from miyu_bot.bot.master_asset_manager import MasterFilterManager +from miyu_bot.bot.name_aliases import NameAliases + + +class D4DJBot(commands.Bot): + assets: AssetManager + asset_filters: MasterFilterManager + aliases: NameAliases + + def __init__(self, assets, asset_filters, *args, **kwargs): + self.assets = assets + self.asset_filters = asset_filters + self.aliases = NameAliases(assets) + super().__init__(*args, **kwargs) diff --git a/miyu_bot/commands/common/master_asset_manager.py b/miyu_bot/bot/master_asset_manager.py similarity index 82% rename from miyu_bot/commands/common/master_asset_manager.py rename to miyu_bot/bot/master_asset_manager.py index be276f0..f1baa02 100644 --- a/miyu_bot/commands/common/master_asset_manager.py +++ b/miyu_bot/bot/master_asset_manager.py @@ -1,13 +1,12 @@ import hashlib -from functools import lru_cache -from timeit import default_timer from typing import Callable, Any, Optional, Union from d4dj_utils.manager.asset_manager import AssetManager +from d4dj_utils.master.event_master import EventMaster, EventState from d4dj_utils.master.master_asset import MasterDict, MasterAsset from discord.ext import commands -from miyu_bot.commands.common.aliases.event import event_aliases +from miyu_bot.bot.aliases.event import event_aliases from miyu_bot.commands.common.fuzzy_matching import FuzzyFilteredMap, romanize import datetime as dt @@ -22,7 +21,7 @@ class MasterFilterManager: filter_function=lambda m: m.is_released, fallback_naming_function=lambda m: m.id, ) - self.events = MasterFilter( + self.events = EventFilter( self.manager.event_master, aliases=event_aliases, naming_function=lambda e: e.name, @@ -99,6 +98,21 @@ class MasterFilter: return self.default_filter.values() +class EventFilter(MasterFilter): + def get_latest_event(self, ctx: commands.Context) -> EventMaster: + """Returns the oldest event that has not ended or the newest event otherwise.""" + try: + # NY event overlapped with previous event + return min((v for v in self.values(ctx) if v.state() == EventState.Open), + key=lambda e: e.start_datetime) + except ValueError: + try: + return min((v for v in self.values(ctx) if v.state() < EventState.Ended), + key=lambda e: e.start_datetime) + except ValueError: + return max(self.values(ctx), key=lambda v: v.start_datetime) + + def hash_master(master: MasterAsset): return hashlib.md5(master.extended_description().encode('utf-8')).hexdigest() diff --git a/miyu_bot/bot/name_aliases.py b/miyu_bot/bot/name_aliases.py new file mode 100644 index 0000000..72a4ed6 --- /dev/null +++ b/miyu_bot/bot/name_aliases.py @@ -0,0 +1,58 @@ +from functools import cached_property + +from d4dj_utils.manager.asset_manager import AssetManager + + +class NameAliases: + assets: AssetManager + + def __init__(self, assets): + self.assets = assets + + @cached_property + def characters_by_name(self): + characters_by_name = {} + for character in self.assets.character_master.values(): + for name in character.full_name_english.split(): + characters_by_name[name.lower()] = character + return characters_by_name + + @cached_property + def attributes_by_name(self): + return {attribute.en_name: attribute for attribute in self.assets.attribute_master.values()} + + @cached_property + def units_by_name(self): + units_by_name = {unit.name.lower().replace(' ', '_'): unit for unit in self.assets.unit_master.values()} + units_by_name['rondo'] = units_by_name['燐舞曲'] + units_by_name['special'] = units_by_name['スペシャル'] + units_by_name['other'] = units_by_name['その他'] + return units_by_name + + unit_aliases = { + 'happyaround': 'happy_around!', + 'happy_around': 'happy_around!', + 'hapiara': 'happy_around!', + 'happy': 'happy_around!', + 'ha': 'happy_around', + 'peakyp-key': 'peaky_p-key', + 'peakypkey': 'peaky_p-key', + 'peaky': 'peaky_p-key', + 'p-key': 'peaky_p-key', + 'pkey': 'peaky_p-key', + 'pkpk': 'peaky_p-key', + 'pk': 'peaky_p-key', + 'photonmaiden': 'photon_maiden', + 'photome': 'photon_maiden', + 'photon': 'photon_maiden', + 'pm': 'photon_maiden', + 'mermaid': 'merm4id', + 'mmd': 'merm4id', + 'lyricallily': 'lyrical_lily', + 'riririri': 'lyrical_lily', + 'lililili': 'lyrical_lily', + 'lily': 'lyrical_lily', + 'lili': 'lyrical_lily', + 'll': 'lyrical_lily', + 'fuhifumi': 'lyrical_lily', + } diff --git a/miyu_bot/commands/cogs/card.py b/miyu_bot/commands/cogs/card.py index 6ab7fc1..206a4d8 100644 --- a/miyu_bot/commands/cogs/card.py +++ b/miyu_bot/commands/cogs/card.py @@ -4,24 +4,23 @@ import logging import discord from d4dj_utils.master.card_master import CardMaster -from d4dj_utils.master.event_master import EventMaster from d4dj_utils.master.event_specific_bonus_master import EventSpecificBonusMaster from d4dj_utils.master.skill_master import SkillMaster from discord.ext import commands -from main import masters, asset_manager +from miyu_bot.bot.bot import D4DJBot from miyu_bot.commands.common.argument_parsing import ParsedArguments, parse_arguments, ArgumentError from miyu_bot.commands.common.emoji import rarity_emoji_ids, attribute_emoji_ids_by_attribute_id, \ unit_emoji_ids_by_unit_id, parameter_bonus_emoji_ids_by_parameter_id -from miyu_bot.commands.common.event import get_latest_event from miyu_bot.commands.common.formatting import format_info -from miyu_bot.commands.common.master_asset_manager import hash_master -from miyu_bot.commands.common.name_aliases import characters_by_name, attributes_by_name, units_by_name, unit_aliases +from miyu_bot.bot.master_asset_manager import hash_master from miyu_bot.commands.common.reaction_message import run_tabbed_message, run_reaction_message, run_paged_message class Card(commands.Cog): - def __init__(self, bot: commands.Bot): + bot: D4DJBot + + def __init__(self, bot): self.bot = bot self.logger = logging.getLogger(__name__) @@ -123,17 +122,20 @@ class Card(commands.Cog): # Not used, but here because it's a valid argument before running require_all_arguments_used. display, _ = arguments.single(['display', 'disp'], sort, allowed_operators=['='], converter=card_attribute_aliases) - characters = {characters_by_name[c].id for c in arguments.words(characters_by_name.keys())} - units = {units_by_name[unit].id - for unit in arguments.tags(names=units_by_name.keys(), aliases=unit_aliases)} + characters = {self.bot.aliases.characters_by_name[c].id + for c in arguments.words(self.bot.aliases.characters_by_name.keys())} + units = {self.bot.aliases.units_by_name[unit].id + for unit in arguments.tags(names=self.bot.aliases.units_by_name.keys(), + aliases=self.bot.aliases.unit_aliases)} rarity_names = ['4*', '3*', '2*', '1*', r'4\*', r'3\*', r'2\*', r'1\*'] rarities = {int(r[0]) for r in arguments.words(rarity_names) | arguments.tags(rarity_names)} - attributes = {attributes_by_name[a].id for a in arguments.tags(attributes_by_name.keys())} + attributes = {self.bot.aliases.attributes_by_name[a].id + for a in arguments.tags(self.bot.aliases.attributes_by_name.keys())} event_bonus = bool(arguments.tags(['event', 'eventbonus', 'event_bonus'])) if event_bonus: - latest_event = get_latest_event(ctx) + latest_event = self.bot.asset_filters.events.get_latest_event(ctx) bonus: EventSpecificBonusMaster = latest_event.bonus if not characters: @@ -149,7 +151,7 @@ class Card(commands.Cog): arguments.require_all_arguments_used() - cards = masters.cards.get_sorted(arguments.text(), ctx) + cards = self.bot.asset_filters.cards.get_sorted(arguments.text(), ctx) if not (arguments.text() and sort is None): sort = sort or CardAttribute.Power cards = sorted(cards, key=lambda c: (sort.get_sort_key_from_card(c), c.max_power_with_limit_break)) diff --git a/miyu_bot/commands/cogs/event.py b/miyu_bot/commands/cogs/event.py index 3ee7c82..856331c 100644 --- a/miyu_bot/commands/cogs/event.py +++ b/miyu_bot/commands/cogs/event.py @@ -10,19 +10,20 @@ from d4dj_utils.master.event_master import EventMaster, EventState from discord.ext import commands from pytz import UnknownTimeZoneError -from main import asset_manager, masters +from miyu_bot.bot.bot import D4DJBot from miyu_bot.commands.common.emoji import attribute_emoji_ids_by_attribute_id, unit_emoji_ids_by_unit_id, \ parameter_bonus_emoji_ids_by_parameter_id, \ event_point_emoji_id -from miyu_bot.commands.common.event import get_latest_event from miyu_bot.commands.common.formatting import format_info from miyu_bot.commands.common.fuzzy_matching import romanize -from miyu_bot.commands.common.master_asset_manager import MasterFilter, hash_master +from miyu_bot.bot.master_asset_manager import hash_master from miyu_bot.commands.common.reaction_message import run_paged_message, run_dynamically_paged_message class Event(commands.Cog): - def __init__(self, bot: commands.Bot): + bot: D4DJBot + + def __init__(self, bot): self.bot = bot self.logger = logging.getLogger(__name__) @@ -38,14 +39,14 @@ class Event(commands.Cog): # Allows relative id searches like `!event +1` for next event or `!event -2` for the event before last event if arg[0] in ['-', '+']: try: - latest = get_latest_event(ctx) - event = masters.events.get(str(latest.id + int(arg)), ctx) + latest = self.bot.asset_filters.events.get_latest_event(ctx) + event = self.bot.asset_filters.events.get(str(latest.id + int(arg)), ctx) except ValueError: - event = masters.events.get(arg, ctx) + event = self.bot.asset_filters.events.get(arg, ctx) else: - event = masters.events.get(arg, ctx) + event = self.bot.asset_filters.events.get(arg, ctx) else: - event = get_latest_event(ctx) + event = self.bot.asset_filters.events.get_latest_event(ctx) if not event: msg = f'Failed to find event "{arg}".' @@ -58,7 +59,7 @@ class Event(commands.Cog): def generator(n): nonlocal current_id - new_event = masters.events.get(current_id + n, ctx) + new_event = self.bot.asset_filters.events.get(current_id + n, ctx) if new_event: current_id = new_event.id return self.get_event_embed(new_event) @@ -151,7 +152,7 @@ class Event(commands.Cog): description='Displays the time left in the current event', help='!timeleft') async def time_left(self, ctx: commands.Context): - latest = get_latest_event(ctx) + latest = self.bot.asset_filters.events.get_latest_event(ctx) state = latest.state() @@ -214,7 +215,7 @@ class Event(commands.Cog): async with session.get('http://www.projectdivar.com/eventdata/t20') as resp: leaderboard = await resp.json(encoding='utf-8') - latest = get_latest_event(ctx) + latest = self.bot.asset_filters.events.get_latest_event(ctx) logo = discord.File(latest.logo_path, filename='logo.png') embed = discord.Embed(title=f'{latest.name} t20').set_thumbnail(url=f'attachment://logo.png') max_points_digits = len(str(leaderboard[0]['points'])) diff --git a/miyu_bot/commands/cogs/music.py b/miyu_bot/commands/cogs/music.py index 85ee94f..2de761b 100644 --- a/miyu_bot/commands/cogs/music.py +++ b/miyu_bot/commands/cogs/music.py @@ -3,7 +3,6 @@ import contextlib import enum import logging import wave -from functools import lru_cache from inspect import cleandoc from typing import Tuple @@ -13,18 +12,19 @@ from d4dj_utils.master.common_enums import ChartSectionType from d4dj_utils.master.music_master import MusicMaster from discord.ext import commands -from main import asset_manager, masters +from miyu_bot.bot.bot import D4DJBot from miyu_bot.commands.common.argument_parsing import parse_arguments, ArgumentError, list_operator_for from miyu_bot.commands.common.emoji import difficulty_emoji_ids from miyu_bot.commands.common.formatting import format_info from miyu_bot.commands.common.fuzzy_matching import romanize -from miyu_bot.commands.common.master_asset_manager import hash_master -from miyu_bot.commands.common.name_aliases import units_by_name, unit_aliases +from miyu_bot.bot.master_asset_manager import hash_master from miyu_bot.commands.common.reaction_message import run_tabbed_message, run_paged_message class Music(commands.Cog): - def __init__(self, bot: commands.Bot): + bot: D4DJBot + + def __init__(self, bot): self.bot = bot self.logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ class Music(commands.Cog): async def song(self, ctx: commands.Context, *, arg: commands.clean_content): self.logger.info(f'Searching for song "{arg}".') - song = masters.music.get(arg, ctx) + song = self.bot.asset_filters.music.get(arg, ctx) if not song: msg = f'No results for song "{arg}".' @@ -67,7 +67,7 @@ class Music(commands.Cog): thumb = discord.File(song.jacket_path, filename='jacket.png') except FileNotFoundError: # Just a fallback - thumb = discord.File(asset_manager.path / 'ondemand/stamp/stamp_10006.png', filename='jacket.png') + thumb = discord.File(self.bot.assets.path / 'ondemand/stamp/stamp_10006.png', filename='jacket.png') embed = discord.Embed(title=song.name) embed.set_thumbnail(url=f'attachment://jacket.png') @@ -107,7 +107,7 @@ class Music(commands.Cog): self.logger.info(f'Searching for chart "{arg}".') name, difficulty = self.parse_chart_args(arg) - song = masters.music.get(name, ctx) + song = self.bot.asset_filters.music.get(name, ctx) if not song: msg = f'Failed to find chart "{name}".' @@ -129,7 +129,7 @@ class Music(commands.Cog): self.logger.info(f'Searching for chart sections "{arg}".') name, difficulty = self.parse_chart_args(arg) - song = masters.music.get(name, ctx) + song = self.bot.asset_filters.music.get(name, ctx) if not song: msg = f'Failed to find chart "{name}".' @@ -179,8 +179,9 @@ class Music(commands.Cog): reverse_sort = sort_op == '<' or arguments.tag('reverse') display, _ = arguments.single(['display', 'disp'], sort, allowed_operators=['='], converter=music_attribute_aliases) - units = {units_by_name[unit].id - for unit in arguments.tags(names=units_by_name.keys(), aliases=unit_aliases)} + units = {self.bot.aliases.units_by_name[unit].id + for unit in arguments.tags(names=self.bot.aliases.units_by_name.keys(), + aliases=self.bot.aliases.unit_aliases)} def difficulty_converter(d): return int(d[:-1]) + 0.5 if d[-1] == '+' else int(d) @@ -188,7 +189,7 @@ class Music(commands.Cog): difficulty = arguments.repeatable(['difficulty', 'diff', 'level'], is_list=True, converter=difficulty_converter) - songs = masters.music.get_sorted(arguments.text(), ctx) + songs = self.bot.asset_filters.music.get_sorted(arguments.text(), ctx) arguments.require_all_arguments_used() except ArgumentError as e: @@ -230,7 +231,7 @@ class Music(commands.Cog): thumb = discord.File(song.jacket_path, filename='jacket.png') except FileNotFoundError: # fallback - thumb = discord.File(asset_manager.path / 'ondemand/stamp/stamp_10006.png', filename='jacket.png') + thumb = discord.File(self.bot.assets.path / 'ondemand/stamp/stamp_10006.png', filename='jacket.png') files = [thumb] diff --git a/miyu_bot/commands/cogs/utility.py b/miyu_bot/commands/cogs/utility.py index e1c5a4a..553e92f 100644 --- a/miyu_bot/commands/cogs/utility.py +++ b/miyu_bot/commands/cogs/utility.py @@ -2,11 +2,14 @@ import logging from discord.ext import commands +from miyu_bot.bot.bot import D4DJBot from miyu_bot.commands.common.fuzzy_matching import romanize, FuzzyMatcher class Utility(commands.Cog): - def __init__(self, bot: commands.Bot): + bot: D4DJBot + + def __init__(self, bot): self.bot = bot self.logger = logging.getLogger(__name__) diff --git a/miyu_bot/commands/common/argument_parsing.py b/miyu_bot/commands/common/argument_parsing.py index 9368ad0..3f60216 100644 --- a/miyu_bot/commands/common/argument_parsing.py +++ b/miyu_bot/commands/common/argument_parsing.py @@ -1,10 +1,10 @@ import re -# https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes -# https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes from collections import namedtuple from typing import Dict, List, Optional, Container, Any, Union, Callable, Set, Iterable +# https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes +# https://stackoverflow.com/questions/21105360/regex-find-comma-not-inside-quotes # The ` ?` is just so it matches the space after during the replace with blank so there's no double spaces _param_re = re.compile( r'(([a-zA-Z]+)(!=|>=|<=|>|<|==|=)(("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+)(,("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|[^,\s]+))*)) ?') diff --git a/miyu_bot/commands/common/emoji.py b/miyu_bot/commands/common/emoji.py index b0cafac..5982e1b 100644 --- a/miyu_bot/commands/common/emoji.py +++ b/miyu_bot/commands/common/emoji.py @@ -7,7 +7,6 @@ difficulty_emoji_ids = { ChartDifficulty.Expert: 790050636225052694, } -# \:buff_power: \:buff_heart: \:buff_technique: \:buff_physical: parameter_bonus_emoji_ids = { 'all': 792930826735583293, 'heart': 792096971040620564, diff --git a/miyu_bot/commands/common/event.py b/miyu_bot/commands/common/event.py deleted file mode 100644 index 9e889f9..0000000 --- a/miyu_bot/commands/common/event.py +++ /dev/null @@ -1,19 +0,0 @@ -from d4dj_utils.master.event_master import EventMaster, EventState -from discord.ext import commands - -from main import masters - - -def get_latest_event(ctx: commands.Context) -> EventMaster: - """Returns the oldest event that has not ended or the newest event otherwise.""" - try: - # NY event overlapped with previous event - return min((v for v in masters.events.values(ctx) if v.state() == EventState.Open), - key=lambda e: e.start_datetime) - except ValueError: - try: - return min((v for v in masters.events.values(ctx) if v.state() < EventState.Ended), - key=lambda e: e.start_datetime) - except ValueError: - return max(masters.events.values(ctx), key=lambda v: v.start_datetime) - diff --git a/miyu_bot/commands/common/name_aliases.py b/miyu_bot/commands/common/name_aliases.py deleted file mode 100644 index 8cb0eff..0000000 --- a/miyu_bot/commands/common/name_aliases.py +++ /dev/null @@ -1,40 +0,0 @@ -from main import asset_manager - -characters_by_name = {} -for character in asset_manager.character_master.values(): - for name in character.full_name_english.split(): - characters_by_name[name.lower()] = character - -attributes_by_name = {attribute.en_name: attribute for attribute in asset_manager.attribute_master.values()} - -units_by_name = {unit.name.lower().replace(' ', '_'): unit for unit in asset_manager.unit_master.values()} -units_by_name['rondo'] = units_by_name['燐舞曲'] -units_by_name['special'] = units_by_name['スペシャル'] -units_by_name['other'] = units_by_name['その他'] -unit_aliases = { - 'happyaround': 'happy_around!', - 'happy_around': 'happy_around!', - 'hapiara': 'happy_around!', - 'happy': 'happy_around!', - 'ha': 'happy_around', - 'peakyp-key': 'peaky_p-key', - 'peakypkey': 'peaky_p-key', - 'peaky': 'peaky_p-key', - 'p-key': 'peaky_p-key', - 'pkey': 'peaky_p-key', - 'pkpk': 'peaky_p-key', - 'pk': 'peaky_p-key', - 'photonmaiden': 'photon_maiden', - 'photome': 'photon_maiden', - 'photon': 'photon_maiden', - 'pm': 'photon_maiden', - 'mermaid': 'merm4id', - 'mmd': 'merm4id', - 'lyricallily': 'lyrical_lily', - 'riririri': 'lyrical_lily', - 'lililili': 'lyrical_lily', - 'lily': 'lyrical_lily', - 'lili': 'lyrical_lily', - 'll': 'lyrical_lily', - 'fuhifumi': 'lyrical_lily', -}