# vim: set fileencoding=utf-8 from abc import ABC, abstractmethod import binascii import random from typing import Any, Dict, List, Optional, Tuple from typing_extensions import Final from bemani.backend.popn.base import PopnMusicBase from bemani.common import Time, ID, Profile, ValidatedDict, Parallel from bemani.data import Data, UserID, Achievement, Link from bemani.protocol import Node class PopnMusicModernBase(PopnMusicBase, ABC): # Chart type, as returned from the game GAME_CHART_TYPE_EASY: Final[int] = 0 GAME_CHART_TYPE_NORMAL: Final[int] = 1 GAME_CHART_TYPE_HYPER: Final[int] = 2 GAME_CHART_TYPE_EX: Final[int] = 3 # Medal type, as returned from the game GAME_PLAY_MEDAL_CIRCLE_FAILED: Final[int] = 1 GAME_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 2 GAME_PLAY_MEDAL_STAR_FAILED: Final[int] = 3 GAME_PLAY_MEDAL_EASY_CLEAR: Final[int] = 4 GAME_PLAY_MEDAL_CIRCLE_CLEARED: Final[int] = 5 GAME_PLAY_MEDAL_DIAMOND_CLEARED: Final[int] = 6 GAME_PLAY_MEDAL_STAR_CLEARED: Final[int] = 7 GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO: Final[int] = 8 GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO: Final[int] = 9 GAME_PLAY_MEDAL_STAR_FULL_COMBO: Final[int] = 10 GAME_PLAY_MEDAL_PERFECT: Final[int] = 11 # Rank type, as returned from the game GAME_PLAY_RANK_E: Final[int] = 1 GAME_PLAY_RANK_D: Final[int] = 2 GAME_PLAY_RANK_C: Final[int] = 3 GAME_PLAY_RANK_B: Final[int] = 4 GAME_PLAY_RANK_A: Final[int] = 5 GAME_PLAY_RANK_AA: Final[int] = 6 GAME_PLAY_RANK_AAA: Final[int] = 7 GAME_PLAY_RANK_S: Final[int] = 8 # Biggest ID in the music DB GAME_MAX_MUSIC_ID: int # Biggest deco part ID in the game GAME_MAX_DECO_ID: int # Return the local2 and lobby2 service so that Pop'n Music 24+ will # send game packets. extra_services: List[str] = [ "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) ] if all_songs: 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_requests(self, request: Node) -> 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") @abstractmethod def get_common_config(self) -> Tuple[Dict[int, int], bool]: """ Return a tuple of configuration options for sending the common node back to the client. The first parameter is a dictionary whose keys are event IDs and values are the event phase number. The second parameter is a bool representing whether or not to send areas. """ def __construct_common_info(self, root: Node) -> None: phases, send_areas = self.get_common_config() for phaseid, phase_value in phases.items(): phase = Node.void("phase") root.add_child(phase) phase.add_child(Node.s16("event_id", phaseid)) phase.add_child(Node.s16("phase", phase_value)) # Gather course information 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")) ) if send_areas: for area_id in range(1, 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(self.GAME_MAX_DECO_ID): if goods_id < 15: price = 30 elif goods_id < 30: price = 40 elif goods_id < 45: price = 60 elif goods_id < 60: price = 80 elif goods_id < 98: price = 200 else: price = 250 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_num", 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 if score.data.get_int("medal") == self.PLAY_MEDAL_NO_PLAY: 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 if score.data.get_int("medal") == self.PLAY_MEDAL_NO_PLAY: 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 ) if request.child_value("is_image_store") == 1: self.broadcast_score(userid, songid, chart, medal, points, combo, 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 if score.data.get_int("medal") == self.PLAY_MEDAL_NO_PLAY: 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 if self.data.triggers.has_broadcast_destination(self.game) else -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)) ) game_config = self.get_game_config() if game_config.get_bool("force_unlock_songs"): songs = { song.id for song in self.data.local.music.get_all_songs(self.game, self.version) } for song in songs: item = Node.void("item") root.add_child(item) item.add_child(Node.u8("type", 0)) item.add_child(Node.u16("id", song)) item.add_child(Node.u16("param", 15)) item.add_child(Node.bool("is_new", False)) item.add_child(Node.u64("get_time", 0)) # 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 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? if game_config.get_bool("force_unlock_songs") and itemtype == 0: # We already sent song unlocks in the force unlock section above. continue item = Node.void("item") root.add_child(item) 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 = oldprofile.clone() 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 game_config = self.get_game_config() 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") if game_config.get_bool("force_unlock_songs") and itemtype == 0: # If we enabled force song unlocks, don't save songs to the profile. continue 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