# vim: set fileencoding=utf-8 import copy from typing import Dict, List, Optional from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.fantasia import PopnMusicFantasia from bemani.backend.base import Status from bemani.common import Profile, VersionConstants, ID from bemani.data import UserID, Link from bemani.protocol import Node class PopnMusicSunnyPark(PopnMusicBase): name = "Pop'n Music Sunny Park" version = VersionConstants.POPN_MUSIC_SUNNY_PARK # Chart type, as returned from the game GAME_CHART_TYPE_EASY = 0 GAME_CHART_TYPE_NORMAL = 1 GAME_CHART_TYPE_HYPER = 2 GAME_CHART_TYPE_EX = 3 # Chart type, as packed into a hiscore binary GAME_CHART_TYPE_EASY_POSITION = 0 GAME_CHART_TYPE_NORMAL_POSITION = 1 GAME_CHART_TYPE_HYPER_POSITION = 2 GAME_CHART_TYPE_EX_POSITION = 3 # Medal type, as returned from the game GAME_PLAY_MEDAL_CIRCLE_FAILED = 1 GAME_PLAY_MEDAL_DIAMOND_FAILED = 2 GAME_PLAY_MEDAL_STAR_FAILED = 3 GAME_PLAY_MEDAL_CIRCLE_CLEARED = 5 GAME_PLAY_MEDAL_DIAMOND_CLEARED = 6 GAME_PLAY_MEDAL_STAR_CLEARED = 7 GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO = 9 GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO = 10 GAME_PLAY_MEDAL_STAR_FULL_COMBO = 11 GAME_PLAY_MEDAL_PERFECT = 15 # Maximum music ID for this game GAME_MAX_MUSIC_ID = 1350 def previous_version(self) -> Optional[PopnMusicBase]: return PopnMusicFantasia(self.data, self.config, self.model) def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void('playerdata') # Set up the base profile base = Node.void('base') root.add_child(base) base.add_child(Node.string('name', profile.get_str('name', 'なし'))) base.add_child(Node.string('g_pm_id', ID.format_extid(profile.extid))) base.add_child(Node.u8('mode', profile.get_int('mode', 0))) base.add_child(Node.s8('button', profile.get_int('button', 0))) base.add_child(Node.s8('last_play_flag', profile.get_int('last_play_flag', -1))) base.add_child(Node.u8('medal_and_friend', profile.get_int('medal_and_friend', 0))) base.add_child(Node.s8('category', profile.get_int('category', -1))) base.add_child(Node.s8('sub_category', profile.get_int('sub_category', -1))) base.add_child(Node.s16('chara', profile.get_int('chara', -1))) base.add_child(Node.s8('chara_category', profile.get_int('chara_category', -1))) base.add_child(Node.u8('collabo', 255)) base.add_child(Node.u8('sheet', profile.get_int('sheet', 0))) base.add_child(Node.s8('tutorial', profile.get_int('tutorial', 0))) base.add_child(Node.s8('music_open_pt', profile.get_int('music_open_pt', 0))) base.add_child(Node.s8('is_conv', -1)) base.add_child(Node.s32('option', profile.get_int('option', 0))) base.add_child(Node.s16('music', profile.get_int('music', -1))) base.add_child(Node.u16('ep', profile.get_int('ep', 0))) base.add_child(Node.s32_array('sp_color_flg', profile.get_int_array('sp_color_flg', 2))) base.add_child(Node.s32('read_news', profile.get_int('read_news', 0))) base.add_child(Node.s16('consecutive_days_coupon', profile.get_int('consecutive_days_coupon', 0))) base.add_child(Node.s8('staff', 0)) # These are probably from an old event, but if they aren't present and defaulted, # then different songs show up in the Zoo event. base.add_child(Node.u16_array('gitadora_point', profile.get_int_array('gitadora_point', 3, [2000, 2000, 2000]))) base.add_child(Node.u8('gitadora_select', profile.get_int('gitadora_select', 2))) # Statistics section and scores section statistics = self.get_play_statistics(userid) base.add_child(Node.s32('total_play_cnt', statistics.total_plays)) base.add_child(Node.s16('today_play_cnt', statistics.today_plays)) base.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) # Number of rivals that are active for this version. links = self.data.local.user.get_links(self.game, self.version, userid) rivalcount = 0 for link in links: if link.type != 'rival': continue if not self.has_profile(link.other_userid): continue # This profile is valid. rivalcount += 1 base.add_child(Node.u8('active_fr_num', rivalcount)) last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 3)] most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 20)] while len(last_played) < 3: last_played.append(-1) while len(most_played) < 20: most_played.append(-1) hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8) clear_medal = [0] * self.GAME_MAX_MUSIC_ID clear_medal_sub = [0] * self.GAME_MAX_MUSIC_ID scores = self.data.remote.music.get_scores(self.game, self.version, userid) for score in scores: if score.id > self.GAME_MAX_MUSIC_ID: continue # Skip any scores for chart types we don't support if score.chart not in [ self.CHART_TYPE_EASY, self.CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER, self.CHART_TYPE_EX, ]: continue points = score.points medal = { self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED, # Map approximately self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED, self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED, self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED, self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO, self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO, self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO, self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT, }[score.data.get_int('medal')] medal_pos = { self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION, }[score.chart] clear_medal[score.id] = clear_medal[score.id] | (medal << (medal_pos * 4)) hiscore_index = (score.id * 4) + { self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION, }[score.chart] hiscore_byte_pos = int((hiscore_index * 17) / 8) hiscore_bit_pos = int((hiscore_index * 17) % 8) hiscore_value = points << hiscore_bit_pos hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF) hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF) hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF) hiscore = bytes(hiscore_array) base.add_child(Node.s16_array('my_best', most_played)) base.add_child(Node.s16_array('latest_music', last_played)) base.add_child(Node.u16_array('clear_medal', clear_medal)) base.add_child(Node.u8_array('clear_medal_sub', clear_medal_sub)) # Goes outside of base for some reason root.add_child(Node.binary('hiscore', hiscore)) # Avatar section avatar_dict = profile.get_dict('avatar') avatar = Node.void('avatar') root.add_child(avatar) avatar.add_child(Node.u8('hair', avatar_dict.get_int('hair', 0))) avatar.add_child(Node.u8('face', avatar_dict.get_int('face', 0))) avatar.add_child(Node.u8('body', avatar_dict.get_int('body', 0))) avatar.add_child(Node.u8('effect', avatar_dict.get_int('effect', 0))) avatar.add_child(Node.u8('object', avatar_dict.get_int('object', 0))) avatar.add_child(Node.u8_array('comment', avatar_dict.get_int_array('comment', 2))) avatar.add_child(Node.s32_array('get_hair', avatar_dict.get_int_array('get_hair', 2))) avatar.add_child(Node.s32_array('get_face', avatar_dict.get_int_array('get_face', 2))) avatar.add_child(Node.s32_array('get_body', avatar_dict.get_int_array('get_body', 2))) avatar.add_child(Node.s32_array('get_effect', avatar_dict.get_int_array('get_effect', 2))) avatar.add_child(Node.s32_array('get_object', avatar_dict.get_int_array('get_object', 2))) avatar.add_child(Node.s32_array('get_comment_over', avatar_dict.get_int_array('get_comment_over', 3))) avatar.add_child(Node.s32_array('get_comment_under', avatar_dict.get_int_array('get_comment_under', 3))) # Avatar add section avatar_add_dict = profile.get_dict('avatar_add') avatar_add = Node.void('avatar_add') root.add_child(avatar_add) avatar_add.add_child(Node.s32_array('get_hair', avatar_add_dict.get_int_array('get_hair', 2))) avatar_add.add_child(Node.s32_array('get_face', avatar_add_dict.get_int_array('get_face', 2))) avatar_add.add_child(Node.s32_array('get_body', avatar_add_dict.get_int_array('get_body', 2))) avatar_add.add_child(Node.s32_array('get_effect', avatar_add_dict.get_int_array('get_effect', 2))) avatar_add.add_child(Node.s32_array('get_object', avatar_add_dict.get_int_array('get_object', 2))) avatar_add.add_child(Node.s32_array('get_comment_over', avatar_add_dict.get_int_array('get_comment_over', 2))) avatar_add.add_child(Node.s32_array('get_comment_under', avatar_add_dict.get_int_array('get_comment_under', 2))) avatar_add.add_child(Node.s32_array('new_hair', avatar_add_dict.get_int_array('new_hair', 2))) avatar_add.add_child(Node.s32_array('new_face', avatar_add_dict.get_int_array('new_face', 2))) avatar_add.add_child(Node.s32_array('new_body', avatar_add_dict.get_int_array('new_body', 2))) avatar_add.add_child(Node.s32_array('new_effect', avatar_add_dict.get_int_array('new_effect', 2))) avatar_add.add_child(Node.s32_array('new_object', avatar_add_dict.get_int_array('new_object', 2))) avatar_add.add_child(Node.s32_array('new_comment_over', avatar_add_dict.get_int_array('new_comment_over', 2))) avatar_add.add_child(Node.s32_array('new_comment_under', avatar_add_dict.get_int_array('new_comment_under', 2))) # Net VS section netvs = Node.void('netvs') root.add_child(netvs) netvs.add_child(Node.s32('rank_point', 0)) netvs.add_child(Node.s16_array('record', [0, 0, 0, 0, 0, 0])) netvs.add_child(Node.u8('rank', 0)) netvs.add_child(Node.s8('vs_rank_old', 0)) netvs.add_child(Node.s8_array('ojama_condition', [0] * 74)) netvs.add_child(Node.s8_array('set_ojama', [0, 0, 0])) netvs.add_child(Node.s8_array('set_recommend', [0, 0, 0])) netvs.add_child(Node.u8('netvs_play_cnt', 0)) for dialog in [0, 1, 2, 3, 4, 5]: # TODO: Configure this, maybe? netvs.add_child(Node.string('dialog', f'dialog#{dialog}')) sp_data = Node.void('sp_data') root.add_child(sp_data) sp_data.add_child(Node.s32('sp', profile.get_int('sp', 0))) gakuen = Node.void('gakuen_data') root.add_child(gakuen) gakuen.add_child(Node.s32('music_list', -1)) saucer = Node.void('flying_saucer') root.add_child(saucer) saucer.add_child(Node.s32('music_list', -1)) saucer.add_child(Node.s32('tune_count', -1)) saucer.add_child(Node.u32('clear_norma', 0)) saucer.add_child(Node.u32('clear_norma_add', 0)) zoo_dict = profile.get_dict('zoo') zoo = Node.void('zoo') root.add_child(zoo) zoo.add_child(Node.u16_array('point', zoo_dict.get_int_array('point', 5))) zoo.add_child(Node.s32_array('music_list', zoo_dict.get_int_array('music_list', 2))) zoo.add_child(Node.s8_array('today_play_flag', zoo_dict.get_int_array('today_play_flag', 4))) triple = Node.void('triple_journey') root.add_child(triple) triple.add_child(Node.s32('music_list', -1)) triple.add_child(Node.s32_array('boss_damage', [65534, 65534, 65534, 65534])) triple.add_child(Node.s32_array('boss_stun', [0, 0, 0, 0])) triple.add_child(Node.s32('magic_gauge', 0)) triple.add_child(Node.s32('today_party', 0)) triple.add_child(Node.bool('union_magic', False)) triple.add_child(Node.float('base_attack_rate', 1.0)) triple.add_child(Node.s32('iidx_play_num', 0)) triple.add_child(Node.s32('reflec_play_num', 0)) triple.add_child(Node.s32('voltex_play_num', 0)) triple.add_child(Node.bool('iidx_play_flg', True)) triple.add_child(Node.bool('reflec_play_flg', True)) triple.add_child(Node.bool('voltex_play_flg', True)) ios = Node.void('ios') root.add_child(ios) ios.add_child(Node.s32('continueRightAnswer', 30)) ios.add_child(Node.s32('totalRightAnswer', 30)) kac2013 = Node.void('kac2013') root.add_child(kac2013) kac2013.add_child(Node.s8('music_num', 0)) kac2013.add_child(Node.s16('music', 0)) kac2013.add_child(Node.u8('sheet', 0)) baseball = Node.void('baseball_data') root.add_child(baseball) baseball.add_child(Node.s64('music_list', -1)) for id in [3, 5, 7]: node = Node.void('floor_infection') root.add_child(node) node.add_child(Node.s32('infection_id', id)) node.add_child(Node.s32('music_list', -1)) return root def format_conversion(self, userid: UserID, profile: Profile) -> Node: # Circular import, ugh from bemani.backend.popn.lapistoria import PopnMusicLapistoria root = Node.void('playerdata') root.add_child(Node.string('name', profile.get_str('name', 'なし'))) root.add_child(Node.s16('chara', profile.get_int('chara', -1))) root.add_child(Node.s32('option', profile.get_int('option', 0))) root.add_child(Node.s8('result', 1)) scores = self.data.remote.music.get_scores(self.game, self.version, userid) for score in scores: if score.id > self.GAME_MAX_MUSIC_ID: continue # Skip any scores for chart types we don't support if score.chart not in [ self.CHART_TYPE_EASY, self.CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER, self.CHART_TYPE_EX, ]: continue points = score.points medal = score.data.get_int('medal') music = Node.void('music') root.add_child(music) music.add_child(Node.s16('music_num', score.id)) music.add_child(Node.u8('sheet_num', { self.CHART_TYPE_EASY: PopnMusicLapistoria.GAME_CHART_TYPE_EASY, self.CHART_TYPE_NORMAL: PopnMusicLapistoria.GAME_CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER: PopnMusicLapistoria.GAME_CHART_TYPE_HYPER, self.CHART_TYPE_EX: PopnMusicLapistoria.GAME_CHART_TYPE_EX, }[score.chart])) music.add_child(Node.s16('cnt', score.plays)) music.add_child(Node.s32('score', 0)) music.add_child(Node.u8('clear_type', 0)) music.add_child(Node.s32('old_score', points)) music.add_child(Node.u8('old_clear_type', { self.PLAY_MEDAL_CIRCLE_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_EASY_CLEAR: PopnMusicLapistoria.GAME_PLAY_MEDAL_EASY_CLEAR, self.PLAY_MEDAL_CIRCLE_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_CLEARED, self.PLAY_MEDAL_DIAMOND_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_CLEARED, self.PLAY_MEDAL_STAR_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_CLEARED, self.PLAY_MEDAL_CIRCLE_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO, self.PLAY_MEDAL_DIAMOND_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO, self.PLAY_MEDAL_STAR_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_FULL_COMBO, self.PLAY_MEDAL_PERFECT: PopnMusicLapistoria.GAME_PLAY_MEDAL_PERFECT, }[medal])) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: newprofile = copy.deepcopy(oldprofile) newprofile.replace_int('option', request.child_value('option')) newprofile.replace_int('chara', request.child_value('chara')) newprofile.replace_int('mode', request.child_value('mode')) newprofile.replace_int('button', request.child_value('button')) newprofile.replace_int('music', request.child_value('music')) newprofile.replace_int('sheet', request.child_value('sheet')) newprofile.replace_int('last_play_flag', request.child_value('last_play_flag')) newprofile.replace_int('category', request.child_value('category')) newprofile.replace_int('sub_category', request.child_value('sub_category')) newprofile.replace_int('chara_category', request.child_value('chara_category')) newprofile.replace_int('medal_and_friend', request.child_value('medal_and_friend')) newprofile.replace_int('ep', request.child_value('ep')) newprofile.replace_int_array('sp_color_flg', 2, request.child_value('sp_color_flg')) newprofile.replace_int('read_news', request.child_value('read_news')) newprofile.replace_int('consecutive_days_coupon', request.child_value('consecutive_days_coupon')) newprofile.replace_int('tutorial', request.child_value('tutorial')) newprofile.replace_int('music_open_pt', request.child_value('music_open_pt')) newprofile.replace_int_array('gitadora_point', 3, request.child_value('gitadora_point')) newprofile.replace_int('gitadora_select', request.child_value('gitadora_select')) sp_node = request.child('sp_data') if sp_node is not None: newprofile.replace_int('sp', sp_node.child_value('sp')) zoo_dict = newprofile.get_dict('zoo') zoo_node = request.child('zoo') if zoo_node is not None: zoo_dict.replace_int_array('point', 5, zoo_node.child_value('point')) zoo_dict.replace_int_array('music_list', 2, zoo_node.child_value('music_list')) zoo_dict.replace_int_array('today_play_flag', 4, zoo_node.child_value('today_play_flag')) newprofile.replace_dict('zoo', zoo_dict) avatar_dict = newprofile.get_dict('avatar') avatar_dict.replace_int('hair', request.child_value('hair')) avatar_dict.replace_int('face', request.child_value('face')) avatar_dict.replace_int('body', request.child_value('body')) avatar_dict.replace_int('effect', request.child_value('effect')) avatar_dict.replace_int('object', request.child_value('object')) avatar_dict.replace_int_array('comment', 2, request.child_value('comment')) avatar_dict.replace_int_array('get_hair', 2, request.child_value('get_hair')) avatar_dict.replace_int_array('get_face', 2, request.child_value('get_face')) avatar_dict.replace_int_array('get_body', 2, request.child_value('get_body')) avatar_dict.replace_int_array('get_effect', 2, request.child_value('get_effect')) avatar_dict.replace_int_array('get_object', 2, request.child_value('get_object')) avatar_dict.replace_int_array('get_comment_over', 3, request.child_value('get_comment_over')) avatar_dict.replace_int_array('get_comment_under', 3, request.child_value('get_comment_under')) newprofile.replace_dict('avatar', avatar_dict) avatar_add_dict = newprofile.get_dict('avatar_add') avatar_add_node = request.child('avatar_add') if avatar_add_node is not None: avatar_add_dict.replace_int_array('get_hair', 2, avatar_add_node.child_value('get_hair')) avatar_add_dict.replace_int_array('get_face', 2, avatar_add_node.child_value('get_face')) avatar_add_dict.replace_int_array('get_body', 2, avatar_add_node.child_value('get_body')) avatar_add_dict.replace_int_array('get_effect', 2, avatar_add_node.child_value('get_effect')) avatar_add_dict.replace_int_array('get_object', 2, avatar_add_node.child_value('get_object')) avatar_add_dict.replace_int_array('get_comment_over', 2, avatar_add_node.child_value('get_comment_over')) avatar_add_dict.replace_int_array('get_comment_under', 2, avatar_add_node.child_value('get_comment_under')) avatar_add_dict.replace_int_array('new_hair', 2, avatar_add_node.child_value('new_hair')) avatar_add_dict.replace_int_array('new_face', 2, avatar_add_node.child_value('new_face')) avatar_add_dict.replace_int_array('new_body', 2, avatar_add_node.child_value('new_body')) avatar_add_dict.replace_int_array('new_effect', 2, avatar_add_node.child_value('new_effect')) avatar_add_dict.replace_int_array('new_object', 2, avatar_add_node.child_value('new_object')) avatar_add_dict.replace_int_array('new_comment_over', 2, avatar_add_node.child_value('new_comment_over')) avatar_add_dict.replace_int_array('new_comment_under', 2, avatar_add_node.child_value('new_comment_under')) newprofile.replace_dict('avatar_add', avatar_add_dict) # Keep track of play statistics self.update_play_statistics(userid) # Extract scores for node in request.children: if node.name == 'stage': songid = node.child_value('no') chart = { self.GAME_CHART_TYPE_EASY: self.CHART_TYPE_EASY, self.GAME_CHART_TYPE_NORMAL: self.CHART_TYPE_NORMAL, self.GAME_CHART_TYPE_HYPER: self.CHART_TYPE_HYPER, self.GAME_CHART_TYPE_EX: self.CHART_TYPE_EX, }[node.child_value('sheet')] medal = (node.child_value('n_data') >> (chart * 4)) & 0x000F medal = { self.GAME_PLAY_MEDAL_CIRCLE_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED, self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED, self.GAME_PLAY_MEDAL_STAR_FAILED: self.PLAY_MEDAL_STAR_FAILED, self.GAME_PLAY_MEDAL_CIRCLE_CLEARED: self.PLAY_MEDAL_CIRCLE_CLEARED, self.GAME_PLAY_MEDAL_DIAMOND_CLEARED: self.PLAY_MEDAL_DIAMOND_CLEARED, self.GAME_PLAY_MEDAL_STAR_CLEARED: self.PLAY_MEDAL_STAR_CLEARED, self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO: self.PLAY_MEDAL_CIRCLE_FULL_COMBO, self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO: self.PLAY_MEDAL_DIAMOND_FULL_COMBO, self.GAME_PLAY_MEDAL_STAR_FULL_COMBO: self.PLAY_MEDAL_STAR_FULL_COMBO, self.GAME_PLAY_MEDAL_PERFECT: self.PLAY_MEDAL_PERFECT, }[medal] points = node.child_value('score') self.update_score(userid, songid, chart, points, medal) return newprofile def handle_game_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'get': # TODO: Hook these up to config so we can change this root = Node.void('game') root.add_child(Node.s32('ir_phase', 0)) root.add_child(Node.s32('music_open_phase', 8)) root.add_child(Node.s32('collabo_phase', 8)) root.add_child(Node.s32('personal_event_phase', 10)) root.add_child(Node.s32('shop_event_phase', 6)) root.add_child(Node.s32('netvs_phase', 0)) root.add_child(Node.s32('card_phase', 9)) root.add_child(Node.s32('other_phase', 9)) root.add_child(Node.s32('local_matching_enable', 1)) root.add_child(Node.s32('n_matching_sec', 60)) root.add_child(Node.s32('l_matching_sec', 60)) root.add_child(Node.s32('is_check_cpu', 0)) root.add_child(Node.s32('week_no', 0)) root.add_child(Node.s16_array('sel_ranking', [-1, -1, -1, -1, -1])) root.add_child(Node.s16_array('up_ranking', [-1, -1, -1, -1, -1])) return root if method == 'active': # Update the name of this cab for admin purposes self.update_machine_name(request.child_value('shop_name')) return Node.void('game') if method == 'taxphase': return Node.void('game') # Invalid method return None def handle_playerdata_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'expire': return Node.void('playerdata') elif method == 'logout': return Node.void('playerdata') elif method == 'get': modelstring = request.attribute('model') refid = request.child_value('ref_id') root = self.get_profile_by_refid( refid, self.NEW_PROFILE_ONLY if modelstring is None else self.OLD_PROFILE_ONLY, ) if root is None: root = Node.void('playerdata') root.set_attribute('status', str(Status.NO_PROFILE)) return root elif method == 'conversion': refid = request.child_value('ref_id') name = request.child_value('name') chara = request.child_value('chara') root = self.new_profile_by_refid(refid, name, chara) if root is None: root = Node.void('playerdata') root.set_attribute('status', str(Status.NO_PROFILE)) return root elif method == 'new': refid = request.child_value('ref_id') name = request.child_value('name') root = self.new_profile_by_refid(refid, name) if root is None: root = Node.void('playerdata') root.set_attribute('status', str(Status.NO_PROFILE)) return root elif method == 'set': refid = request.attribute('ref_id') root = Node.void('playerdata') root.add_child(Node.s8('pref', -1)) if refid is None: return root userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: return root oldprofile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0) newprofile = self.unformat_profile(userid, request, oldprofile) if newprofile is not None: self.put_profile(userid, newprofile) root.add_child(Node.string('name', newprofile['name'])) return root elif method == 'friend': refid = request.attribute('ref_id') root = Node.void('playerdata') # Look up our own user ID based on the RefID provided. userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: root.set_attribute('status', str(Status.NO_PROFILE)) return root # Grab the links that we care about. links = self.data.local.user.get_links(self.game, self.version, userid) profiles: Dict[UserID, Profile] = {} rivals: List[Link] = [] for link in links: if link.type != 'rival': continue other_profile = self.get_profile(link.other_userid) if other_profile is None: continue profiles[link.other_userid] = other_profile rivals.append(link) for rival in links[:2]: rivalid = rival.other_userid rivalprofile = profiles[rivalid] scores = self.data.remote.music.get_scores(self.game, self.version, rivalid) # First, output general profile info. friend = Node.void('friend') root.add_child(friend) # This might be for having non-active or non-confirmed friends, but setting to 0 makes the # ranking numbers disappear and the player icon show a questionmark. friend.add_child(Node.s8('open', 1)) # Set up some sane defaults. friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし'))) friend.add_child(Node.string('g_pm_id', ID.format_extid(rivalprofile.extid))) friend.add_child(Node.s16('chara', rivalprofile.get_int('chara', -1))) # Set up player avatar. avatar_dict = rivalprofile.get_dict('avatar') friend.add_child(Node.u8('hair', avatar_dict.get_int('hair', 0))) friend.add_child(Node.u8('face', avatar_dict.get_int('face', 0))) friend.add_child(Node.u8('body', avatar_dict.get_int('body', 0))) friend.add_child(Node.u8('effect', avatar_dict.get_int('effect', 0))) friend.add_child(Node.u8('object', avatar_dict.get_int('object', 0))) friend.add_child(Node.u8_array('comment', avatar_dict.get_int_array('comment', 2))) # Perform hiscore/medal conversion. hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8) clear_medal = [0] * self.GAME_MAX_MUSIC_ID for score in scores: if score.id > self.GAME_MAX_MUSIC_ID: continue # Skip any scores for chart types we don't support if score.chart not in [ self.CHART_TYPE_EASY, self.CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER, self.CHART_TYPE_EX, ]: continue points = score.points medal = { self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED, # Map approximately self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED, self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED, self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED, self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO, self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO, self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO, self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT, }[score.data.get_int('medal')] medal_pos = { self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION, }[score.chart] clear_medal[score.id] = clear_medal[score.id] | (medal << (medal_pos * 4)) hiscore_index = (score.id * 4) + { self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION, }[score.chart] hiscore_byte_pos = int((hiscore_index * 17) / 8) hiscore_bit_pos = int((hiscore_index * 17) % 8) hiscore_value = points << hiscore_bit_pos hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF) hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF) hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF) hiscore = bytes(hiscore_array) friend.add_child(Node.u16_array('clear_medal', clear_medal)) friend.add_child(Node.binary('hiscore', hiscore)) return root # Invalid method return None def handle_lobby_request(self, request: Node) -> Optional[Node]: # Stub out the entire lobby service return Node.void('lobby')