# vim: set fileencoding=utf-8 from typing import Any, Dict, Iterator, Optional, Tuple, List from flask_caching import Cache from bemani.backend.iidx import IIDXFactory, IIDXBase from bemani.common import Profile, ValidatedDict, GameConstants, RegionConstants from bemani.data import Attempt, Data, Config, Score, Song, UserID from bemani.frontend.base import FrontendBase class IIDXFrontend(FrontendBase): game: GameConstants = GameConstants.IIDX valid_charts: List[int] = [ IIDXBase.CHART_TYPE_N7, IIDXBase.CHART_TYPE_H7, IIDXBase.CHART_TYPE_A7, IIDXBase.CHART_TYPE_N14, IIDXBase.CHART_TYPE_H14, IIDXBase.CHART_TYPE_A14, ] valid_rival_types: List[str] = [ 'sp_rival', 'dp_rival', ] def __init__(self, data: Data, config: Config, cache: Cache) -> None: super().__init__(data, config, cache) self.machines: Dict[int, str] = {} def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]: yield from IIDXFactory.all_games() def get_duplicate_id(self, musicid: int, chart: int) -> Optional[Tuple[int, int]]: modern_to_legacy_map = { 23066: 4213, 22068: 9203, 22052: 10203, 22039: 12201, 21201: 12204, 21064: 12206, 23077: 13215, 22025: 14202, 21068: 14210, 22069: 14211, 23070: 14214, 23069: 15202, 21063: 15204, 21065: 15205, 22028: 15207, 22049: 15208, 22043: 15209, 23060: 15211, 21062: 15215, 21067: 16207, 23062: 16209, 21066: 16212, 23030: 22096, 23051: 22097, 11101: 21214, 14101: 21221, 15104: 21225, 15102: 21226, 15101: 21231, 15103: 21237, 16105: 21240, 16104: 21242, 16103: 21253, 16102: 21258, 16101: 21262, 14100: 21220, } # Some charts were changed, and others kept the same on these if chart in [0, 1, 2]: modern_to_legacy_map[23065] = 9206 oldid = modern_to_legacy_map.get(musicid) oldchart = chart if oldid == 12204: if oldchart == 1: oldchart = 2 elif oldchart == 2: oldchart = 1 if oldid is not None: return (oldid, oldchart) else: return None def format_dan_rank(self, rank: int) -> str: if rank == -1: return '--' return { IIDXBase.DAN_RANK_7_KYU: '七級', IIDXBase.DAN_RANK_6_KYU: '六級', IIDXBase.DAN_RANK_5_KYU: '五級', IIDXBase.DAN_RANK_4_KYU: '四級', IIDXBase.DAN_RANK_3_KYU: '三級', IIDXBase.DAN_RANK_2_KYU: '二級', IIDXBase.DAN_RANK_1_KYU: '一級', IIDXBase.DAN_RANK_1_DAN: '初段', IIDXBase.DAN_RANK_2_DAN: '二段', IIDXBase.DAN_RANK_3_DAN: '三段', IIDXBase.DAN_RANK_4_DAN: '四段', IIDXBase.DAN_RANK_5_DAN: '五段', IIDXBase.DAN_RANK_6_DAN: '六段', IIDXBase.DAN_RANK_7_DAN: '七段', IIDXBase.DAN_RANK_8_DAN: '八段', IIDXBase.DAN_RANK_9_DAN: '九段', IIDXBase.DAN_RANK_10_DAN: '十段', IIDXBase.DAN_RANK_CHUDEN: '中伝', IIDXBase.DAN_RANK_KAIDEN: '皆伝', }[rank] def format_flags(self, settings_dict: ValidatedDict) -> Dict[str, Any]: flags = settings_dict.get_int('flags') return { 'grade': (flags & 0x001) != 0, 'status': (flags & 0x002) != 0, 'difficulty': (flags & 0x004) != 0, 'alphabet': (flags & 0x008) != 0, 'rival_played': (flags & 0x010) != 0, 'rival_win_lose': (flags & 0x040) != 0, 'rival_info': (flags & 0x080) != 0, 'hide_play_count': (flags & 0x100) != 0, 'disable_graph_cutin': (flags & 0x200) != 0, 'classic_hispeed': (flags & 0x400) != 0, 'hide_iidx_id': (flags & 0x1000) != 0, 'disable_song_preview': settings_dict.get_int('disable_song_preview') != 0, 'effector_lock': settings_dict.get_int('effector_lock') != 0, 'disable_hcn_color': settings_dict.get_int('disable_hcn_color') != 0, } def format_settings(self, settings_dict: ValidatedDict) -> Dict[str, Any]: return { 'frame': settings_dict.get_int('frame'), 'turntable': settings_dict.get_int('turntable'), 'burst': settings_dict.get_int('burst'), 'bgm': settings_dict.get_int('bgm'), 'towel': settings_dict.get_int('towel'), 'judge_pos': settings_dict.get_int('judge_pos'), 'voice': settings_dict.get_int('voice'), 'noteskin': settings_dict.get_int('noteskin'), 'full_combo': settings_dict.get_int('full_combo'), 'beam': settings_dict.get_int('beam'), 'judge': settings_dict.get_int('judge'), 'pacemaker': settings_dict.get_int('pacemaker'), 'effector_preset': settings_dict.get_int('effector_preset'), 'explosion_size': settings_dict.get_int('explosion_size'), 'note_preview': settings_dict.get_int('note_preview'), } def format_qpro(self, qpro_dict: ValidatedDict) -> Dict[str, Any]: return { 'body': qpro_dict.get_int('body'), 'face': qpro_dict.get_int('face'), 'hair': qpro_dict.get_int('hair'), 'hand': qpro_dict.get_int('hand'), 'head': qpro_dict.get_int('head'), } def get_all_items(self, versions: list) -> Dict[str, List[Dict[str, Any]]]: result = {} for version in versions: qpro = self.__format_iidx_extras(version) result[version] = qpro['qpros'] return result def __format_iidx_extras(self, version: int) -> Dict[str, List[Dict[str, Any]]]: # Gotta look up the unlock catalog items = self.data.local.game.get_items(self.game, version) return { "qpros": [ { "identifier": item.data.get_str("identifier"), "id": str(item.id), "name": item.data.get_str("name"), "type": item.type[3:], } for item in items if item.type in ['qp_body', 'qp_face', 'qp_hair', 'qp_hand', 'qp_head'] ], } def format_profile(self, profile: Profile, playstats: ValidatedDict) -> Dict[str, Any]: formatted_profile = super().format_profile(profile, playstats) formatted_profile.update({ 'arcade': "", 'prefecture': RegionConstants.game_to_db_region(profile.version >= 25, profile.get_int('pid', self.config.server.region)), 'settings': self.format_settings(profile.get_dict('settings')), 'flags': self.format_flags(profile.get_dict('settings')), 'sdjp': playstats.get_int('single_dj_points'), 'ddjp': playstats.get_int('double_dj_points'), 'sp': playstats.get_int('single_plays'), 'dp': playstats.get_int('double_plays'), 'sdan': self.format_dan_rank(profile.get_int('sgrade', -1)), 'ddan': self.format_dan_rank(profile.get_int('dgrade', -1)), 'srank': profile.get_int('sgrade', -1), 'drank': profile.get_int('dgrade', -1), 'qpro': self.format_qpro(profile.get_dict('qpro')), }) if 'shop_location' in profile: shop_id = profile.get_int('shop_location') if shop_id in self.machines: formatted_profile['arcade'] = self.machines[shop_id] else: pcbid = self.data.local.machine.from_machine_id(shop_id) if pcbid is not None: machine = self.data.local.machine.get_machine(pcbid) self.machines[shop_id] = machine.name formatted_profile['arcade'] = machine.name return formatted_profile def format_score(self, userid: UserID, score: Score) -> Dict[str, Any]: formatted_score = super().format_score(userid, score) formatted_score['miss_count'] = score.data.get_int('miss_count', -1) formatted_score['lamp'] = score.data.get_int('clear_status') formatted_score['status'] = { IIDXBase.CLEAR_STATUS_NO_PLAY: 'NO PLAY', IIDXBase.CLEAR_STATUS_FAILED: 'FAILED', IIDXBase.CLEAR_STATUS_ASSIST_CLEAR: 'ASSIST CLEAR', IIDXBase.CLEAR_STATUS_EASY_CLEAR: 'EASY CLEAR', IIDXBase.CLEAR_STATUS_CLEAR: 'CLEAR', IIDXBase.CLEAR_STATUS_HARD_CLEAR: 'HARD CLEAR', IIDXBase.CLEAR_STATUS_EX_HARD_CLEAR: 'EX HARD CLEAR', IIDXBase.CLEAR_STATUS_FULL_COMBO: 'FULL COMBO', }.get(score.data.get_int('clear_status'), 'NO PLAY') return formatted_score def format_top_score(self, userid: UserID, score: Score) -> Dict[str, Any]: formatted_score = super().format_score(userid, score) formatted_score['miss_count'] = score.data.get_int('miss_count', -1) formatted_score['lamp'] = score.data.get_int('clear_status') formatted_score['ghost'] = [x for x in (score.data.get_bytes('ghost') or b'')] formatted_score['status'] = { IIDXBase.CLEAR_STATUS_NO_PLAY: 'NO PLAY', IIDXBase.CLEAR_STATUS_FAILED: 'FAILED', IIDXBase.CLEAR_STATUS_ASSIST_CLEAR: 'ASSIST CLEAR', IIDXBase.CLEAR_STATUS_EASY_CLEAR: 'EASY CLEAR', IIDXBase.CLEAR_STATUS_CLEAR: 'CLEAR', IIDXBase.CLEAR_STATUS_HARD_CLEAR: 'HARD CLEAR', IIDXBase.CLEAR_STATUS_EX_HARD_CLEAR: 'EX HARD CLEAR', IIDXBase.CLEAR_STATUS_FULL_COMBO: 'FULL COMBO', }.get(score.data.get_int('clear_status'), 'NO PLAY') return formatted_score def format_attempt(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]: formatted_attempt = super().format_attempt(userid, attempt) formatted_attempt['miss_count'] = attempt.data.get_int('miss_count', -1) formatted_attempt['status'] = { IIDXBase.CLEAR_STATUS_NO_PLAY: 'NO PLAY', IIDXBase.CLEAR_STATUS_FAILED: 'FAILED', IIDXBase.CLEAR_STATUS_ASSIST_CLEAR: 'ASSIST CLEAR', IIDXBase.CLEAR_STATUS_EASY_CLEAR: 'EASY CLEAR', IIDXBase.CLEAR_STATUS_CLEAR: 'CLEAR', IIDXBase.CLEAR_STATUS_HARD_CLEAR: 'HARD CLEAR', IIDXBase.CLEAR_STATUS_EX_HARD_CLEAR: 'EX HARD CLEAR', IIDXBase.CLEAR_STATUS_FULL_COMBO: 'FULL COMBO', }.get(attempt.data.get_int('clear_status'), 'NO PLAY') return formatted_attempt def format_song(self, song: Song) -> Dict[str, Any]: difficulties = [0, 0, 0, 0, 0, 0] notecounts = [0, 0, 0, 0, 0, 0] difficulties[song.chart] = song.data.get_int('difficulty', 13) notecounts[song.chart] = song.data.get_int('notecount', 5730) formatted_song = super().format_song(song) formatted_song['bpm_min'] = song.data.get_int('bpm_min', 120) formatted_song['bpm_max'] = song.data.get_int('bpm_max', 120) formatted_song['difficulties'] = difficulties formatted_song['notecounts'] = notecounts return formatted_song def merge_song(self, existing: Dict[str, Any], new: Song) -> Dict[str, Any]: new_song = super().merge_song(existing, new) if ( existing['difficulties'][new.chart] == 0 or existing['notecounts'][new.chart] == 0 ): new_song['difficulties'][new.chart] = new.data.get_int('difficulty', 13) new_song['notecounts'][new.chart] = new.data.get_int('notecount', 5730) return new_song