from typing import Optional, Dict, Any, Tuple from typing_extensions import Final from bemani.backend.reflec.base import ReflecBeatBase from bemani.backend.reflec.reflecbeat import ReflecBeat from bemani.common import Profile, VersionConstants, ID, Time from bemani.data import UserID from bemani.protocol import Node class ReflecBeatLimelight(ReflecBeatBase): name: str = "REFLEC BEAT limelight" version: int = VersionConstants.REFLEC_BEAT_LIMELIGHT # Clear types according to the game GAME_CLEAR_TYPE_NO_PLAY: Final[int] = 0 GAME_CLEAR_TYPE_FAILED: Final[int] = 2 GAME_CLEAR_TYPE_CLEARED: Final[int] = 3 GAME_CLEAR_TYPE_FULL_COMBO: Final[int] = 4 # Reflec Beat Limelight requires non-expired profiles to do conversions properly supports_expired_profiles: bool = False def previous_version(self) -> Optional[ReflecBeatBase]: return ReflecBeat(self.data, self.config, self.model) @classmethod def get_settings(cls) -> Dict[str, Any]: """ Return all of our front-end modifiably settings. """ return { "bools": [ { "name": "Force Song Unlock", "tip": "Force unlock all songs.", "category": "game_config", "setting": "force_unlock_songs", }, ], "ints": [], } def __db_to_game_clear_type(self, db_clear_type: int, db_combo_type: int) -> int: if db_clear_type == self.CLEAR_TYPE_NO_PLAY: return self.GAME_CLEAR_TYPE_NO_PLAY if db_clear_type == self.CLEAR_TYPE_FAILED: return self.GAME_CLEAR_TYPE_FAILED if db_clear_type in [ self.CLEAR_TYPE_CLEARED, self.CLEAR_TYPE_HARD_CLEARED, self.CLEAR_TYPE_S_HARD_CLEARED, ]: if db_combo_type in [ self.COMBO_TYPE_NONE, self.COMBO_TYPE_ALMOST_COMBO, ]: return self.GAME_CLEAR_TYPE_CLEARED if db_combo_type in [ self.COMBO_TYPE_FULL_COMBO, self.COMBO_TYPE_FULL_COMBO_ALL_JUST, ]: return self.GAME_CLEAR_TYPE_FULL_COMBO raise Exception(f"Invalid db_combo_type {db_combo_type}") raise Exception(f"Invalid db_clear_type {db_clear_type}") def __game_to_db_clear_type(self, game_clear_type: int) -> Tuple[int, int]: if game_clear_type == self.GAME_CLEAR_TYPE_NO_PLAY: return (self.CLEAR_TYPE_NO_PLAY, self.COMBO_TYPE_NONE) if game_clear_type == self.GAME_CLEAR_TYPE_FAILED: return (self.CLEAR_TYPE_FAILED, self.COMBO_TYPE_NONE) if game_clear_type == self.GAME_CLEAR_TYPE_CLEARED: return (self.CLEAR_TYPE_CLEARED, self.COMBO_TYPE_NONE) if game_clear_type == self.GAME_CLEAR_TYPE_FULL_COMBO: return (self.CLEAR_TYPE_CLEARED, self.COMBO_TYPE_FULL_COMBO) raise Exception(f"Invalid game_clear_type {game_clear_type}") def handle_log_exception_request(self, request: Node) -> Node: return Node.void("log") def handle_log_pcb_status_request(self, request: Node) -> Node: return Node.void("log") def handle_log_opsetting_request(self, request: Node) -> Node: return Node.void("log") def handle_log_play_request(self, request: Node) -> Node: return Node.void("log") def handle_pcbinfo_get_request(self, request: Node) -> Node: shop_id = ID.parse_machine_id(request.child_value("lid")) machine = self.get_machine_by_id(shop_id) if machine is not None: machine_name = machine.name close = machine.data.get_bool("close") hour = machine.data.get_int("hour") minute = machine.data.get_int("minute") pref = machine.data.get_int("pref", self.get_machine_region()) else: machine_name = "" close = False hour = 0 minute = 0 pref = self.get_machine_region() root = Node.void("pcbinfo") info = Node.void("info") root.add_child(info) info.add_child(Node.string("name", machine_name)) info.add_child(Node.s16("pref", pref)) info.add_child(Node.bool("close", close)) info.add_child(Node.u8("hour", hour)) info.add_child(Node.u8("min", minute)) return root def handle_pcbinfo_set_request(self, request: Node) -> Node: self.update_machine_name(request.child_value("info/name")) self.update_machine_data( { "close": request.child_value("info/close"), "hour": request.child_value("info/hour"), "minute": request.child_value("info/min"), "pref": request.child_value("info/pref"), } ) return Node.void("pcbinfo") def __add_event_info(self, request: Node) -> None: events: Dict[int, int] = {} for (_eventid, _phase) in events.items(): data = Node.void("data") request.add_child(data) data.add_child(Node.s32("type", -1)) data.add_child(Node.s32("value", -1)) def handle_sysinfo_get_request(self, request: Node) -> Node: root = Node.void("sysinfo") trd = Node.void("trd") root.add_child(trd) # Add event info self.__add_event_info(trd) return root def handle_ranking_read_request(self, request: Node) -> Node: root = Node.void("ranking") licenses = Node.void("lic_10") root.add_child(licenses) originals = Node.void("org_10") root.add_child(originals) licenses.add_child(Node.time("time", Time.now())) originals.add_child(Node.time("time", Time.now())) hitchart = self.data.local.music.get_hit_chart(self.game, self.version, 10) rank = 1 for (mid, _plays) in hitchart: record = Node.void("record") originals.add_child(record) record.add_child(Node.s16("id", mid)) record.add_child(Node.s16("rank", rank)) rank = rank + 1 return root def handle_event_r_get_all_request(self, request: Node) -> Node: limit = request.child_value("limit") comments = [ achievement for achievement in self.data.local.user.get_all_time_based_achievements( self.game, self.version ) if achievement[1].type == "puzzle_comment" ] comments.sort(key=lambda x: x[1].timestamp, reverse=True) statuses = self.data.local.lobby.get_all_play_session_infos( self.game, self.version ) statuses.sort(key=lambda x: x[1]["time"], reverse=True) # Cap all comment blocks to the limit if limit >= 0: comments = comments[:limit] statuses = statuses[:limit] # Mapping of profiles to userIDs uid_mapping = { uid: prof for (uid, prof) in self.get_any_profiles( [c[0] for c in comments] + [s[0] for s in statuses] ) } # Mapping of location ID to machine name lid_mapping: Dict[int, str] = {} root = Node.void("event_r") root.add_child(Node.s32("time", Time.now())) statusnode = Node.void("status") root.add_child(statusnode) commentnode = Node.void("comment") root.add_child(commentnode) for (uid, comment) in comments: lid = ID.parse_machine_id(comment.data.get_str("lid")) # Look up external data for the request if lid not in lid_mapping: machine = self.get_machine_by_id(lid) if machine is not None: lid_mapping[lid] = machine.name else: lid_mapping[lid] = "" c = Node.void("c") commentnode.add_child(c) c.add_child(Node.s32("uid", uid_mapping[uid].extid)) c.add_child(Node.string("p_name", uid_mapping[uid].get_str("name"))) c.add_child(Node.s32("exp", uid_mapping[uid].get_int("exp"))) c.add_child(Node.s32("customize", comment.data.get_int("customize"))) c.add_child(Node.s32("tid", comment.data.get_int("teamid"))) c.add_child(Node.string("t_name", comment.data.get_str("teamname"))) c.add_child(Node.string("lid", comment.data.get_str("lid"))) c.add_child(Node.string("s_name", lid_mapping[lid])) c.add_child(Node.s8("pref", comment.data.get_int("prefecture"))) c.add_child(Node.s32("time", comment.timestamp)) c.add_child(Node.string("comment", comment.data.get_str("comment"))) c.add_child(Node.bool("is_tweet", comment.data.get_bool("tweet"))) for (uid, status) in statuses: lid = ID.parse_machine_id(status.get_str("lid")) # Look up external data for the request if lid not in lid_mapping: machine = self.get_machine_by_id(lid) if machine is not None: lid_mapping[lid] = machine.name else: lid_mapping[lid] = "" s = Node.void("s") statusnode.add_child(s) s.add_child(Node.s32("uid", uid_mapping[uid].extid)) s.add_child(Node.string("p_name", uid_mapping[uid].get_str("name"))) s.add_child(Node.s32("exp", uid_mapping[uid].get_int("exp"))) s.add_child(Node.s32("customize", status.get_int("customize"))) s.add_child(Node.s32("tid", uid_mapping[uid].get_int("team_id", -1))) s.add_child( Node.string("t_name", uid_mapping[uid].get_str("team_name", "")) ) s.add_child(Node.string("lid", status.get_str("lid"))) s.add_child(Node.string("s_name", lid_mapping[lid])) s.add_child(Node.s8("pref", status.get_int("prefecture"))) s.add_child(Node.s32("time", status.get_int("time"))) s.add_child(Node.s8("status", status.get_int("status"))) s.add_child(Node.s8("stage", status.get_int("stage"))) s.add_child(Node.s32("mid", status.get_int("mid"))) s.add_child(Node.s8("ng", status.get_int("ng"))) return root def handle_event_w_add_comment_request(self, request: Node) -> Node: extid = request.child_value("uid") userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is None: # Anonymous comment userid = UserID(0) customize = request.child_value("customize") lid = request.child_value("lid") teamid = request.child_value("tid") teamname = request.child_value("t_name") prefecture = request.child_value("pref") comment = request.child_value("comment") is_tweet = request.child_value("is_tweet") # Link comment to user's profile self.data.local.user.put_time_based_achievement( self.game, self.version, userid, 0, # We never have an ID for this, since comments are add-only "puzzle_comment", { "customize": customize, "lid": lid, "teamid": teamid, "teamname": teamname, "prefecture": prefecture, "comment": comment, "tweet": is_tweet, }, ) return Node.void("event_w") def handle_event_w_update_status_request(self, request: Node) -> Node: # Update user status so puzzle comments can show it extid = request.child_value("uid") userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: customize = request.child_value("customize") status = request.child_value("status") stage = request.child_value("stage") mid = request.child_value("mid") ng = request.child_value("ng") lid = request.child_value("lid") prefecture = request.child_value("pref") self.data.local.lobby.put_play_session_info( self.game, self.version, userid, { "customize": customize, "status": status, "stage": stage, "mid": mid, "ng": ng, "lid": lid, "prefecture": prefecture, }, ) return Node.void("event_w") def handle_lobby_entry_request(self, request: Node) -> Node: root = Node.void("lobby") root.add_child(Node.s32("interval", 120)) root.add_child(Node.s32("interval_p", 120)) # Create a lobby entry for this user extid = request.child_value("e/uid") userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: profile = self.get_profile(userid) self.data.local.lobby.put_lobby( self.game, self.version, userid, { "mid": request.child_value("e/mid"), "ng": request.child_value("e/ng"), "mopt": request.child_value("e/mopt"), "tid": request.child_value("e/tid"), "tn": request.child_value("e/tn"), "topt": request.child_value("e/topt"), "lid": request.child_value("e/lid"), "sn": request.child_value("e/sn"), "pref": request.child_value("e/pref"), "stg": request.child_value("e/stg"), "pside": request.child_value("e/pside"), "eatime": request.child_value("e/eatime"), "ga": request.child_value("e/ga"), "gp": request.child_value("e/gp"), "la": request.child_value("e/la"), }, ) lobby = self.data.local.lobby.get_lobby( self.game, self.version, userid, ) root.add_child(Node.s32("eid", lobby.get_int("id"))) e = Node.void("e") root.add_child(e) e.add_child(Node.s32("eid", lobby.get_int("id"))) e.add_child(Node.u16("mid", lobby.get_int("mid"))) e.add_child(Node.u8("ng", lobby.get_int("ng"))) e.add_child(Node.s32("uid", profile.extid)) e.add_child(Node.string("pn", profile.get_str("name"))) e.add_child(Node.s32("uattr", profile.get_int("uattr"))) e.add_child(Node.s32("mopt", lobby.get_int("mopt"))) e.add_child(Node.s16("mg", profile.get_int("mg"))) e.add_child(Node.s32("tid", lobby.get_int("tid"))) e.add_child(Node.string("tn", lobby.get_str("tn"))) e.add_child(Node.s32("topt", lobby.get_int("topt"))) e.add_child(Node.string("lid", lobby.get_str("lid"))) e.add_child(Node.string("sn", lobby.get_str("sn"))) e.add_child(Node.u8("pref", lobby.get_int("pref"))) e.add_child(Node.s8("stg", lobby.get_int("stg"))) e.add_child(Node.s8("pside", lobby.get_int("pside"))) e.add_child(Node.s16("eatime", lobby.get_int("eatime"))) e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4))) e.add_child(Node.u16("gp", lobby.get_int("gp"))) e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4))) return root def handle_lobby_read_request(self, request: Node) -> Node: root = Node.void("lobby") root.add_child(Node.s32("interval", 120)) root.add_child(Node.s32("interval_p", 120)) # Look up all lobbies matching the criteria specified mg = request.child_value("m_grade") # noqa: F841 extid = request.child_value("uid") limit = request.child_value("max") userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: lobbies = self.data.local.lobby.get_all_lobbies(self.game, self.version) for (user, lobby) in lobbies: if limit <= 0: break if user == userid: # If we have our own lobby, don't return it continue profile = self.get_profile(user) if profile is None: # No profile info, don't return this lobby continue e = Node.void("e") root.add_child(e) e.add_child(Node.s32("eid", lobby.get_int("id"))) e.add_child(Node.u16("mid", lobby.get_int("mid"))) e.add_child(Node.u8("ng", lobby.get_int("ng"))) e.add_child(Node.s32("uid", profile.extid)) e.add_child(Node.string("pn", profile.get_str("name"))) e.add_child(Node.s32("uattr", profile.get_int("uattr"))) e.add_child(Node.s32("mopt", lobby.get_int("mopt"))) e.add_child(Node.s16("mg", profile.get_int("mg"))) e.add_child(Node.s32("tid", lobby.get_int("tid"))) e.add_child(Node.string("tn", lobby.get_str("tn"))) e.add_child(Node.s32("topt", lobby.get_int("topt"))) e.add_child(Node.string("lid", lobby.get_str("lid"))) e.add_child(Node.string("sn", lobby.get_str("sn"))) e.add_child(Node.u8("pref", lobby.get_int("pref"))) e.add_child(Node.s8("stg", lobby.get_int("stg"))) e.add_child(Node.s8("pside", lobby.get_int("pside"))) e.add_child(Node.s16("eatime", lobby.get_int("eatime"))) e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4))) e.add_child(Node.u16("gp", lobby.get_int("gp"))) e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4))) limit = limit - 1 return root def handle_lobby_delete_request(self, request: Node) -> Node: eid = request.child_value("eid") self.data.local.lobby.destroy_lobby(eid) return Node.void("lobby") def handle_player_start_request(self, request: Node) -> Node: # Add a dummy entry into the lobby setup so we can clean up on end play refid = request.child_value("rid") userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: self.data.local.lobby.put_play_session_info( self.game, self.version, userid, {} ) root = Node.void("player") root.add_child(Node.bool("is_suc", True)) unlock_music = Node.void("unlock_music") root.add_child(unlock_music) unlock_item = Node.void("unlock_item") root.add_child(unlock_item) item_lock_ctrl = Node.void("item_lock_ctrl") root.add_child(item_lock_ctrl) lincle_link_4 = Node.void("lincle_link_4") root.add_child(lincle_link_4) lincle_link_4.add_child(Node.u32("qpro", 0)) lincle_link_4.add_child(Node.u32("glass", 0)) lincle_link_4.add_child(Node.u32("treasure", 0)) lincle_link_4.add_child(Node.bool("for_iidx_0_0", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_1", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_2", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_3", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_4", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_5", False)) lincle_link_4.add_child(Node.bool("for_iidx_0_6", False)) lincle_link_4.add_child(Node.bool("for_iidx_0", False)) lincle_link_4.add_child(Node.bool("for_iidx_1", False)) lincle_link_4.add_child(Node.bool("for_iidx_2", False)) lincle_link_4.add_child(Node.bool("for_iidx_3", False)) lincle_link_4.add_child(Node.bool("for_iidx_4", False)) lincle_link_4.add_child(Node.bool("for_rb_0_0", False)) lincle_link_4.add_child(Node.bool("for_rb_0_1", False)) lincle_link_4.add_child(Node.bool("for_rb_0_2", False)) lincle_link_4.add_child(Node.bool("for_rb_0_3", False)) lincle_link_4.add_child(Node.bool("for_rb_0_4", False)) lincle_link_4.add_child(Node.bool("for_rb_0_5", False)) lincle_link_4.add_child(Node.bool("for_rb_0_6", False)) lincle_link_4.add_child(Node.bool("for_rb_0", False)) lincle_link_4.add_child(Node.bool("for_rb_1", False)) lincle_link_4.add_child(Node.bool("for_rb_2", False)) lincle_link_4.add_child(Node.bool("for_rb_3", False)) lincle_link_4.add_child(Node.bool("for_rb_4", False)) lincle_link_4.add_child(Node.bool("qproflg", False)) lincle_link_4.add_child(Node.bool("glassflg", False)) lincle_link_4.add_child(Node.bool("complete", False)) # Add event info self.__add_event_info(root) return root def handle_player_delete_request(self, request: Node) -> Node: return Node.void("player") def handle_player_end_request(self, request: Node) -> Node: # Destroy play session based on info from the request refid = request.child_value("rid") userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: # Kill any lingering lobbies by this user lobby = self.data.local.lobby.get_lobby( self.game, self.version, userid, ) if lobby is not None: self.data.local.lobby.destroy_lobby(lobby.get_int("id")) self.data.local.lobby.destroy_play_session_info( self.game, self.version, userid ) return Node.void("player") def handle_player_read_request(self, request: Node) -> Node: refid = request.child_value("rid") profile = self.get_profile_by_refid(refid) if profile: return profile return Node.void("player") def handle_player_write_request(self, request: Node) -> Node: refid = request.child_value("rid") profile = self.put_profile_by_refid(refid, request) root = Node.void("player") if profile is None: root.add_child(Node.s32("uid", 0)) else: root.add_child(Node.s32("uid", profile.extid)) root.add_child(Node.s32("time", Time.now())) return root def handle_player_succeed_request(self, request: Node) -> Node: refid = request.child_value("rid") userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: previous_version = self.previous_version() profile = previous_version.get_profile(userid) achievements = self.data.local.user.get_achievements( previous_version.game, previous_version.version, userid ) scores = self.data.remote.music.get_scores( previous_version.game, previous_version.version, userid ) else: profile = None root = Node.void("player") if profile is None: root.add_child(Node.string("name", "")) root.add_child(Node.s32("lv", -1)) root.add_child(Node.s32("exp", -1)) root.add_child(Node.s32("grade", -1)) root.add_child(Node.s32("ap", -1)) root.add_child(Node.void("released")) root.add_child(Node.void("mrecord")) else: root.add_child(Node.string("name", profile.get_str("name"))) root.add_child(Node.s32("lv", profile.get_int("lvl"))) root.add_child(Node.s32("exp", profile.get_int("exp"))) root.add_child(Node.s32("grade", profile.get_int("mg"))) # This is a guess root.add_child(Node.s32("ap", profile.get_int("ap"))) released = Node.void("released") root.add_child(released) for item in achievements: if item.type != "item_0": continue released.add_child(Node.s16("i", item.id)) mrecord = Node.void("mrecord") root.add_child(mrecord) for score in scores: mrec = Node.void("mrec") mrecord.add_child(mrec) mrec.add_child(Node.s32("mid", score.id)) mrec.add_child(Node.s32("ctype", score.chart)) mrec.add_child( Node.s32("win", score.data.get_dict("stats").get_int("win")) ) mrec.add_child( Node.s32("lose", score.data.get_dict("stats").get_int("win")) ) mrec.add_child( Node.s32("draw", score.data.get_dict("stats").get_int("win")) ) mrec.add_child(Node.s32("score", score.points)) mrec.add_child(Node.s32("combo", score.data.get_int("combo"))) mrec.add_child(Node.s32("miss", score.data.get_int("miss_count"))) mrec.add_child( Node.s32( "grade", self.__db_to_game_clear_type( score.data.get_int("clear_type"), score.data.get_int("combo_type"), ), ) ) mrec.add_child(Node.s32("ap", score.data.get_int("achievement_rate"))) return root def format_profile(self, userid: UserID, profile: Profile) -> Node: statistics = self.get_play_statistics(userid) game_config = self.get_game_config() achievements = self.data.local.user.get_achievements( self.game, self.version, userid ) scores = self.data.remote.music.get_scores(self.game, self.version, userid) links = self.data.local.user.get_links(self.game, self.version, userid) root = Node.void("player") pdata = Node.void("pdata") root.add_child(pdata) base = Node.void("base") pdata.add_child(base) base.add_child(Node.s32("uid", profile.extid)) base.add_child(Node.string("name", profile.get_str("name"))) base.add_child(Node.s16("icon_id", profile.get_int("icon"))) base.add_child(Node.s16("lv", profile.get_int("lvl"))) base.add_child(Node.s32("exp", profile.get_int("exp"))) base.add_child(Node.s16("mg", profile.get_int("mg"))) base.add_child(Node.s16("ap", profile.get_int("ap"))) base.add_child(Node.s32("pc", profile.get_int("pc"))) base.add_child(Node.s32("uattr", profile.get_int("uattr"))) con = Node.void("con") pdata.add_child(con) con.add_child(Node.s32("day", statistics.today_plays)) con.add_child(Node.s32("cnt", statistics.total_plays)) con.add_child(Node.s32("total_cnt", statistics.total_plays)) con.add_child(Node.s32("last", statistics.last_play_timestamp)) con.add_child(Node.s32("now", Time.now())) team = Node.void("team") pdata.add_child(team) team.add_child(Node.s32("id", profile.get_int("team_id", -1))) team.add_child(Node.string("name", profile.get_str("team_name", ""))) custom = Node.void("custom") customdict = profile.get_dict("custom") pdata.add_child(custom) custom.add_child(Node.u8("s_gls", customdict.get_int("s_gls"))) custom.add_child(Node.u8("bgm_m", customdict.get_int("bgm_m"))) custom.add_child(Node.u8("st_f", customdict.get_int("st_f"))) custom.add_child(Node.u8("st_bg", customdict.get_int("st_bg"))) custom.add_child(Node.u8("st_bg_b", customdict.get_int("st_bg_b"))) custom.add_child(Node.u8("eff_e", customdict.get_int("eff_e"))) custom.add_child(Node.u8("se_s", customdict.get_int("se_s"))) custom.add_child(Node.u8("se_s_v", customdict.get_int("se_s_v"))) custom.add_child(Node.s16("last_music_id", customdict.get_int("last_music_id"))) custom.add_child( Node.u8("last_note_grade", customdict.get_int("last_note_grade")) ) custom.add_child(Node.u8("sort_type", customdict.get_int("sort_type"))) custom.add_child( Node.u8("narrowdown_type", customdict.get_int("narrowdown_type")) ) custom.add_child( Node.bool("is_begginer", customdict.get_bool("is_begginer")) ) # Yes, this is spelled right custom.add_child(Node.bool("is_tut", customdict.get_bool("is_tut"))) custom.add_child( Node.s16_array( "symbol_chat_0", customdict.get_int_array("symbol_chat_0", 6) ) ) custom.add_child( Node.s16_array( "symbol_chat_1", customdict.get_int_array("symbol_chat_1", 6) ) ) custom.add_child(Node.u8("gauge_style", customdict.get_int("gauge_style"))) custom.add_child(Node.u8("obj_shade", customdict.get_int("obj_shade"))) custom.add_child(Node.u8("obj_size", customdict.get_int("obj_size"))) custom.add_child( Node.s16_array("byword", customdict.get_int_array("byword", 2)) ) custom.add_child( Node.bool_array( "is_auto_byword", customdict.get_bool_array("is_auto_byword", 2) ) ) custom.add_child(Node.bool("is_tweet", customdict.get_bool("is_tweet"))) custom.add_child( Node.bool("is_link_twitter", customdict.get_bool("is_link_twitter")) ) custom.add_child(Node.s16("mrec_type", customdict.get_int("mrec_type"))) custom.add_child( Node.s16("card_disp_type", customdict.get_int("card_disp_type")) ) custom.add_child(Node.s16("tab_sel", customdict.get_int("tab_sel"))) custom.add_child( Node.s32_array("hidden_param", customdict.get_int_array("hidden_param", 20)) ) released = Node.void("released") pdata.add_child(released) for item in achievements: if item.type[:5] != "item_": continue itemtype = int(item.type[5:]) if game_config.get_bool("force_unlock_songs") and itemtype == 0: # Don't echo unlocks when we're force unlocking, we'll do it later continue info = Node.void("info") released.add_child(info) info.add_child(Node.u8("type", itemtype)) info.add_child(Node.u16("id", item.id)) info.add_child(Node.u16("param", item.data.get_int("param"))) if game_config.get_bool("force_unlock_songs"): ids: Dict[int, int] = {} songs = self.data.local.music.get_all_songs(self.game, self.version) for song in songs: if song.id not in ids: ids[song.id] = 0 if song.data.get_int("difficulty") > 0: ids[song.id] = ids[song.id] | (1 << song.chart) for songid in ids: if ids[songid] == 0: continue info = Node.void("info") released.add_child(info) info.add_child(Node.u8("type", 0)) info.add_child(Node.u16("id", songid)) info.add_child(Node.u16("param", ids[songid])) # Scores record = Node.void("record") pdata.add_child(record) for score in scores: rec = Node.void("rec") record.add_child(rec) rec.add_child(Node.u16("mid", score.id)) rec.add_child(Node.u8("ng", score.chart)) rec.add_child( Node.s32("point", score.data.get_dict("stats").get_int("earned_points")) ) rec.add_child(Node.s32("played_time", score.timestamp)) mrec_0 = Node.void("mrec_0") rec.add_child(mrec_0) mrec_0.add_child( Node.s32("win", score.data.get_dict("stats").get_int("win")) ) mrec_0.add_child( Node.s32("lose", score.data.get_dict("stats").get_int("lose")) ) mrec_0.add_child( Node.s32("draw", score.data.get_dict("stats").get_int("draw")) ) mrec_0.add_child( Node.u8( "ct", self.__db_to_game_clear_type( score.data.get_int("clear_type"), score.data.get_int("combo_type"), ), ) ) mrec_0.add_child( Node.s16("ar", int(score.data.get_int("achievement_rate") / 10)) ) mrec_0.add_child(Node.s32("bs", score.points)) mrec_0.add_child(Node.s16("mc", score.data.get_int("combo"))) mrec_0.add_child(Node.s16("bmc", score.data.get_int("miss_count"))) mrec_1 = Node.void("mrec_1") rec.add_child(mrec_1) mrec_1.add_child(Node.s32("win", 0)) mrec_1.add_child(Node.s32("lose", 0)) mrec_1.add_child(Node.s32("draw", 0)) mrec_1.add_child(Node.u8("ct", 0)) mrec_1.add_child(Node.s16("ar", 0)) mrec_1.add_child(Node.s32("bs", 0)) mrec_1.add_child(Node.s16("mc", 0)) mrec_1.add_child(Node.s16("bmc", -1)) # Comment (seems unused?) pdata.add_child(Node.string("cmnt", "")) # Rivals rival = Node.void("rival") pdata.add_child(rival) slotid = 0 for link in links: if link.type != "rival": continue rprofile = self.get_profile(link.other_userid) if rprofile is None: continue r = Node.void("r") rival.add_child(r) r.add_child(Node.u8("slot_id", slotid)) r.add_child(Node.string("name", rprofile.get_str("name"))) r.add_child(Node.s32("id", rprofile.extid)) r.add_child(Node.bool("friend", True)) r.add_child(Node.bool("locked", False)) r.add_child(Node.s32("rc", 0)) slotid = slotid + 1 # Glass points glass = Node.void("glass") pdata.add_child(glass) for item in achievements: if item.type != "glass": continue g = Node.void("g") glass.add_child(g) g.add_child(Node.s32("id", item.id)) g.add_child(Node.s32("exp", item.data.get_int("exp"))) # Favorite music fav_music_slot = Node.void("fav_music_slot") pdata.add_child(fav_music_slot) for item in achievements: if item.type != "music": continue slot = Node.void("slot") fav_music_slot.add_child(slot) slot.add_child(Node.u8("slot_id", item.id)) slot.add_child(Node.s16("music_id", item.data.get_int("music_id"))) narrow_down = Node.void("narrow_down") pdata.add_child(narrow_down) narrow_down.add_child( Node.s32_array( "adv_param", [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], ) ) return root def unformat_profile( self, userid: UserID, request: Node, oldprofile: Profile ) -> Profile: game_config = self.get_game_config() newprofile = oldprofile.clone() newprofile.replace_int("lid", ID.parse_machine_id(request.child_value("lid"))) newprofile.replace_str("name", request.child_value("pdata/base/name")) newprofile.replace_int("icon", request.child_value("pdata/base/icon_id")) newprofile.replace_int("lvl", request.child_value("pdata/base/lv")) newprofile.replace_int("exp", request.child_value("pdata/base/exp")) newprofile.replace_int("mg", request.child_value("pdata/base/mg")) newprofile.replace_int("ap", request.child_value("pdata/base/ap")) newprofile.replace_int("pc", request.child_value("pdata/base/pc")) newprofile.replace_int("uattr", request.child_value("pdata/base/uattr")) customdict = newprofile.get_dict("custom") custom = request.child("pdata/custom") if custom: customdict.replace_int("s_gls", custom.child_value("s_gls")) customdict.replace_int("bgm_m", custom.child_value("bgm_m")) customdict.replace_int("st_f", custom.child_value("st_f")) customdict.replace_int("st_bg", custom.child_value("st_bg")) customdict.replace_int("st_bg_b", custom.child_value("st_bg_b")) customdict.replace_int("eff_e", custom.child_value("eff_e")) customdict.replace_int("se_s", custom.child_value("se_s")) customdict.replace_int("se_s_v", custom.child_value("se_s_v")) customdict.replace_int("last_music_id", custom.child_value("last_music_id")) customdict.replace_int( "last_note_grade", custom.child_value("last_note_grade") ) customdict.replace_int("sort_type", custom.child_value("sort_type")) customdict.replace_int( "narrowdown_type", custom.child_value("narrowdown_type") ) customdict.replace_bool( "is_begginer", custom.child_value("is_begginer") ) # Yes, this is spelled right customdict.replace_bool("is_tut", custom.child_value("is_tut")) customdict.replace_int_array( "symbol_chat_0", 6, custom.child_value("symbol_chat_0") ) customdict.replace_int_array( "symbol_chat_1", 6, custom.child_value("symbol_chat_1") ) customdict.replace_int("gauge_style", custom.child_value("gauge_style")) customdict.replace_int("obj_shade", custom.child_value("obj_shade")) customdict.replace_int("obj_size", custom.child_value("obj_size")) customdict.replace_int_array("byword", 2, custom.child_value("byword")) customdict.replace_bool_array( "is_auto_byword", 2, custom.child_value("is_auto_byword") ) customdict.replace_bool("is_tweet", custom.child_value("is_tweet")) customdict.replace_bool( "is_link_twitter", custom.child_value("is_link_twitter") ) customdict.replace_int("mrec_type", custom.child_value("mrec_type")) customdict.replace_int( "card_disp_type", custom.child_value("card_disp_type") ) customdict.replace_int("tab_sel", custom.child_value("tab_sel")) customdict.replace_int_array( "hidden_param", 20, custom.child_value("hidden_param") ) newprofile.replace_dict("custom", customdict) # Music unlocks and other stuff released = request.child("pdata/released") if released: for child in released.children: if child.name != "info": continue item_id = child.child_value("id") item_type = child.child_value("type") param = child.child_value("param") if game_config.get_bool("force_unlock_songs") and item_type == 0: # Don't save unlocks when we're force unlocking continue self.data.local.user.put_achievement( self.game, self.version, userid, item_id, f"item_{item_type}", { "param": param, }, ) # Grab any new records set during this play session. Reflec Beat Limelight only sends # the top record back for songs that were played at least once during the session. # Note that it sends the top record, so if you play the song twice, it will return # only one record. Also, if you get a lower score than a previous try, it will return # the previous try. So, we must also look at the battle log for the actual play scores, # and combine the data if we can. savedrecords: Dict[int, Dict[int, Dict[str, int]]] = {} songplays = request.child("pdata/record") if songplays: for child in songplays.children: if child.name != "rec": continue songid = child.child_value("mid") chart = child.child_value("ng") # These don't get sent with the battle logs, so we try to construct # the values here. if songid not in savedrecords: savedrecords[songid] = {} savedrecords[songid][chart] = { "achievement_rate": child.child_value("mrec_0/ar") * 10, "points": child.child_value("mrec_0/bs"), "combo": child.child_value("mrec_0/mc"), "miss_count": child.child_value("mrec_0/bmc"), "win": child.child_value("mrec_0/win"), "lose": child.child_value("mrec_0/lose"), "draw": child.child_value("mrec_0/draw"), "earned_points": child.child_value("point"), } # Now, see the actual battles that were played. If we can, unify the data with a record. # We only do that when the record achievement rate and score matches the battle achievement # rate and score, so we know for a fact that that record was generated by this battle. battlelogs = request.child("pdata/blog") if battlelogs: for child in battlelogs.children: if child.name != "log": continue songid = child.child_value("mid") chart = child.child_value("ng") clear_type = child.child_value("myself/ct") achievement_rate = child.child_value("myself/ar") * 10 points = child.child_value("myself/s") clear_type, combo_type = self.__game_to_db_clear_type(clear_type) combo = None miss_count = -1 stats = None if songid in savedrecords: if chart in savedrecords[songid]: data = savedrecords[songid][chart] if ( data["achievement_rate"] == achievement_rate and data["points"] == points ): # This is the same record! Use the stats from it to update our # internal representation. combo = data["combo"] miss_count = data["miss_count"] stats = { "win": data["win"], "lose": data["lose"], "draw": data["draw"], "earned_points": data["earned_points"], } self.update_score( userid, songid, chart, points, achievement_rate, clear_type, combo_type, miss_count, combo=combo, stats=stats, ) # Keep track of glass points so unlocks work glass = request.child("pdata/glass") if glass: for child in glass.children: if child.name != "g": continue gid = child.child_value("id") exp = child.child_value("exp") self.data.local.user.put_achievement( self.game, self.version, userid, gid, "glass", { "exp": exp, }, ) # Keep track of favorite music selections fav_music_slot = request.child("pdata/fav_music_slot") if fav_music_slot: for child in fav_music_slot.children: if child.name != "slot": continue slot_id = child.child_value("slot_id") music_id = child.child_value("music_id") if music_id == -1: # Delete this favorite self.data.local.user.destroy_achievement( self.game, self.version, userid, slot_id, "music", ) else: # Add/update this favorite self.data.local.user.put_achievement( self.game, self.version, userid, slot_id, "music", { "music_id": music_id, }, ) # Keep track of play statistics self.update_play_statistics(userid) return newprofile