# vim: set fileencoding=utf-8 from typing import Any, Dict, List, Optional from typing_extensions import Final from bemani.backend.ess import EventLogHandler from bemani.backend.sdvx.base import SoundVoltexBase from bemani.backend.sdvx.infiniteinfection import SoundVoltexInfiniteInfection from bemani.common import ID, VersionConstants from bemani.protocol import Node class SoundVoltexGravityWars( EventLogHandler, SoundVoltexBase, ): name: str = 'SOUND VOLTEX III GRAVITY WARS' version: int = VersionConstants.SDVX_GRAVITY_WARS GAME_LIMITED_LOCKED: Final[int] = 1 GAME_LIMITED_UNLOCKABLE: Final[int] = 2 GAME_LIMITED_UNLOCKED: Final[int] = 3 GAME_CURRENCY_PACKETS: Final[int] = 0 GAME_CURRENCY_BLOCKS: Final[int] = 1 GAME_CLEAR_TYPE_NO_CLEAR: Final[int] = 1 GAME_CLEAR_TYPE_CLEAR: Final[int] = 2 GAME_CLEAR_TYPE_HARD_CLEAR: Final[int] = 3 GAME_CLEAR_TYPE_ULTIMATE_CHAIN: Final[int] = 4 GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: Final[int] = 5 GAME_GRADE_NO_PLAY: Final[int] = 0 GAME_GRADE_D: Final[int] = 1 GAME_GRADE_C: Final[int] = 2 GAME_GRADE_B: Final[int] = 3 GAME_GRADE_A: Final[int] = 4 GAME_GRADE_AA: Final[int] = 5 GAME_GRADE_AAA: Final[int] = 6 GAME_CATALOG_TYPE_SONG: Final[int] = 0 GAME_CATALOG_TYPE_APPEAL_CARD: Final[int] = 1 GAME_CATALOG_TYPE_CREW: Final[int] = 4 GAME_GAUGE_TYPE_SKILL: Final[int] = 1 @classmethod def get_settings(cls) -> Dict[str, Any]: """ Return all of our front-end modifiably settings. """ return { 'bools': [ { 'name': 'Disable Online Matching', 'tip': 'Disable online matching between games.', 'category': 'game_config', 'setting': 'disable_matching', }, { 'name': 'Force Song Unlock', 'tip': 'Force unlock all songs.', 'category': 'game_config', 'setting': 'force_unlock_songs', }, { 'name': 'Force Appeal Card Unlock', 'tip': 'Force unlock all appeal cards.', 'category': 'game_config', 'setting': 'force_unlock_cards', }, { 'name': 'Force Crew Card Unlock', 'tip': 'Force unlock all crew and subcrew cards.', 'category': 'game_config', 'setting': 'force_unlock_crew', }, ], } def previous_version(self) -> Optional[SoundVoltexBase]: return SoundVoltexInfiniteInfection(self.data, self.config, self.model) def _get_skill_analyzer_courses(self) -> List[Dict[str, Any]]: # This is overridden in S1/S2 code. return [] def _get_skill_analyzer_seasons(self) -> Dict[int, str]: # This is overridden in S1/S2 code. return {} def _get_extra_events(self) -> List[int]: # This is overridden in S1/S2 code. return [] def __game_to_db_clear_type(self, clear_type: int) -> int: return { self.GAME_CLEAR_TYPE_NO_CLEAR: self.CLEAR_TYPE_FAILED, self.GAME_CLEAR_TYPE_CLEAR: self.CLEAR_TYPE_CLEAR, self.GAME_CLEAR_TYPE_HARD_CLEAR: self.CLEAR_TYPE_HARD_CLEAR, self.GAME_CLEAR_TYPE_ULTIMATE_CHAIN: self.CLEAR_TYPE_ULTIMATE_CHAIN, self.GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: self.CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN, }[clear_type] def __db_to_game_clear_type(self, clear_type: int) -> int: return { self.CLEAR_TYPE_NO_PLAY: self.GAME_CLEAR_TYPE_NO_CLEAR, self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_NO_CLEAR, self.CLEAR_TYPE_CLEAR: self.GAME_CLEAR_TYPE_CLEAR, self.CLEAR_TYPE_HARD_CLEAR: self.GAME_CLEAR_TYPE_HARD_CLEAR, self.CLEAR_TYPE_ULTIMATE_CHAIN: self.GAME_CLEAR_TYPE_ULTIMATE_CHAIN, self.CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: self.GAME_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN, }[clear_type] def __game_to_db_grade(self, grade: int) -> int: return { self.GAME_GRADE_NO_PLAY: self.GRADE_NO_PLAY, self.GAME_GRADE_D: self.GRADE_D, self.GAME_GRADE_C: self.GRADE_C, self.GAME_GRADE_B: self.GRADE_B, self.GAME_GRADE_A: self.GRADE_A, self.GAME_GRADE_AA: self.GRADE_AA, self.GAME_GRADE_AAA: self.GRADE_AAA, }[grade] def __db_to_game_grade(self, grade: int) -> int: return { self.GRADE_NO_PLAY: self.GAME_GRADE_NO_PLAY, self.GRADE_D: self.GAME_GRADE_D, self.GRADE_C: self.GAME_GRADE_C, self.GRADE_B: self.GAME_GRADE_B, self.GRADE_A: self.GAME_GRADE_A, self.GRADE_A_PLUS: self.GAME_GRADE_A, self.GRADE_AA: self.GAME_GRADE_AA, self.GRADE_AA_PLUS: self.GAME_GRADE_AA, self.GRADE_AAA: self.GAME_GRADE_AAA, self.GRADE_AAA_PLUS: self.GAME_GRADE_AAA, self.GRADE_S: self.GAME_GRADE_AAA, }[grade] def __get_skill_analyzer_skill_levels(self) -> Dict[int, str]: return { 0: 'Skill LEVEL 01 岳翔', 1: 'Skill LEVEL 02 流星', 2: 'Skill LEVEL 03 月衝', 3: 'Skill LEVEL 04 瞬光', 4: 'Skill LEVEL 05 天極', 5: 'Skill LEVEL 06 烈風', 6: 'Skill LEVEL 07 雷電', 7: 'Skill LEVEL 08 麗華', 8: 'Skill LEVEL 09 魔騎士', 9: 'Skill LEVEL 10 剛力羅', 10: 'Skill LEVEL 11 或帝滅斗', 11: 'Skill LEVEL ∞(12) 暴龍天', } def handle_game_3_common_request(self, request: Node) -> Node: game = Node.void('game_3') limited = Node.void('music_limited') game.add_child(limited) # Song unlock config game_config = self.get_game_config() if game_config.get_bool('force_unlock_songs'): ids = set() songs = self.data.local.music.get_all_songs(self.game, self.version) for song in songs: if song.data.get_int('limited') in (self.GAME_LIMITED_LOCKED, self.GAME_LIMITED_UNLOCKABLE): ids.add((song.id, song.chart)) for (songid, chart) in ids: info = Node.void('info') limited.add_child(info) info.add_child(Node.s32('music_id', songid)) info.add_child(Node.u8('music_type', chart)) info.add_child(Node.u8('limited', self.GAME_LIMITED_UNLOCKED)) # Event config event = Node.void('event') game.add_child(event) def enable_event(eid: int) -> None: evt = Node.void('info') event.add_child(evt) evt.add_child(Node.u32('event_id', eid)) if not game_config.get_bool('disable_matching'): enable_event(1) # Matching enabled enable_event(2) # Floor Infection enable_event(3) # Policy Break enable_event(60) # BEMANI Summer Diary for eventid in self._get_extra_events(): enable_event(eventid) # Skill Analyzer config skill_course = Node.void('skill_course') game.add_child(skill_course) seasons = self._get_skill_analyzer_seasons() skillnames = self.__get_skill_analyzer_skill_levels() courses = self._get_skill_analyzer_courses() max_level: Dict[int, int] = {} for course in courses: max_level[course['level']] = max(course['season_id'], max_level.get(course['level'], -1)) for course in courses: info = Node.void('info') skill_course.add_child(info) info.add_child(Node.s16('course_id', course.get('id', course['level']))) info.add_child(Node.s16('level', course['level'])) info.add_child(Node.s32('season_id', course['season_id'])) info.add_child(Node.string('season_name', seasons[course['season_id']])) info.add_child(Node.bool('season_new_flg', max_level[course['level']] == course['season_id'])) info.add_child(Node.string('course_name', course.get('skill_name', skillnames.get(course['level'], '')))) info.add_child(Node.s16('course_type', 0)) info.add_child(Node.s16('skill_name_id', course.get('skill_name_id', course['level']))) info.add_child(Node.bool('matching_assist', course['level'] >= 0 and course['level'] <= 6)) info.add_child(Node.s16('gauge_type', self.GAME_GAUGE_TYPE_SKILL)) info.add_child(Node.s16('paseli_type', 0)) for trackno, trackdata in enumerate(course['tracks']): track = Node.void('track') info.add_child(track) track.add_child(Node.s16('track_no', trackno)) track.add_child(Node.s32('music_id', trackdata['id'])) track.add_child(Node.s8('music_type', trackdata['type'])) return game def handle_game_3_exception_request(self, request: Node) -> Node: return Node.void('game_3') def handle_game_3_shop_request(self, request: Node) -> Node: self.update_machine_name(request.child_value('shopname')) # Respond with number of milliseconds until next request game = Node.void('game_3') game.add_child(Node.u32('nxt_time', 1000 * 5 * 60)) return game def handle_game_3_lounge_request(self, request: Node) -> Node: game = Node.void('game_3') # Refresh interval in seconds. game.add_child(Node.u32('interval', 10)) return game def handle_game_3_entry_s_request(self, request: Node) -> Node: game = Node.void('game_3') # This should be created on the fly for a lobby that we're in. game.add_child(Node.u32('entry_id', 1)) return game def handle_game_3_entry_e_request(self, request: Node) -> Node: # Lobby destroy method, eid node (u32) should be used # to destroy any open lobbies. return Node.void('game_3') def handle_game_3_frozen_request(self, request: Node) -> Node: game = Node.void('game_3') game.add_child(Node.u8('result', 0)) return game def handle_game_3_save_e_request(self, request: Node) -> Node: # This has to do with Policy Break against ReflecBeat and # floor infection, but we don't implement multi-game support so meh. return Node.void('game_3') def handle_game_3_play_e_request(self, request: Node) -> Node: return Node.void('game_3') def handle_game_3_buy_request(self, request: Node) -> Node: refid = request.child_value('refid') if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None if userid is not None: profile = self.get_profile(userid) else: profile = None if userid is not None and profile is not None: # Look up packets and blocks packet = profile.get_int('packet') block = profile.get_int('block') # Add on any additional we earned this round packet = packet + (request.child_value('earned_gamecoin_packet') or 0) block = block + (request.child_value('earned_gamecoin_block') or 0) currency_type = request.child_value('currency_type') price = request.child_value('item/price') if isinstance(price, list): # Sometimes we end up buying more than one item at once price = sum(price) if currency_type == self.GAME_CURRENCY_PACKETS: # This is a valid purchase newpacket = packet - price if newpacket < 0: result = 1 else: packet = newpacket result = 0 elif currency_type == self.GAME_CURRENCY_BLOCKS: # This is a valid purchase newblock = block - price if newblock < 0: result = 1 else: block = newblock result = 0 else: # Bad currency type result = 1 if result == 0: # Transaction is valid, update the profile with new packets and blocks profile.replace_int('packet', packet) profile.replace_int('block', block) self.put_profile(userid, profile) # If this was a song unlock, we should mark it as unlocked item_type = request.child_value('item/item_type') item_id = request.child_value('item/item_id') param = request.child_value('item/param') if not isinstance(item_type, list): # Sometimes we buy multiple things at once. Make it easier by always assuming this. item_type = [item_type] item_id = [item_id] param = [param] for i in range(len(item_type)): self.data.local.user.put_achievement( self.game, self.version, userid, item_id[i], f'item_{item_type[i]}', { 'param': param[i], }, ) else: # Unclear what to do here, return a bad response packet = 0 block = 0 result = 1 game = Node.void('game_3') game.add_child(Node.u32('gamecoin_packet', packet)) game.add_child(Node.u32('gamecoin_block', block)) game.add_child(Node.s8('result', result)) return game def handle_game_3_new_request(self, request: Node) -> Node: refid = request.child_value('refid') name = request.child_value('name') loc = ID.parse_machine_id(request.child_value('locid')) self.new_profile_by_refid(refid, name, loc) root = Node.void('game_3') return root def handle_game_3_load_request(self, request: Node) -> Node: refid = request.child_value('refid') root = self.get_profile_by_refid(refid) if root is not None: return root # Figure out if this user has an older profile or not userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: previous_game = self.previous_version() else: previous_game = None if previous_game is not None: profile = previous_game.get_profile(userid) else: profile = None if profile is not None: root = Node.void('game_3') root.add_child(Node.u8('result', 2)) root.add_child(Node.string('name', profile.get_str('name'))) return root else: root = Node.void('game_3') root.add_child(Node.u8('result', 1)) return root def handle_game_3_save_request(self, request: Node) -> Node: refid = request.child_value('refid') if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None if userid is not None: oldprofile = self.get_profile(userid) newprofile = self.unformat_profile(userid, request, oldprofile) else: newprofile = None if userid is not None and newprofile is not None: self.put_profile(userid, newprofile) return Node.void('game_3') def handle_game_3_load_m_request(self, request: Node) -> Node: refid = request.child_value('dataid') if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None if userid is not None: scores = self.data.remote.music.get_scores(self.game, self.version, userid) else: scores = [] # Output to the game game = Node.void('game_3') new = Node.void('new') game.add_child(new) for score in scores: music = Node.void('music') new.add_child(music) music.add_child(Node.u32('music_id', score.id)) music.add_child(Node.u32('music_type', score.chart)) music.add_child(Node.u32('score', score.points)) music.add_child(Node.u32('cnt', score.plays)) music.add_child(Node.u32('clear_type', self.__db_to_game_clear_type(score.data.get_int('clear_type')))) music.add_child(Node.u32('score_grade', self.__db_to_game_grade(score.data.get_int('grade')))) stats = score.data.get_dict('stats') music.add_child(Node.u32('btn_rate', stats.get_int('btn_rate'))) music.add_child(Node.u32('long_rate', stats.get_int('long_rate'))) music.add_child(Node.u32('vol_rate', stats.get_int('vol_rate'))) return game def handle_game_3_save_m_request(self, request: Node) -> Node: refid = request.child_value('refid') if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None # Doesn't matter if userid is None here, that's an anonymous score musicid = request.child_value('music_id') chart = request.child_value('music_type') points = request.child_value('score') combo = request.child_value('max_chain') clear_type = self.__game_to_db_clear_type(request.child_value('clear_type')) grade = self.__game_to_db_grade(request.child_value('score_grade')) stats = { 'btn_rate': request.child_value('btn_rate'), 'long_rate': request.child_value('long_rate'), 'vol_rate': request.child_value('vol_rate'), 'critical': request.child_value('critical'), 'near': request.child_value('near'), 'error': request.child_value('error'), } # Save the score self.update_score( userid, musicid, chart, points, clear_type, grade, combo, stats, ) # Return a blank response return Node.void('game_3') def handle_game_3_save_c_request(self, request: Node) -> Node: refid = request.child_value('dataid') if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None if userid is not None: course_id = request.child_value('crsid') clear_type = request.child_value('ct') achievement_rate = request.child_value('ar') season_id = request.child_value('ssnid') self.data.local.user.put_achievement( self.game, self.version, userid, (season_id * 100) + course_id, 'course', { 'clear_type': clear_type, 'achievement_rate': achievement_rate, }, ) # Return a blank response return Node.void('game_3')