# vim: set fileencoding=utf-8 import binascii import copy import random from typing import Any, Dict, List, Optional, Tuple from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.eclale import PopnMusicEclale from bemani.common import Time, ID, Profile, ValidatedDict, VersionConstants, Parallel from bemani.data import Data, UserID, Achievement, Link from bemani.protocol import Node class PopnMusicUsaNeko(PopnMusicBase): name = "Pop'n Music うさぎと猫と少年の夢" version = VersionConstants.POPN_MUSIC_USANEKO # 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 # Rank type, as returned from the game GAME_PLAY_RANK_E = 1 GAME_PLAY_RANK_D = 2 GAME_PLAY_RANK_C = 3 GAME_PLAY_RANK_B = 4 GAME_PLAY_RANK_A = 5 GAME_PLAY_RANK_AA = 6 GAME_PLAY_RANK_AAA = 7 GAME_PLAY_RANK_S = 8 # Biggest ID in the music DB GAME_MAX_MUSIC_ID = 1704 def previous_version(self) -> Optional[PopnMusicBase]: return PopnMusicEclale(self.data, self.config, self.model) def extra_services(self) -> List[str]: """ Return the local2 and lobby2 service so that Pop'n Music 24 will send game packets. """ return [ 'local2', 'lobby2', ] @classmethod def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]: """ Once a week, insert a new course. """ events = [] if data.local.network.should_schedule(cls.game, cls.version, 'course', 'weekly'): # Generate a new course list, save it to the DB. start_time, end_time = data.local.network.get_schedule_duration('weekly') all_songs = [song.id for song in data.local.music.get_all_songs(cls.game, cls.version)] course_song = random.choice(all_songs) data.local.game.put_time_sensitive_settings( cls.game, cls.version, 'course', { 'start_time': start_time, 'end_time': end_time, 'music': course_song, }, ) events.append(( 'pnm_course', { 'version': cls.version, 'song': course_song, }, )) # Mark that we did some actual work here. data.local.network.mark_scheduled(cls.game, cls.version, 'course', 'weekly') return events def __score_to_rank(self, score: int) -> int: if score < 50000: return self.GAME_PLAY_RANK_E if score < 62000: return self.GAME_PLAY_RANK_D if score < 72000: return self.GAME_PLAY_RANK_C if score < 82000: return self.GAME_PLAY_RANK_B if score < 90000: return self.GAME_PLAY_RANK_A if score < 95000: return self.GAME_PLAY_RANK_AA if score < 98000: return self.GAME_PLAY_RANK_AAA return self.GAME_PLAY_RANK_S def handle_lobby24_request(self, request: Node) -> Optional[Node]: # Stub out the entire lobby24 service return Node.void('lobby24') def handle_pcb24_error_request(self, request: Node) -> Node: return Node.void('pcb24') def handle_pcb24_boot_request(self, request: Node) -> Node: return Node.void('pcb24') def handle_pcb24_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('pcb24') def __construct_common_info(self, root: Node) -> None: # Event phases # TODO: Hook event mode settings up to the front end. phases = { # Default song phase availability (0-11) 0: 11, # Unknown event (0-2) 1: 2, # Unknown event (0-2) 2: 2, # Unknown event (0-4) 3: 4, # Unknown event (0-1) 4: 1, # Enable Net Taisen, including win/loss display on song select (0-1) 5: 1, # Enable NAVI-kun shunkyoku toujou, allows song 1608 to be unlocked (0-1) 6: 1, # Unknown event (0-1) 7: 1, # Unknown event (0-2) 8: 2, # Daily Mission (0-2) 9: 2, # NAVI-kun Song phase availability (0-15) 10: 15, # Unknown event (0-1) 11: 1, # Unknown event (0-2) 12: 2, # Enable Pop'n Peace preview song (0-1) 13: 1, } 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])) # Gather course informatino and course ranking for users. course_infos, achievements, profiles = Parallel.execute([ lambda: self.data.local.game.get_all_time_sensitive_settings(self.game, self.version, 'course'), lambda: self.data.local.user.get_all_achievements(self.game, self.version), lambda: self.data.local.user.get_all_profiles(self.game, self.version), ]) # Sort courses by newest to oldest so we can grab the newest 256. course_infos = sorted( course_infos, key=lambda c: c['start_time'], reverse=True, ) # Sort achievements within course ID from best to worst ranking. achievements_by_course_id: Dict[int, Dict[str, List[Tuple[UserID, Achievement]]]] = {} type_to_chart_lut: Dict[str, str] = { f'course_{self.GAME_CHART_TYPE_EASY}': "loc_ranking_e", f'course_{self.GAME_CHART_TYPE_NORMAL}': "loc_ranking_n", f'course_{self.GAME_CHART_TYPE_HYPER}': "loc_ranking_h", f'course_{self.GAME_CHART_TYPE_EX}': "loc_ranking_ex", } for uid, ach in achievements: if ach.type[:7] != 'course_': continue if ach.id not in achievements_by_course_id: achievements_by_course_id[ach.id] = { "loc_ranking_e": [], "loc_ranking_n": [], "loc_ranking_h": [], "loc_ranking_ex": [], } achievements_by_course_id[ach.id][type_to_chart_lut[ach.type]].append((uid, ach)) for courseid in achievements_by_course_id: for chart in ["loc_ranking_e", "loc_ranking_n", "loc_ranking_h", "loc_ranking_ex"]: achievements_by_course_id[courseid][chart] = sorted( achievements_by_course_id[courseid][chart], key=lambda uid_and_ach: uid_and_ach[1].data.get_int('score'), reverse=True, ) # Cache of userID to profile userid_to_profile: Dict[UserID, Profile] = {uid: profile for (uid, profile) in profiles} # Course ranking info for the last 256 courses for course_info in course_infos[:256]: course_id = int(course_info['start_time'] / 604800) course_rankings = achievements_by_course_id.get(course_id, {}) ranking_info = Node.void('ranking_info') root.add_child(ranking_info) ranking_info.add_child(Node.s16('course_id', course_id)) ranking_info.add_child(Node.u64('start_date', course_info['start_time'] * 1000)) ranking_info.add_child(Node.u64('end_date', course_info['end_time'] * 1000)) ranking_info.add_child(Node.s32('music_id', course_info['music'])) # Top 20 rankings for each particular chart. for name in ["loc_ranking_e", "loc_ranking_n", "loc_ranking_h", "loc_ranking_ex"]: chart_rankings = course_rankings.get(name, []) for pos, (uid, ach) in enumerate(chart_rankings[:20]): profile = userid_to_profile.get(uid, Profile(self.game, self.version, "", 0)) subnode = Node.void(name) ranking_info.add_child(subnode) subnode.add_child(Node.s16('rank', pos + 1)) subnode.add_child(Node.string('name', profile.get_str('name'))) subnode.add_child(Node.s16('chara_num', profile.get_int('chara', -1))) subnode.add_child(Node.s32('total_score', ach.data.get_int('score'))) subnode.add_child(Node.u8('clear_type', ach.data.get_int('clear_type'))) subnode.add_child(Node.u8('clear_rank', ach.data.get_int('clear_rank'))) for area_id in range(0, 16): area = Node.void('area') root.add_child(area) area.add_child(Node.s16('area_id', area_id)) area.add_child(Node.u64('end_date', 0)) area.add_child(Node.s16('medal_id', area_id)) area.add_child(Node.bool('is_limit', False)) for choco_id in range(0, 5): choco = Node.void('choco') root.add_child(choco) choco.add_child(Node.s16('choco_id', choco_id)) choco.add_child(Node.s32('param', -1)) # Set up goods, educated guess here. for goods_id in range(97): if goods_id < 15: price = 30 elif goods_id < 30: price = 40 elif goods_id < 45: price = 60 elif goods_id < 60: price = 80 else: price = 200 goods = Node.void('goods') root.add_child(goods) goods.add_child(Node.s32('item_id', goods_id + 1)) goods.add_child(Node.s16('item_type', 3)) goods.add_child(Node.s32('price', price)) goods.add_child(Node.s16('goods_type', 0)) # Ignoring NAVIfes node, we don't set these. # fes = Node.void('fes') # fes.add_child(Node.s16('fes_id', -1)) # fes.add_child(Node.s32('gauge_count', -1)) # fes.add_child(Node.s32_array('gauge', [-1, -1, -1, -1, -1, -1])) # fes.add_child(Node.s32_array('music', [-1, -1, -1, -1, -1, -1])) # fes.add_child(Node.s16('r', -1)) # fes.add_child(Node.s16('g', -1)) # fes.add_child(Node.s16('b', -1)) # fes.add_child(Node.s16('poster', -1)) # 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, ) # Top 20 Popular characters for rank, (charaid, _usecount) in enumerate(charamap[:20]): popular = Node.void('popular') root.add_child(popular) popular.add_child(Node.s16('rank', rank + 1)) popular.add_child(Node.s16('chara_num', charaid)) # Top 500 Popular music 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)) # Ignoring recommended music, we don't set this # recommend = Node.void('recommend') # root.add_child(recommend) # recommend.add_child(Node.s32_array('music_no', [-1] * 30)) # Ignoring mission points, we don't set these. # mission_point = Node.void('mission_point') # mission_point.add_child(Node.s32('point', -1)) # mission_point.add_child(Node.s32('bonus_point', -1)) # Ignoring medals, we don't set these. # medal = Node.void('medal') # medal.add_child(Node.s16('medal_id', -1)) # medal.add_child(Node.s16('percent', -1)) # Ignoring chara ranking, we don't set these. # chara_ranking = Node.void('chara_ranking') # chara_ranking.add_child(Node.s32('rank', -1)) # chara_ranking.add_child(Node.s32('kind_id', -1)) # chara_ranking.add_child(Node.s32('point', -1)) # chara_ranking.add_child(Node.s32('month', -1)) def handle_info24_common_request(self, root: Node) -> Node: root = Node.void('info24') self.__construct_common_info(root) return root def handle_player24_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('player24') root.add_child(Node.s8('result', 2)) return root def handle_player24_conversion_request(self, request: Node) -> Node: refid = request.child_value('ref_id') name = request.child_value('name') chara = request.child_value('chara') achievements: List[Achievement] = [] 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') get_time = node.child_value('get_time') achievements.append( Achievement( itemid, f'item_{itemtype}', 0, { 'param': param, 'is_new': is_new, 'get_time': get_time, }, ) ) root = self.new_profile_by_refid(refid, name, chara, achievements=achievements) if root is None: root = Node.void('player24') root.add_child(Node.s8('result', 2)) return root def handle_player24_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('player24') root.add_child(Node.s8('result', 2)) return root def handle_player24_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('player24') def handle_player24_update_ranking_request(self, request: Node) -> Node: refid = request.child_value('ref_id') root = Node.void('player24') 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('course_id') chart = request.child_value('sheet_num') score = request.child_value('total_score') clear_type = request.child_value('clear_type') clear_rank = request.child_value('clear_rank') prefecture = request.child_value('pref') loc_id = ID.parse_machine_id(request.child_value('location_id')) course_type = f"course_{chart}" old_course = self.data.local.user.get_achievement( self.game, self.version, userid, course_id, course_type, ) if old_course is None: old_course = ValidatedDict() new_course = ValidatedDict({ 'score': max(score, old_course.get_int('score')), 'clear_type': max(clear_type, old_course.get_int('clear_type')), 'clear_rank': max(clear_rank, old_course.get_int('clear_rank')), 'pref': prefecture, 'lid': loc_id, 'count': old_course.get_int('count') + 1, }) self.data.local.user.put_achievement( self.game, self.version, userid, course_id, course_type, new_course, ) # Handle fetching all scores uids_and_courses, profile = Parallel.execute([ lambda: self.data.local.user.get_all_achievements(self.game, self.version), lambda: self.get_profile(userid) or Profile(self.game, self.version, "", 0) ]) # Grab a sorted list of all scores for this course and chart global_uids_and_courses = sorted( [(uid, ach) for (uid, ach) in uids_and_courses if ach.type == course_type and ach.id == course_id], key=lambda uid_and_course: uid_and_course[1].data.get_int('score'), reverse=True, ) # Grab smaller lists that contain only sorted for our prefecture/location pref_uids_and_courses = [(uid, ach) for (uid, ach) in global_uids_and_courses if ach.data.get_int('pref') == prefecture] loc_uids_and_courses = [(uid, ach) for (uid, ach) in global_uids_and_courses if ach.data.get_int('lid') == loc_id] def _get_rank(uac: List[Tuple[UserID, Achievement]]) -> Optional[int]: for rank, (uid, _) in enumerate(uac): if uid == userid: return rank + 1 return None for nodename, ranklist in [ ("all_ranking", global_uids_and_courses), ("pref_ranking", pref_uids_and_courses), ("location_ranking", loc_uids_and_courses), ]: # Grab the rank, bail if we don't have any answer since the game doesn't # require a response. rank = _get_rank(ranklist) if rank is None: continue # Send back the data for this ranking. node = Node.void(nodename) root.add_child(node) node.add_child(Node.string("name", profile.get_str('name', 'なし'))) node.add_child(Node.s16("chara_num", profile.get_int('chara', -1))) node.add_child(Node.s32("total_score", new_course.get_int('score'))) node.add_child(Node.u8("clear_type", new_course.get_int('clear_type'))) node.add_child(Node.u8("clear_rank", new_course.get_int('clear_rank'))) node.add_child(Node.s16("player_count", len(ranklist))) node.add_child(Node.s16("player_rank", rank)) return root def handle_player24_friend_request(self, request: Node) -> Node: refid = request.attribute('ref_id') no = int(request.attribute('no', '-1')) root = Node.void('player24') 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))) # UsaNeko 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('clearrank', str(self.__score_to_rank(score.points))) music.set_attribute('cleartype', 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])) achievements = self.data.local.user.get_achievements(self.game, self.version, rivalid) for achievement in achievements: if achievement.type[:7] == 'course_': sheet = int(achievement.type[7:]) course_data = Node.void('course_data') root.add_child(course_data) course_data.add_child(Node.s16('course_id', achievement.id)) course_data.add_child(Node.u8('clear_type', achievement.data.get_int('clear_type'))) course_data.add_child(Node.u8('clear_rank', achievement.data.get_int('clear_rank'))) course_data.add_child(Node.s32('total_score', achievement.data.get_int('score'))) course_data.add_child(Node.s32('update_count', achievement.data.get_int('count'))) course_data.add_child(Node.u8('sheet_num', sheet)) return root def handle_player24_read_score_request(self, request: Node) -> Node: refid = request.child_value('ref_id') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: return Node.void('player24') root = Node.void('player24') 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 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', 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, }[score.data.get_int('medal')])) music.add_child(Node.u8('clear_rank', self.__score_to_rank(score.points))) music.add_child(Node.s16('cnt', score.plays)) return root def handle_player24_write_music_request(self, request: Node) -> Node: refid = request.child_value('ref_id') root = Node.void('player24') 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('clear_type') 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 handle_player24_start_request(self, request: Node) -> Node: root = Node.void('player24') root.add_child(Node.s32('play_id', 0)) self.__construct_common_info(root) return root def handle_player24_logout_request(self, request: Node) -> Node: return Node.void('player24') def handle_player24_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('player_point', 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('player24') def format_conversion(self, userid: UserID, profile: Profile) -> Node: root = Node.void('player24') 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('con_type', 0)) root.add_child(Node.s8('result', 1)) # Scores 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 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', 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, }[score.data.get_int('medal')])) music.add_child(Node.u8('clear_rank', self.__score_to_rank(score.points))) 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('player24') # Mark this as a current profile root.add_child(Node.s8('result', 0)) # Basic account info account = Node.void('account') root.add_child(account) account.add_child(Node.string('g_pm_id', self.format_extid(profile.extid))) account.add_child(Node.string('name', profile.get_str('name', 'なし'))) account.add_child(Node.s16('area_id', profile.get_int('area_id'))) account.add_child(Node.s16('use_navi', profile.get_int('use_navi'))) account.add_child(Node.s16('read_news', profile.get_int('read_news'))) 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, [-1] * 8))) account.add_child(Node.s16_array('chocolate_charalist', profile.get_int_array('chocolate_charalist', 5, [-1] * 5))) account.add_child(Node.s32('chocolate_sp_chara', profile.get_int('chocolate_sp_chara', -1))) account.add_child(Node.s32('chocolate_pass_cnt', profile.get_int('chocolate_pass_cnt'))) account.add_child(Node.s32('chocolate_hon_cnt', profile.get_int('chocolate_hon_cnt'))) account.add_child(Node.s16_array('teacher_setting', profile.get_int_array('teacher_setting', 10, [-1] * 10))) account.add_child(Node.bool('welcom_pack', False)) # Set to true to grant extra stage no matter what. account.add_child(Node.s32('ranking_node', profile.get_int('ranking_node'))) account.add_child(Node.s32('chara_ranking_kind_id', profile.get_int('chara_ranking_kind_id'))) account.add_child(Node.s8('navi_evolution_flg', profile.get_int('navi_evolution_flg'))) account.add_child(Node.s32('ranking_news_last_no', profile.get_int('ranking_news_last_no'))) account.add_child(Node.s32('power_point', profile.get_int('power_point'))) account.add_child(Node.s32('player_point', profile.get_int('player_point', 300))) account.add_child(Node.s32_array('power_point_list', profile.get_int_array('power_point_list', 20, [-1] * 20))) # Tutorial handling is all sorts of crazy in UsaNeko. the tutorial flag # is split into two values. The game uses the flag modulo 100 for navigation # tutorial progress, and the flag divided by 100 for the hold note tutorial. # The hold note tutorial will activate the first time you choose a song with # hold notes in it, regardless of whether you say yes/no. The total times you # have ever played Pop'n Music also factors in, as the game will only attempt # to offer you the basic "how to play" tutorial screen and song on the playthrough # attempt where the "total_play_cnt" value is 1. The game expects this to be 1-based, # and if you set it to 0 for the first playthorough then it will play a mandatory # cursed tutorial stage on the second profile load using the chart of your last # played song and keysounds of system menu entries. Valid values for each of the # two tutorial values is as follows: # # Lower values: # 0 - Brand new profile and user has not been prompted to choose any tutorials. # Prompts the user for the nagivation tutorial. If the user selects "no" then # moves the tutorial state to "1" at the end of the round. If the user selects # "yes" then moves the tutorial state to "3" immediately and starts the navigation # tutorial. If the total play count for this user is "1" when this value is hit, # the game will offer a basic "how to play" tutorial that can be played or skipped. # 1 - Prompt the user on the mode select screen asking them if they want to see # the navigation tutorial. If the user selects "no" then moves the tutorial state # to "2" after the round. If the user selects "yes" then moves the tutorial state # to "3" immediately. If the total play count for this user is "1" when this value # is hit, then the game will bug out and play the hold note tutorial and then crash. # 2 - Prompt the user on the mode select screen asking them if they want to see # the navigation tutorial. If the user selects "no" then moves the tutorial state # to "8" immediately. If the user selects "yes" then moves the tutorial state # to "3" immediately. If the total play count for this user is "1" when this value # is hit, then the game will bug out and play the hold note tutorial and then crash. # 3 - Display some tutorial elements on most screens, and then advance the tutorial # state to "4" on profile save. # 4 - Display some tutorial elements on most screens, and then advance the tutorial # state to "5" on profile save. # 5 - Display some tutorial elements on most screens, and then prompt user with a # repeat tutorial question. If the user selects "no" then moves the tutorial # state to "8". If the user selects "yes" then moves the tutorial state to "3". # 6 - Do nothing, display nothing, but advance the tutorial state to "7" at the # end of the game. It seems that nothing requests this state. # 7 - Display guide information prompt on the option select screen. Game moves # this to "8" after this tutorial has been displayed. It appears that there is # code to go to this state instead of "8" when selecting "no" on the navigation # tutorial prompt but only when the total play count is "1". That crashes the game # as documented above, so it is not clear how this state was ever reachable. # 8 - Do not display any more tutorial stuff, this is a terminal state. # # Upper values: # 0 - Brand new profile and user has not been asked for the above navigation tutorial # or shown an optional "how to play" tutorial. The game will advance this to "1" # after going through the mode and character select screens, but only if the total # play count is "1". # 1 - Hold note tutorial has not been activated yet and will be displayed when # the player chooses a song with hold notes. Game moves this to "2" after this # tutorial has been activated. # 2 - Hold note tutorial was displayed to the user, but the mini-tutorial showing # the hold note indicator that pops up after the hold note tutorial has not # been displayed yet. Presumably this is just in case you play a hold note # song on your last stage. Game moves this to "3" after this tutorial has been # displayed. # 3 - All hold note tutorials are finished, this is a terminal state. statistics = self.get_play_statistics(userid) account.add_child(Node.s16('tutorial', profile.get_int('tutorial', 100 if statistics.total_plays > 1 else 0))) # 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.s16_array('license_data', [-1] * 20)) # Song statistics last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 10)] most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 20)] while len(last_played) < 10: last_played.append(-1) while len(most_played) < 20: most_played.append(-1) account.add_child(Node.s16_array('my_best', most_played)) account.add_child(Node.s16_array('latest_music', last_played)) # Player statistics account.add_child(Node.s16('total_play_cnt', statistics.total_plays)) account.add_child(Node.s16('today_play_cnt', statistics.today_plays)) account.add_child(Node.s16('consecutive_days', statistics.consecutive_days)) account.add_child(Node.s16('total_days', statistics.total_days)) account.add_child(Node.s16('interval_day', 0)) # 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)) # eAmuse account link eaappli = Node.void('eaappli') root.add_child(eaappli) eaappli.add_child(Node.s8('relation', -1)) # Player info info = Node.void('info') root.add_child(info) info.add_child(Node.u16('ep', profile.get_int('ep'))) # Player config 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', -1))) config.add_child(Node.s8('ms_down_info', profile.get_int('ms_down_info', -1))) config.add_child(Node.s8('ms_side_info', profile.get_int('ms_side_info', -1))) config.add_child(Node.s8('ms_raise_type', profile.get_int('ms_raise_type', -1))) config.add_child(Node.s8('ms_rnd_type', profile.get_int('ms_rnd_type', -1))) config.add_child(Node.s8('banner_sort', profile.get_int('banner_sort', -1))) # 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'))) option.add_child(Node.s8('guide_se', option_dict.get_int('guide_se'))) # Player custom category 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)) # Navi data navi_data = Node.void('navi_data') root.add_child(navi_data) if 'navi_points' in profile: navi_data.add_child(Node.s32_array('raisePoint', profile.get_int_array('navi_points', 5))) # 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') get_time = achievement.data.get_int('get_time') item = Node.void('item') root.add_child(item) # Item type can be 0-6 inclusive and is the type of the unlock/item. # Item 0 is music unlocks. 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 # seen 8 and 0. Might be what chart is available? # # Item limits are as follows: # 0: 1704 - ID is the music ID that the player purchased/unlocked. # 1: 2201 # 2: 3 # 3: 97 - ID points at a character part that can be purchased on the character screen. # 4: 1 # 5: 1 # 6: 60 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)) item.add_child(Node.u64('get_time', get_time)) 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 == 'navi': # There should only be 12 of these. friendship = achievement.data.get_int('friendship') # This relies on the above Navi data section to ensure the navi_param # node is created. navi_param = Node.void('navi_param') navi_data.add_child(navi_param) navi_param.add_child(Node.u16('navi_id', achievement.id)) navi_param.add_child(Node.s32('friendship', friendship)) elif achievement.type == 'area': # There should only be 16 of these. index = achievement.data.get_int('index') points = achievement.data.get_int('points') cleared = achievement.data.get_bool('cleared') diary = achievement.data.get_int('diary') area = Node.void('area') root.add_child(area) area.add_child(Node.u32('area_id', achievement.id)) area.add_child(Node.u8('chapter_index', index)) area.add_child(Node.u16('gauge_point', points)) area.add_child(Node.bool('is_cleared', cleared)) area.add_child(Node.u32('diary', diary)) elif achievement.type[:7] == 'course_': sheet = int(achievement.type[7:]) course_data = Node.void('course_data') root.add_child(course_data) course_data.add_child(Node.s16('course_id', achievement.id)) course_data.add_child(Node.u8('clear_type', achievement.data.get_int('clear_type'))) course_data.add_child(Node.u8('clear_rank', achievement.data.get_int('clear_rank'))) course_data.add_child(Node.s32('total_score', achievement.data.get_int('score'))) course_data.add_child(Node.s32('update_count', achievement.data.get_int('count'))) course_data.add_child(Node.u8('sheet_num', sheet)) elif achievement.type == 'fes': index = achievement.data.get_int('index') points = achievement.data.get_int('points') cleared = achievement.data.get_bool('cleared') fes = Node.void('fes') root.add_child(fes) fes.add_child(Node.u32('fes_id', achievement.id)) fes.add_child(Node.u8('chapter_index', index)) fes.add_child(Node.u16('gauge_point', points)) fes.add_child(Node.bool('is_cleared', cleared)) # Handle daily mission. Note that we should be presenting 3 random IDs # in the range of 1-228 inclusive, and presenting three new ones per day. achievements = self.data.local.user.get_time_based_achievements( self.game, self.version, userid, since=Time.beginning_of_today(), until=Time.end_of_today(), ) achievements = sorted(achievements, key=lambda a: a.timestamp) daily_missions: Dict[int, ValidatedDict] = {} # Find the newest version of each daily mission completion, # since we've sorted by time above. If we haven't started for # today, the defaults will be set after this loop so we at least # give the game the right ID. for achievement in achievements: if achievement.type == 'mission': daily_missions[achievement.id] = achievement.data while len(daily_missions) < 3: new_id = random.randint(1, 228) if new_id not in daily_missions: daily_missions[new_id] = ValidatedDict() for i, (daily_id, data) in enumerate(daily_missions.items()): if i >= 3: break points = data.get_int('points') complete = data.get_int('complete') mission = Node.void('mission') root.add_child(mission) mission.add_child(Node.u32('mission_id', daily_id)) mission.add_child(Node.u32('gauge_point', points)) mission.add_child(Node.u32('mission_comp', complete)) # Player 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)) # Character 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'))) # 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) 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('use_navi', account.child_value('use_navi')) newprofile.replace_int('ranking_node', account.child_value('ranking_node')) newprofile.replace_int('chara_ranking_kind_id', account.child_value('chara_ranking_kind_id')) newprofile.replace_int('navi_evolution_flg', account.child_value('navi_evolution_flg')) newprofile.replace_int('ranking_news_last_no', account.child_value('ranking_news_last_no')) newprofile.replace_int('power_point', account.child_value('power_point')) newprofile.replace_int('player_point', account.child_value('player_point')) 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('chocolate_sp_chara', account.child_value('chocolate_sp_chara')) newprofile.replace_int('chocolate_pass_cnt', account.child_value('chocolate_pass_cnt')) newprofile.replace_int('chocolate_hon_cnt', account.child_value('chocolate_hon_cnt')) newprofile.replace_int('chocolate_giri_cnt', account.child_value('chocolate_giri_cnt')) newprofile.replace_int('chocolate_kokyu_cnt', account.child_value('chocolate_kokyu_cnt')) newprofile.replace_int_array('teacher_setting', 10, account.child_value('teacher_setting')) newprofile.replace_int_array('power_point_list', 20, account.child_value('power_point_list')) info = request.child('info') if info is not None: newprofile.replace_int('ep', info.child_value('ep')) 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')) 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')) newprofile.replace_int('banner_sort', config.child_value('banner_sort')) 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')) option_dict.replace_int('guide_se', option.child_value('guide_se')) 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')) navi_data = request.child('navi_data') if navi_data is not None: newprofile.replace_int_array('navi_points', 5, navi_data.child_value('raisePoint')) # Extract navi achievements for node in navi_data.children: if node.name == 'navi_param': navi_id = node.child_value('navi_id') friendship = node.child_value('friendship') self.data.local.user.put_achievement( self.game, self.version, userid, navi_id, 'navi', { 'friendship': friendship, }, ) # 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') get_time = node.child_value('get_time') self.data.local.user.put_achievement( self.game, self.version, userid, itemid, f'item_{itemtype}', { 'param': param, 'is_new': is_new, 'get_time': get_time, }, ) 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 == 'area': area_id = node.child_value('area_id') index = node.child_value('chapter_index') points = node.child_value('gauge_point') cleared = node.child_value('is_cleared') diary = node.child_value('diary') self.data.local.user.put_achievement( self.game, self.version, userid, area_id, 'area', { 'index': index, 'points': points, 'cleared': cleared, 'diary': diary, }, ) elif node.name == 'mission': # If you don't send the right values on login, then # the game sends 0 for mission_id three times. Skip # those values since they're bogus. mission_id = node.child_value('mission_id') if mission_id > 0: points = node.child_value('gauge_point') complete = node.child_value('mission_comp') self.data.local.user.put_time_based_achievement( self.game, self.version, userid, mission_id, 'mission', { 'points': points, 'complete': complete, }, ) # Unlock NAVI-kun and Kenshi Yonezu after one play for songid in [1592, 1608]: self.data.local.user.put_achievement( self.game, self.version, userid, songid, 'item_0', { 'param': 0xF, 'is_new': False, 'get_time': Time.now(), }, ) # Keep track of play statistics self.update_play_statistics(userid) return newprofile