# vim: set fileencoding=utf-8 import binascii import copy from typing import Dict, List, Optional from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.lapistoria import PopnMusicLapistoria from bemani.common import Time, Profile, VersionConstants from bemani.data import UserID, Link from bemani.protocol import Node class PopnMusicEclale(PopnMusicBase): name = "Pop'n Music éclale" version = VersionConstants.POPN_MUSIC_ECLALE # 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 # 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_EASY_CLEAR = 4 GAME_PLAY_MEDAL_CIRCLE_CLEARED = 5 GAME_PLAY_MEDAL_DIAMOND_CLEARED = 6 GAME_PLAY_MEDAL_STAR_CLEARED = 7 GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO = 8 GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO = 9 GAME_PLAY_MEDAL_STAR_FULL_COMBO = 10 GAME_PLAY_MEDAL_PERFECT = 11 # Biggest ID in the music DB GAME_MAX_MUSIC_ID = 1550 def previous_version(self) -> Optional[PopnMusicBase]: return PopnMusicLapistoria(self.data, self.config, self.model) def __construct_common_info(self, root: Node) -> None: # Event phases phases = { # Unknown event (0-16) 0: 16, # Unknown event (0-3) 1: 3, # Unknown event (0-1) 2: 1, # Unknown event (0-2) 3: 2, # Unknown event (0-1) 4: 1, # Unknown event (0-2) 5: 2, # Unknown event (0-1) 6: 1, # Unknown event (0-4) 7: 4, # Unknown event (0-3) 8: 3, # Unknown event (0-4) 9: 4, # Unknown event (0-4) 10: 4, # Unknown event (0-1) 11: 1, # Possibly global event matching related? (0-1) 12: 1, # Unknown event (0-4) 13: 4, } for phaseid in phases: phase = Node.void('phase') root.add_child(phase) phase.add_child(Node.s16('event_id', phaseid)) phase.add_child(Node.s16('phase', phases[phaseid])) for areaid in range(1, 50): area = Node.void('area') root.add_child(area) area.add_child(Node.s16('area_id', areaid)) area.add_child(Node.u64('end_date', 0)) area.add_child(Node.s16('medal_id', areaid)) area.add_child(Node.bool('is_limit', False)) # Calculate most popular characters profiles = self.data.remote.user.get_all_profiles(self.game, self.version) charas: Dict[int, int] = {} for (_userid, profile) in profiles: chara = profile.get_int('chara', -1) if chara <= 0: continue if chara not in charas: charas[chara] = 1 else: charas[chara] = charas[chara] + 1 # Order a typle by most popular character to least popular character charamap = sorted( [(c, charas[c]) for c in charas], key=lambda c: c[1], reverse=True, ) # Output the top 20 of them rank = 1 for (charaid, _usecount) in charamap[:20]: popular = Node.void('popular') root.add_child(popular) popular.add_child(Node.s16('rank', rank)) popular.add_child(Node.s16('chara_num', charaid)) rank = rank + 1 # Output the hit chart for (songid, _plays) in self.data.local.music.get_hit_chart(self.game, self.version, 500): popular_music = Node.void('popular_music') root.add_child(popular_music) popular_music.add_child(Node.s16('music_num', songid)) # Output goods prices for goodsid in range(1, 421): if goodsid >= 1 and goodsid <= 80: price = 60 elif goodsid >= 81 and goodsid <= 120: price = 250 elif goodsid >= 121 and goodsid <= 142: price = 500 elif goodsid >= 143 and goodsid <= 300: price = 100 elif goodsid >= 301 and goodsid <= 420: price = 150 else: raise Exception('Invalid goods ID!') goods = Node.void('goods') root.add_child(goods) goods.add_child(Node.s16('goods_id', goodsid)) goods.add_child(Node.s32('price', price)) goods.add_child(Node.s16('goods_type', 0)) def handle_pcb23_boot_request(self, request: Node) -> Node: return Node.void('pcb23') def handle_pcb23_error_request(self, request: Node) -> Node: return Node.void('pcb23') def handle_pcb23_dlstatus_request(self, request: Node) -> Node: return Node.void('pcb23') def handle_pcb23_write_request(self, request: Node) -> Node: # Update the name of this cab for admin purposes self.update_machine_name(request.child_value('pcb_setting/name')) return Node.void('pcb23') def handle_info23_common_request(self, request: Node) -> Node: info = Node.void('info23') self.__construct_common_info(info) return info def handle_lobby22_request(self, request: Node) -> Optional[Node]: # Stub out the entire lobby22 service (yes, its lobby22 in Pop'n 23) return Node.void('lobby22') def handle_player23_start_request(self, request: Node) -> Node: root = Node.void('player23') root.add_child(Node.s32('play_id', 0)) self.__construct_common_info(root) return root def handle_player23_logout_request(self, request: Node) -> Node: return Node.void('player23') def handle_player23_read_request(self, request: Node) -> Node: refid = request.child_value('ref_id') root = self.get_profile_by_refid(refid, self.OLD_PROFILE_FALLTHROUGH) if root is None: root = Node.void('player23') root.add_child(Node.s8('result', 2)) return root def handle_player23_write_request(self, request: Node) -> Node: refid = request.child_value('ref_id') 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) 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) return Node.void('player23') def handle_player23_new_request(self, request: Node) -> Node: 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('player23') root.add_child(Node.s8('result', 2)) return root def handle_player23_conversion_request(self, request: Node) -> Node: 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('player23') root.add_child(Node.s8('result', 2)) return root def handle_player23_buy_request(self, request: Node) -> Node: refid = request.child_value('ref_id') 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: itemid = request.child_value('id') itemtype = request.child_value('type') itemparam = request.child_value('param') price = request.child_value('price') lumina = request.child_value('lumina') if lumina >= price: # Update player lumina balance profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0) profile.replace_int('lumina', lumina - price) self.put_profile(userid, profile) # Grant the object self.data.local.user.put_achievement( self.game, self.version, userid, itemid, f'item_{itemtype}', { 'param': itemparam, 'is_new': True, }, ) return Node.void('player23') def handle_player23_read_score_request(self, request: Node) -> Node: refid = request.child_value('ref_id') root = Node.void('player23') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: scores = self.data.remote.music.get_scores(self.game, self.version, userid) else: scores = [] for score in scores: # 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: self.GAME_CHART_TYPE_EASY, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX, }[score.chart])) music.add_child(Node.s32('score', points)) music.add_child(Node.u8('clear_type', { 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_EASY_CLEAR, 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, }[medal])) music.add_child(Node.s16('cnt', score.plays)) return root def handle_player23_friend_request(self, request: Node) -> Node: refid = request.attribute('ref_id') no = int(request.attribute('no', '-1')) root = Node.void('player23') if no < 0: root.add_child(Node.s8('result', 2)) return root # 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.add_child(Node.s8('result', 2)) 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) # Somehow requested an invalid profile. if no >= len(rivals): root.add_child(Node.s8('result', 2)) return root rivalid = links[no].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) friend.add_child(Node.s16('no', no)) friend.add_child(Node.string('g_pm_id', self.format_extid(rivalprofile.extid))) # Eclale formats on its own friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし'))) friend.add_child(Node.s16('chara', rivalprofile.get_int('chara', -1))) # 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('is_open', 1)) for score in scores: # 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') friend.add_child(music) music.set_attribute('music_num', str(score.id)) music.set_attribute('sheet_num', str({ self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX, }[score.chart])) music.set_attribute('score', str(points)) music.set_attribute('clearmedal', str({ 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_EASY_CLEAR, 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, }[medal])) return root def handle_player23_write_music_request(self, request: Node) -> Node: refid = request.child_value('ref_id') root = Node.void('player23') if refid is None: return root userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: return root songid = request.child_value('music_num') 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, }[request.child_value('sheet_num')] medal = request.child_value('clearmedal') points = request.child_value('score') combo = request.child_value('combo') stats = { 'cool': request.child_value('cool'), 'great': request.child_value('great'), 'good': request.child_value('good'), 'bad': request.child_value('bad') } 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_EASY_CLEAR: self.PLAY_MEDAL_EASY_CLEAR, 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] self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats) return root def format_conversion(self, userid: UserID, profile: Profile) -> Node: root = Node.void('player23') 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.s8('result', 1)) scores = self.data.remote.music.get_scores(self.game, self.version, userid) for score in scores: # 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: self.GAME_CHART_TYPE_EASY, self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER, self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX, }[score.chart])) music.add_child(Node.s32('score', points)) music.add_child(Node.u8('clear_type', { 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_EASY_CLEAR, 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, }[medal])) music.add_child(Node.s16('cnt', score.plays)) return root def format_extid(self, extid: int) -> str: data = str(extid) crc = abs(binascii.crc32(data.encode('ascii'))) % 10000 return f'{data}{crc:04d}' def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void('player23') # Mark this as a current profile root.add_child(Node.s8('result', 0)) # Account stuff account = Node.void('account') root.add_child(account) account.add_child(Node.string('g_pm_id', self.format_extid(profile.extid))) # Eclale formats on its own account.add_child(Node.string('name', profile.get_str('name', 'なし'))) account.add_child(Node.s8('tutorial', profile.get_int('tutorial'))) account.add_child(Node.s16('area_id', profile.get_int('area_id'))) account.add_child(Node.s16('lumina', profile.get_int('lumina', 300))) account.add_child(Node.s16('read_news', profile.get_int('read_news'))) account.add_child(Node.bool('welcom_pack', profile.get_bool('welcom_pack'))) account.add_child(Node.s16_array('medal_set', profile.get_int_array('medal_set', 4))) account.add_child(Node.s16_array('nice', profile.get_int_array('nice', 30, [-1] * 30))) account.add_child(Node.s16_array('favorite_chara', profile.get_int_array('favorite_chara', 20, [-1] * 20))) account.add_child(Node.s16_array('special_area', profile.get_int_array('special_area', 8))) account.add_child(Node.s16_array('chocolate_charalist', profile.get_int_array('chocolate_charalist', 5, [-1] * 5))) account.add_child(Node.s16_array('teacher_setting', profile.get_int_array('teacher_setting', 10))) # Stuff we never change account.add_child(Node.s8('staff', 0)) account.add_child(Node.s16('item_type', 0)) account.add_child(Node.s16('item_id', 0)) account.add_child(Node.s8('is_conv', 0)) account.add_child(Node.bool('meteor_flg', True)) account.add_child(Node.s16_array('license_data', [-1] * 20)) # Add statistics section last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 5)] most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 10)] while len(last_played) < 5: last_played.append(-1) while len(most_played) < 10: most_played.append(-1) account.add_child(Node.s16_array('my_best', most_played)) account.add_child(Node.s16_array('latest_music', last_played)) # 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 account.add_child(Node.u8('active_fr_num', rivalcount)) # player statistics statistics = self.get_play_statistics(userid) last_play_date = statistics.get_int_array('last_play_date', 3) today_play_date = Time.todays_date() if ( last_play_date[0] == today_play_date[0] and last_play_date[1] == today_play_date[1] and last_play_date[2] == today_play_date[2] ): today_count = statistics.get_int('today_plays', 0) else: today_count = 0 account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0))) account.add_child(Node.s16('today_play_cnt', today_count)) account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0))) account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0))) account.add_child(Node.s16('interval_day', 0)) # Set up info node info = Node.void('info') root.add_child(info) info.add_child(Node.u16('ep', profile.get_int('ep'))) # Set up last information config = Node.void('config') root.add_child(config) config.add_child(Node.u8('mode', profile.get_int('mode'))) config.add_child(Node.s16('chara', profile.get_int('chara', -1))) config.add_child(Node.s16('music', profile.get_int('music', -1))) config.add_child(Node.u8('sheet', profile.get_int('sheet'))) config.add_child(Node.s8('category', profile.get_int('category', -1))) config.add_child(Node.s8('sub_category', profile.get_int('sub_category', -1))) config.add_child(Node.s8('chara_category', profile.get_int('chara_category', -1))) config.add_child(Node.s16('course_id', profile.get_int('course_id', -1))) config.add_child(Node.s8('course_folder', profile.get_int('course_folder', -1))) config.add_child(Node.s8('ms_banner_disp', profile.get_int('ms_banner_disp'))) config.add_child(Node.s8('ms_down_info', profile.get_int('ms_down_info'))) config.add_child(Node.s8('ms_side_info', profile.get_int('ms_side_info'))) config.add_child(Node.s8('ms_raise_type', profile.get_int('ms_raise_type'))) config.add_child(Node.s8('ms_rnd_type', profile.get_int('ms_rnd_type'))) # Player options option = Node.void('option') option_dict = profile.get_dict('option') root.add_child(option) option.add_child(Node.s16('hispeed', option_dict.get_int('hispeed'))) option.add_child(Node.u8('popkun', option_dict.get_int('popkun'))) option.add_child(Node.bool('hidden', option_dict.get_bool('hidden'))) option.add_child(Node.s16('hidden_rate', option_dict.get_int('hidden_rate'))) option.add_child(Node.bool('sudden', option_dict.get_bool('sudden'))) option.add_child(Node.s16('sudden_rate', option_dict.get_int('sudden_rate'))) option.add_child(Node.s8('randmir', option_dict.get_int('randmir'))) option.add_child(Node.s8('gauge_type', option_dict.get_int('gauge_type'))) option.add_child(Node.u8('ojama_0', option_dict.get_int('ojama_0'))) option.add_child(Node.u8('ojama_1', option_dict.get_int('ojama_1'))) option.add_child(Node.bool('forever_0', option_dict.get_bool('forever_0'))) option.add_child(Node.bool('forever_1', option_dict.get_bool('forever_1'))) option.add_child(Node.bool('full_setting', option_dict.get_bool('full_setting'))) option.add_child(Node.u8('judge', option_dict.get_int('judge'))) # Unknown custom category stuff? custom_cate = Node.void('custom_cate') root.add_child(custom_cate) custom_cate.add_child(Node.s8('valid', 0)) custom_cate.add_child(Node.s8('lv_min', -1)) custom_cate.add_child(Node.s8('lv_max', -1)) custom_cate.add_child(Node.s8('medal_min', -1)) custom_cate.add_child(Node.s8('medal_max', -1)) custom_cate.add_child(Node.s8('friend_no', -1)) custom_cate.add_child(Node.s8('score_flg', -1)) # Set up achievements achievements = self.data.local.user.get_achievements(self.game, self.version, userid) for achievement in achievements: if achievement.type[:5] == 'item_': itemtype = int(achievement.type[5:]) param = achievement.data.get_int('param') is_new = achievement.data.get_bool('is_new') item = Node.void('item') root.add_child(item) # Type is the type of unlock/item. Type 0 is song unlock in Eclale. # In this case, the id is the song ID according to the game. Unclear # what the param is supposed to be, but i've seen 8 and 0. Might be # what chart is available? item.add_child(Node.u8('type', itemtype)) item.add_child(Node.u16('id', achievement.id)) item.add_child(Node.u16('param', param)) item.add_child(Node.bool('is_new', is_new)) elif achievement.type == 'chara': friendship = achievement.data.get_int('friendship') chara = Node.void('chara_param') root.add_child(chara) chara.add_child(Node.u16('chara_id', achievement.id)) chara.add_child(Node.u16('friendship', friendship)) elif achievement.type == 'medal': level = achievement.data.get_int('level') exp = achievement.data.get_int('exp') set_count = achievement.data.get_int('set_count') get_count = achievement.data.get_int('get_count') medal = Node.void('medal') root.add_child(medal) medal.add_child(Node.s16('medal_id', achievement.id)) medal.add_child(Node.s16('level', level)) medal.add_child(Node.s32('exp', exp)) medal.add_child(Node.s32('set_count', set_count)) medal.add_child(Node.s32('get_count', get_count)) # Unknown customizations customize = Node.void('customize') root.add_child(customize) customize.add_child(Node.u16('effect_left', profile.get_int('effect_left'))) customize.add_child(Node.u16('effect_center', profile.get_int('effect_center'))) customize.add_child(Node.u16('effect_right', profile.get_int('effect_right'))) customize.add_child(Node.u16('hukidashi', profile.get_int('hukidashi'))) customize.add_child(Node.u16('comment_1', profile.get_int('comment_1'))) customize.add_child(Node.u16('comment_2', profile.get_int('comment_2'))) # NetVS section netvs = Node.void('netvs') root.add_child(netvs) netvs.add_child(Node.s16_array('record', [0] * 6)) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.string('dialog', '')) netvs.add_child(Node.s8_array('ojama_condition', [0] * 74)) netvs.add_child(Node.s8_array('set_ojama', [0] * 3)) netvs.add_child(Node.s8_array('set_recommend', [0] * 3)) netvs.add_child(Node.u32('netvs_play_cnt', 0)) # Event stuff event = Node.void('event') root.add_child(event) event.add_child(Node.s16('enemy_medal', profile.get_int('event_enemy_medal'))) event.add_child(Node.s16('hp', profile.get_int('event_hp'))) # Stamp stuff stamp = Node.void('stamp') root.add_child(stamp) stamp.add_child(Node.s16('stamp_id', profile.get_int('stamp_id'))) stamp.add_child(Node.s16('cnt', profile.get_int('stamp_cnt'))) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: newprofile = copy.deepcopy(oldprofile) # Set that we've seen this profile newprofile.replace_bool('welcom_pack', True) account = request.child('account') if account is not None: newprofile.replace_int('tutorial', account.child_value('tutorial')) newprofile.replace_int('read_news', account.child_value('read_news')) newprofile.replace_int('area_id', account.child_value('area_id')) newprofile.replace_int('lumina', account.child_value('lumina')) newprofile.replace_int_array('medal_set', 4, account.child_value('medal_set')) newprofile.replace_int_array('nice', 30, account.child_value('nice')) newprofile.replace_int_array('favorite_chara', 20, account.child_value('favorite_chara')) newprofile.replace_int_array('special_area', 8, account.child_value('special_area')) newprofile.replace_int_array('chocolate_charalist', 5, account.child_value('chocolate_charalist')) newprofile.replace_int_array('teacher_setting', 10, account.child_value('teacher_setting')) info = request.child('info') if info is not None: newprofile.replace_int('ep', info.child_value('ep')) config = request.child('config') if config is not None: newprofile.replace_int('mode', config.child_value('mode')) newprofile.replace_int('chara', config.child_value('chara')) newprofile.replace_int('music', config.child_value('music')) newprofile.replace_int('sheet', config.child_value('sheet')) newprofile.replace_int('category', config.child_value('category')) newprofile.replace_int('sub_category', config.child_value('sub_category')) newprofile.replace_int('chara_category', config.child_value('chara_category')) newprofile.replace_int('course_id', config.child_value('course_id')) newprofile.replace_int('course_folder', config.child_value('course_folder')) newprofile.replace_int('ms_banner_disp', config.child_value('ms_banner_disp')) newprofile.replace_int('ms_down_info', config.child_value('ms_down_info')) newprofile.replace_int('ms_side_info', config.child_value('ms_side_info')) newprofile.replace_int('ms_raise_type', config.child_value('ms_raise_type')) newprofile.replace_int('ms_rnd_type', config.child_value('ms_rnd_type')) option_dict = newprofile.get_dict('option') option = request.child('option') if option is not None: option_dict.replace_int('hispeed', option.child_value('hispeed')) option_dict.replace_int('popkun', option.child_value('popkun')) option_dict.replace_bool('hidden', option.child_value('hidden')) option_dict.replace_int('hidden_rate', option.child_value('hidden_rate')) option_dict.replace_bool('sudden', option.child_value('sudden')) option_dict.replace_int('sudden_rate', option.child_value('sudden_rate')) option_dict.replace_int('randmir', option.child_value('randmir')) option_dict.replace_int('gauge_type', option.child_value('gauge_type')) option_dict.replace_int('ojama_0', option.child_value('ojama_0')) option_dict.replace_int('ojama_1', option.child_value('ojama_1')) option_dict.replace_bool('forever_0', option.child_value('forever_0')) option_dict.replace_bool('forever_1', option.child_value('forever_1')) option_dict.replace_bool('full_setting', option.child_value('full_setting')) option_dict.replace_int('judge', option.child_value('judge')) newprofile.replace_dict('option', option_dict) customize = request.child('customize') if customize is not None: newprofile.replace_int('effect_left', customize.child_value('effect_left')) newprofile.replace_int('effect_center', customize.child_value('effect_center')) newprofile.replace_int('effect_right', customize.child_value('effect_right')) newprofile.replace_int('hukidashi', customize.child_value('hukidashi')) newprofile.replace_int('comment_1', customize.child_value('comment_1')) newprofile.replace_int('comment_2', customize.child_value('comment_2')) event = request.child('event') if event is not None: newprofile.replace_int('event_enemy_medal', event.child_value('enemy_medal')) newprofile.replace_int('event_hp', event.child_value('hp')) stamp = request.child('stamp') if stamp is not None: newprofile.replace_int('stamp_id', stamp.child_value('stamp_id')) newprofile.replace_int('stamp_cnt', stamp.child_value('cnt')) # Extract achievements for node in request.children: if node.name == 'item': itemid = node.child_value('id') itemtype = node.child_value('type') param = node.child_value('param') is_new = node.child_value('is_new') self.data.local.user.put_achievement( self.game, self.version, userid, itemid, f'item_{itemtype}', { 'param': param, 'is_new': is_new, }, ) elif node.name == 'chara_param': charaid = node.child_value('chara_id') friendship = node.child_value('friendship') self.data.local.user.put_achievement( self.game, self.version, userid, charaid, 'chara', { 'friendship': friendship, }, ) elif node.name == 'medal': medalid = node.child_value('medal_id') level = node.child_value('level') exp = node.child_value('exp') set_count = node.child_value('set_count') get_count = node.child_value('get_count') self.data.local.user.put_achievement( self.game, self.version, userid, medalid, 'medal', { 'level': level, 'exp': exp, 'set_count': set_count, 'get_count': get_count, }, ) # Keep track of play statistics self.update_play_statistics(userid) return newprofile