from typing import Optional, Dict, Any, List, Tuple from typing_extensions import Final from bemani.backend.reflec.base import ReflecBeatBase from bemani.backend.reflec.colette import ReflecBeatColette from bemani.common import Profile, ValidatedDict, VersionConstants, ID, Time from bemani.data import Achievement, Attempt, Score, UserID from bemani.protocol import Node class ReflecBeatGroovin(ReflecBeatBase): name: str = "REFLEC BEAT groovin'!!" version: int = VersionConstants.REFLEC_BEAT_GROOVIN # Clear types according to the game GAME_CLEAR_TYPE_NO_PLAY: Final[int] = 0 GAME_CLEAR_TYPE_EARLY_FAILED: Final[int] = 1 GAME_CLEAR_TYPE_FAILED: Final[int] = 2 GAME_CLEAR_TYPE_CLEARED: Final[int] = 9 GAME_CLEAR_TYPE_HARD_CLEARED: Final[int] = 10 GAME_CLEAR_TYPE_S_HARD_CLEARED: Final[int] = 11 # Combo types according to the game (actually a bitmask, where bit 0 is # full combo status, and bit 2 is just reflec status). But we don't support # saving just reflec without full combo, so we downgrade it. GAME_COMBO_TYPE_NONE: Final[int] = 0 GAME_COMBO_TYPE_ALL_JUST: Final[int] = 2 GAME_COMBO_TYPE_FULL_COMBO: Final[int] = 1 GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST: Final[int] = 3 def previous_version(self) -> Optional[ReflecBeatBase]: return ReflecBeatColette(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_status: int) -> int: return { self.CLEAR_TYPE_NO_PLAY: self.GAME_CLEAR_TYPE_NO_PLAY, self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_FAILED, self.CLEAR_TYPE_CLEARED: self.GAME_CLEAR_TYPE_CLEARED, self.CLEAR_TYPE_HARD_CLEARED: self.GAME_CLEAR_TYPE_HARD_CLEARED, self.CLEAR_TYPE_S_HARD_CLEARED: self.GAME_CLEAR_TYPE_S_HARD_CLEARED, }[db_status] def __game_to_db_clear_type(self, status: int) -> int: return { self.GAME_CLEAR_TYPE_NO_PLAY: self.CLEAR_TYPE_NO_PLAY, self.GAME_CLEAR_TYPE_EARLY_FAILED: self.CLEAR_TYPE_FAILED, self.GAME_CLEAR_TYPE_FAILED: self.CLEAR_TYPE_FAILED, self.GAME_CLEAR_TYPE_CLEARED: self.CLEAR_TYPE_CLEARED, self.GAME_CLEAR_TYPE_HARD_CLEARED: self.CLEAR_TYPE_HARD_CLEARED, self.GAME_CLEAR_TYPE_S_HARD_CLEARED: self.CLEAR_TYPE_S_HARD_CLEARED, }[status] def __db_to_game_combo_type(self, db_combo: int) -> int: return { self.COMBO_TYPE_NONE: self.GAME_COMBO_TYPE_NONE, self.COMBO_TYPE_ALMOST_COMBO: self.GAME_COMBO_TYPE_NONE, self.COMBO_TYPE_FULL_COMBO: self.GAME_COMBO_TYPE_FULL_COMBO, self.COMBO_TYPE_FULL_COMBO_ALL_JUST: self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST, }[db_combo] def __game_to_db_combo_type(self, game_combo: int, miss_count: int) -> int: if game_combo in [ self.GAME_COMBO_TYPE_NONE, self.GAME_COMBO_TYPE_ALL_JUST, ]: if miss_count >= 0 and miss_count <= 2: return self.COMBO_TYPE_ALMOST_COMBO else: return self.COMBO_TYPE_NONE if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO: return self.COMBO_TYPE_FULL_COMBO if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST: return self.COMBO_TYPE_FULL_COMBO_ALL_JUST raise Exception(f'Invalid game_combo value {game_combo}') def handle_pcb_rb4error_request(self, request: Node) -> Node: return Node.void('pcb') def handle_pcb_rb4uptime_update_request(self, request: Node) -> Node: return Node.void('pcb') def handle_pcb_rb4boot_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') else: machine_name = '' close = False hour = 0 minute = 0 root = Node.void('pcb') sinfo = Node.void('sinfo') root.add_child(sinfo) sinfo.add_child(Node.string('nm', machine_name)) sinfo.add_child(Node.bool('cl_enbl', close)) sinfo.add_child(Node.u8('cl_h', hour)) sinfo.add_child(Node.u8('cl_m', minute)) sinfo.add_child(Node.bool('shop_flag', True)) return root def handle_lobby_rb4entry_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) info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid) if profile is None or info is None: return root 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'), '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'), 'ver': request.child_value('e/ver'), 'tension': request.child_value('e/tension'), } ) 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.s32('uattr', profile.get_int('uattr'))) e.add_child(Node.string('pn', profile.get_str('name'))) e.add_child(Node.s32('plyid', info.get_int('id'))) e.add_child(Node.s16('mg', profile.get_int('mg'))) e.add_child(Node.s32('mopt', lobby.get_int('mopt'))) 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))) e.add_child(Node.u8('ver', lobby.get_int('ver'))) e.add_child(Node.s8('tension', lobby.get_int('tension'))) return root def handle_lobby_rb4read_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 ver = request.child_value('var') 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 if ver != lobby.get_int('ver'): # Don't return lobby data for different versions continue profile = self.get_profile(user) info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid) if profile is None or info is None: # No profile info, don't return this lobby return root 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.s32('uattr', profile.get_int('uattr'))) e.add_child(Node.string('pn', profile.get_str('name'))) e.add_child(Node.s32('plyid', info.get_int('id'))) e.add_child(Node.s16('mg', profile.get_int('mg'))) e.add_child(Node.s32('mopt', lobby.get_int('mopt'))) 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))) e.add_child(Node.u8('ver', lobby.get_int('ver'))) e.add_child(Node.s8('tension', lobby.get_int('tension'))) limit = limit - 1 return root def handle_lobby_rb4delete_request(self, request: Node) -> Node: eid = request.child_value('eid') self.data.local.lobby.destroy_lobby(eid) return Node.void('lobby') def handle_shop_rb4setting_write_request(self, request: Node) -> Node: return Node.void('shop') def handle_shop_rb4info_write_request(self, request: Node) -> Node: self.update_machine_name(request.child_value('sinfo/nm')) self.update_machine_data({ 'close': request.child_value('sinfo/cl_enbl'), 'hour': request.child_value('sinfo/cl_h'), 'minute': request.child_value('sinfo/cl_m'), 'pref': request.child_value('sinfo/prf'), }) return Node.void('shop') def __add_event_info(self, root: Node) -> None: event_ctrl = Node.void('event_ctrl') root.add_child(event_ctrl) # Contains zero or more nodes like: # # any # any # any # any # any # any # item_lock_ctrl = Node.void('item_lock_ctrl') root.add_child(item_lock_ctrl) # Contains zero or more nodes like: # # any # any # 0-3 # def __add_shop_score(self, root: Node) -> None: shop_score = Node.void('shop_score') root.add_child(shop_score) today = Node.void('today') shop_score.add_child(today) yesterday = Node.void('yesterday') shop_score.add_child(yesterday) all_profiles = self.data.local.user.get_all_profiles(self.game, self.version) all_attempts = self.data.local.music.get_all_attempts(self.game, self.version, timelimit=(Time.beginning_of_today() - Time.SECONDS_IN_DAY)) machine = self.data.local.machine.get_machine(self.config.machine.pcbid) if machine.arcade is not None: lids = [ machine.id for machine in self.data.local.machine.get_all_machines(machine.arcade) ] else: lids = [machine.id] relevant_profiles = [ profile for profile in all_profiles if profile[1].get_int('lid', -1) in lids ] for (rootnode, timeoffset) in [ (today, 0), (yesterday, Time.SECONDS_IN_DAY), ]: # Grab all attempts made in the relevant day relevant_attempts = [ attempt for attempt in all_attempts if ( attempt[1].timestamp >= (Time.beginning_of_today() - timeoffset) and attempt[1].timestamp <= (Time.end_of_today() - timeoffset) ) ] # Calculate scores based on attempt scores_by_user: Dict[UserID, Dict[int, Dict[int, Attempt]]] = {} for (userid, attempt) in relevant_attempts: if userid not in scores_by_user: scores_by_user[userid] = {} if attempt.id not in scores_by_user[userid]: scores_by_user[userid][attempt.id] = {} if attempt.chart not in scores_by_user[userid][attempt.id]: # No high score for this yet, just use this attempt scores_by_user[userid][attempt.id][attempt.chart] = attempt else: # If this attempt is better than the stored one, replace it if scores_by_user[userid][attempt.id][attempt.chart].points < attempt.points: scores_by_user[userid][attempt.id][attempt.chart] = attempt # Calculate points earned by user in the day points_by_user: Dict[UserID, int] = {} for userid in scores_by_user: points_by_user[userid] = 0 for mid in scores_by_user[userid]: for chart in scores_by_user[userid][mid]: points_by_user[userid] = points_by_user[userid] + scores_by_user[userid][mid][chart].points # Output that day's earned points for (userid, profile) in relevant_profiles: data = Node.void('data') rootnode.add_child(data) data.add_child(Node.s16('day_id', int((Time.now() - timeoffset) / Time.SECONDS_IN_DAY))) data.add_child(Node.s32('user_id', profile.extid)) data.add_child(Node.s16('icon_id', profile.get_dict('config').get_int('icon_id'))) data.add_child(Node.s16('point', min(points_by_user.get(userid, 0), 32767))) data.add_child(Node.s32('update_time', Time.now())) data.add_child(Node.string('name', profile.get_str('name'))) rootnode.add_child(Node.s32('timestamp', Time.beginning_of_today() - timeoffset)) def handle_info_rb4common_request(self, request: Node) -> Node: root = Node.void('info') self.__add_event_info(root) self.__add_shop_score(root) return root def handle_info_rb4ranking_request(self, request: Node) -> Node: version = request.child_value('ver') root = Node.void('info') root.add_child(Node.s32('ver', version)) ranking = Node.void('ranking') root.add_child(ranking) def add_hitchart(name: str, start: int, end: int, hitchart: List[Tuple[int, int]]) -> None: base = Node.void(name) ranking.add_child(base) base.add_child(Node.s32('bt', start)) base.add_child(Node.s32('et', end)) new = Node.void('new') base.add_child(new) for (mid, plays) in hitchart: d = Node.void('d') new.add_child(d) d.add_child(Node.s16('mid', mid)) d.add_child(Node.s32('cnt', plays)) # Weekly hit chart add_hitchart( 'weekly', Time.now() - Time.SECONDS_IN_WEEK, Time.now(), self.data.local.music.get_hit_chart(self.game, self.version, 1024, 7), ) # Monthly hit chart add_hitchart( 'monthly', Time.now() - Time.SECONDS_IN_DAY * 30, Time.now(), self.data.local.music.get_hit_chart(self.game, self.version, 1024, 30), ) # All time hit chart add_hitchart( 'total', Time.now() - Time.SECONDS_IN_DAY * 365, Time.now(), self.data.local.music.get_hit_chart(self.game, self.version, 1024, 365), ) return root def handle_info_rb4shop_score_ranking_request(self, request: Node) -> Node: start_music_id = request.child_value('min') end_music_id = request.child_value('max') root = Node.void('info') shop_score = Node.void('shop_score') root.add_child(shop_score) shop_score.add_child(Node.s32('time', Time.now())) profiles: Dict[UserID, Profile] = {} for songid in range(start_music_id, end_music_id + 1): allscores = self.data.local.music.get_all_scores( self.game, self.version, songid=songid, ) for ng in [ self.CHART_TYPE_BASIC, self.CHART_TYPE_MEDIUM, self.CHART_TYPE_HARD, self.CHART_TYPE_SPECIAL, ]: scores = sorted( [score for score in allscores if score[1].chart == ng], key=lambda score: score[1].points, reverse=True, ) for i in range(len(scores)): userid, score = scores[i] if userid not in profiles: profiles[userid] = self.get_any_profile(userid) profile = profiles[userid] data = Node.void('data') shop_score.add_child(data) data.add_child(Node.s32('rank', i + 1)) data.add_child(Node.s16('music_id', songid)) data.add_child(Node.s8('note_grade', score.chart)) data.add_child(Node.s8('clear_type', self.__db_to_game_clear_type(score.data.get_int('clear_type')))) data.add_child(Node.s32('user_id', profile.extid)) data.add_child(Node.s16('icon_id', profile.get_dict('config').get_int('icon_id'))) data.add_child(Node.s32('score', score.points)) data.add_child(Node.s32('time', score.timestamp)) data.add_child(Node.string('name', profile.get_str('name'))) return root def handle_info_rb4pzlcmt_read_request(self, request: Node) -> Node: extid = request.child_value('uid') locid = ID.parse_machine_id(request.child_value('lid')) limit = request.child_value('limit') userid = self.data.remote.user.from_extid(self.game, self.version, extid) 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) favorites = [ comment for comment in comments if comment[0] == userid ] locationcomments = [ comment for comment in comments if comment[1].data.get_int('locid') == locid ] # Cap all comment blocks to the limit if limit >= 0: comments = comments[:limit] favorites = favorites[:limit] locationcomments = locationcomments[:limit] root = Node.void('info') comment = Node.void('comment') root.add_child(comment) comment.add_child(Node.s32('time', Time.now())) # Mapping of profiles to userIDs uid_mapping = { uid: prof for (uid, prof) in self.get_any_profiles([c[0] for c in comments]) } # Handle anonymous comments by returning a default profile uid_mapping[UserID(0)] = Profile( self.game, self.version, "", 0, {'name': 'PLAYER'}, ) def add_comments(name: str, selected: List[Tuple[UserID, Achievement]]) -> None: for (uid, ach) in selected: cmnt = Node.void(name) root.add_child(cmnt) cmnt.add_child(Node.s32('uid', uid_mapping[uid].extid)) cmnt.add_child(Node.string('name', uid_mapping[uid].get_str('name'))) cmnt.add_child(Node.s16('icon', ach.data.get_int('icon'))) cmnt.add_child(Node.s8('bln', ach.data.get_int('bln'))) cmnt.add_child(Node.string('lid', ID.format_machine_id(ach.data.get_int('locid')))) cmnt.add_child(Node.s8('pref', ach.data.get_int('prefecture'))) cmnt.add_child(Node.s32('time', ach.timestamp)) cmnt.add_child(Node.string('comment', ach.data.get_str('comment'))) cmnt.add_child(Node.bool('is_tweet', ach.data.get_bool('tweet'))) # Add all comments add_comments('c', comments) # Add personal comments (favorites) add_comments('cf', favorites) # Add location comments add_comments('cs', locationcomments) return root def handle_info_rb4pzlcmt_write_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) icon = request.child_value('icon') bln = request.child_value('bln') locid = ID.parse_machine_id(request.child_value('lid')) 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', { 'icon': icon, 'bln': bln, 'locid': locid, 'prefecture': prefecture, 'comment': comment, 'tweet': is_tweet, }, ) return Node.void('info') def handle_player_rb4start_request(self, request: Node) -> Node: root = Node.void('player') # Create a new 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: self.data.local.lobby.put_play_session_info( self.game, self.version, userid, { 'ga': request.child_value('ga'), 'gp': request.child_value('gp'), 'la': request.child_value('la'), 'pnid': request.child_value('pnid'), }, ) info = self.data.local.lobby.get_play_session_info( self.game, self.version, userid, ) if info is not None: play_id = info.get_int('id') else: play_id = 0 else: play_id = 0 # Session stuff, and resend global defaults root.add_child(Node.s32('plyid', play_id)) root.add_child(Node.u64('start_time', Time.now() * 1000)) self.__add_event_info(root) return root def handle_player_rb4end_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_rb4readepisode_request(self, request: Node) -> Node: extid = request.child_value('user_id') userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: achievements = self.data.local.user.get_achievements(self.game, self.version, userid) else: achievements = [] root = Node.void('player') pdata = Node.void('pdata') root.add_child(pdata) episode = Node.void('episode') pdata.add_child(episode) for achievement in achievements: if achievement.type != 'episode': continue info = Node.void('info') episode.add_child(info) info.add_child(Node.s32('user_id', extid)) info.add_child(Node.u8('type', achievement.id)) info.add_child(Node.u16('value0', achievement.data.get_int('value0'))) info.add_child(Node.u16('value1', achievement.data.get_int('value1'))) info.add_child(Node.string('text', achievement.data.get_str('text'))) info.add_child(Node.s32('time', achievement.data.get_int('time'))) return root def handle_player_rb4readscore_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 None: scores: List[Score] = [] else: scores = self.data.remote.music.get_scores(self.game, self.version, userid) root = Node.void('player') pdata = Node.void('pdata') root.add_child(pdata) record = Node.void('record') pdata.add_child(record) record_old = Node.void('record_old') pdata.add_child(record_old) for score in scores: rec = Node.void('rec') record.add_child(rec) rec.add_child(Node.s16('mid', score.id)) rec.add_child(Node.s8('ntgrd', score.chart)) rec.add_child(Node.s32('pc', score.plays)) rec.add_child(Node.s8('ct', self.__db_to_game_clear_type(score.data.get_int('clear_type')))) rec.add_child(Node.s16('ar', score.data.get_int('achievement_rate'))) rec.add_child(Node.s16('scr', score.points)) rec.add_child(Node.s16('ms', score.data.get_int('miss_count'))) rec.add_child(Node.s16( 'param', self.__db_to_game_combo_type(score.data.get_int('combo_type')) + score.data.get_int('param'), )) rec.add_child(Node.s32('bscrt', score.timestamp)) rec.add_child(Node.s32('bart', score.data.get_int('best_achievement_rate_time'))) rec.add_child(Node.s32('bctt', score.data.get_int('best_clear_type_time'))) rec.add_child(Node.s32('bmst', score.data.get_int('best_miss_count_time'))) rec.add_child(Node.s32('time', score.data.get_int('last_played_time'))) return root def handle_player_rb4selectscore_request(self, request: Node) -> Node: extid = request.child_value('uid') songid = request.child_value('music_id') chart = request.child_value('note_grade') userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is None: score = None profile = None else: score = self.data.remote.music.get_score(self.game, self.version, userid, songid, chart) profile = self.get_any_profile(userid) root = Node.void('player') if score is not None and profile is not None: player_select_score = Node.void('player_select_score') root.add_child(player_select_score) player_select_score.add_child(Node.s32('user_id', extid)) player_select_score.add_child(Node.string('name', profile.get_str('name'))) player_select_score.add_child(Node.s32('m_score', score.points)) player_select_score.add_child(Node.s32('m_scoreTime', score.timestamp)) player_select_score.add_child(Node.s16('m_iconID', profile.get_dict('config').get_int('icon_id'))) return root def handle_player_rbsvLinkageSave_request(self, request: Node) -> Node: # I think this is ReflecBeat/SoundVoltex linkage save, and I # am somewhat convinced that PK/BN is for packets/blocks, but # whatever. root = Node.void('player') root.add_child(Node.s32('before_pk_value', -1)) root.add_child(Node.s32('after_pk_value', -1)) root.add_child(Node.s32('before_bn_value', -1)) root.add_child(Node.s32('after_bn_value', -1)) return root def handle_player_rb4total_bestallrank_read_request(self, request: Node) -> Node: # This gives us a 6-integer array mapping to user scores for the following: # [total score, basic chart score, medium chart score, hard chart score, # special chart score, new songs score]. # It appears to return several 6-array values similar to the following: # # 1 2 3 4 5 6 # 101 102 103 104 105 106 # 7 8 9 10 11 12 # # The first 'rank' is the displayed value for the six categories. The # second and third values appear unused in-game. I think this is supposed # to give a player the idea of what ranking they are on the server for # various scores. current_scores = request.child_value('score') # First, grab all scores on the network for this version, and all songs # available so we know which songs are new to this version of the game. all_scores = self.data.remote.music.get_all_scores(self.game, self.version) all_songs = self.data.local.music.get_all_songs(self.game, self.version) # Figure out what song IDs are new new_songs = {song.id for song in all_songs if song.data.get_int('folder', 0) == self.version} # Now grab all participating users that had scores all_users = {userid for (userid, score) in all_scores} # Now, group the scores by user, so we can add up the totals, only including # scores where the user at least cleared the song. scores_by_user = { userid: [ score for (uid, score) in all_scores if uid == userid and score.data.get_int('clear_type') >= self.CLEAR_TYPE_CLEARED] for userid in all_users } # Now, sum up the scores into the six categories that the game expects. total_scores = sorted( [ sum([score.points for score in scores]) for userid, scores in scores_by_user.items() ], reverse=True, ) basic_scores = sorted( [ sum([score.points for score in scores if score.chart == self.CHART_TYPE_BASIC]) for userid, scores in scores_by_user.items() ], reverse=True, ) medium_scores = sorted( [ sum([score.points for score in scores if score.chart == self.CHART_TYPE_MEDIUM]) for userid, scores in scores_by_user.items() ], reverse=True, ) hard_scores = sorted( [ sum([score.points for score in scores if score.chart == self.CHART_TYPE_HARD]) for userid, scores in scores_by_user.items() ], reverse=True, ) special_scores = sorted( [ sum([score.points for score in scores if score.chart == self.CHART_TYPE_SPECIAL]) for userid, scores in scores_by_user.items() ], reverse=True, ) new_scores = sorted( [ sum([score.points for score in scores if score.id in new_songs]) for userid, scores in scores_by_user.items() ], reverse=True, ) # Guarantee that a zero score is at the end of every list, so that it makes # the algorithm for figuring out place have no edge case. total_scores.append(0) basic_scores.append(0) medium_scores.append(0) hard_scores.append(0) special_scores.append(0) new_scores.append(0) # Now, figure out where we fit based on the scores sent from the game. user_place = [1, 1, 1, 1, 1, 1] which_score = [ total_scores, basic_scores, medium_scores, hard_scores, special_scores, new_scores, ] for i in range(len(user_place)): current_score = current_scores[i] scores = which_score[i] for score in scores: if current_score >= score: break user_place[i] = user_place[i] + 1 root = Node.void('player') scorenode = Node.void('score') root.add_child(scorenode) scorenode.add_child(Node.s32_array('rank', user_place)) scorenode.add_child(Node.s32_array('score', [0] * 6)) scorenode.add_child(Node.s32_array('allrank', [len(total_scores)] * 6)) return root def handle_player_rb4delete_request(self, request: Node) -> Node: return Node.void('player') def handle_player_rb4read_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_rb4succeed_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: # Return empty succeed to say this is new root.add_child(Node.string('name', '')) root.add_child(Node.s16('lv', -1)) root.add_child(Node.s32('exp', -1)) root.add_child(Node.s32('grd', -1)) root.add_child(Node.s32('ap', -1)) root.add_child(Node.s32('money', -1)) root.add_child(Node.void('released')) root.add_child(Node.void('mrecord')) else: # Return previous profile formatted to say this is data succession root.add_child(Node.string('name', profile.get_str('name'))) root.add_child(Node.s16('lv', profile.get_int('lvl'))) root.add_child(Node.s32('exp', profile.get_int('exp'))) root.add_child(Node.s32('grd', profile.get_int('mg'))) # This is a guess root.add_child(Node.s32('ap', profile.get_int('ap'))) root.add_child(Node.s32('money', 0)) 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.s16('mid', score.id)) mrec.add_child(Node.s8('ntgrd', score.chart)) mrec.add_child(Node.s32('pc', score.plays)) mrec.add_child(Node.s8('ct', self.__db_to_game_clear_type(score.data.get_int('clear_type')))) mrec.add_child(Node.s16('ar', score.data.get_int('achievement_rate'))) mrec.add_child(Node.s16('scr', score.points)) mrec.add_child(Node.s16('ms', score.data.get_int('miss_count'))) mrec.add_child(Node.u16('ver', 0)) mrec.add_child(Node.s32('bst', score.timestamp)) mrec.add_child(Node.s32('bat', score.data.get_int('best_achievement_rate_time'))) mrec.add_child(Node.s32('bct', score.data.get_int('best_clear_type_time'))) mrec.add_child(Node.s32('bmt', score.data.get_int('best_miss_count_time'))) return root def handle_player_rb4write_request(self, request: Node) -> Node: refid = request.child_value('pdata/account/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)) 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) links = self.data.local.user.get_links(self.game, self.version, userid) root = Node.void('player') pdata = Node.void('pdata') root.add_child(pdata) # Account info account = Node.void('account') pdata.add_child(account) account.add_child(Node.s32('usrid', profile.extid)) account.add_child(Node.s32('tpc', statistics.total_plays)) account.add_child(Node.s32('dpc', statistics.today_plays)) account.add_child(Node.s32('crd', 1)) account.add_child(Node.s32('brd', 1)) account.add_child(Node.s32('tdc', statistics.total_days)) account.add_child(Node.s32('intrvld', 0)) account.add_child(Node.s16('ver', 1)) account.add_child(Node.u64('pst', 0)) account.add_child(Node.u64('st', Time.now() * 1000)) account.add_child(Node.u8('debutVer', 2)) # Base profile info base = Node.void('base') pdata.add_child(base) base.add_child(Node.string('name', profile.get_str('name'))) base.add_child(Node.s32('exp', profile.get_int('exp'))) base.add_child(Node.s32('lv', profile.get_int('lvl'))) base.add_child(Node.s32('mg', profile.get_int('mg'))) base.add_child(Node.s32('ap', profile.get_int('ap'))) base.add_child(Node.string('cmnt', '')) base.add_child(Node.s32('uattr', profile.get_int('uattr'))) base.add_child(Node.s32('money', profile.get_int('money'))) base.add_child(Node.s32('tbs', -1)) base.add_child(Node.s32('tbs_r', -1)) base.add_child(Node.s32('tbgs', -1)) base.add_child(Node.s32('tbgs_r', -1)) base.add_child(Node.s32('tbms', -1)) base.add_child(Node.s32('tbms_r', -1)) base.add_child(Node.s32('qe_win', -1)) base.add_child(Node.s32('qe_legend', -1)) base.add_child(Node.s32('qe2_win', -1)) base.add_child(Node.s32('qe2_legend', -1)) base.add_child(Node.s32('qe3_win', -1)) base.add_child(Node.s32('qe3_legend', -1)) base.add_child(Node.s16_array('mlog', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1])) base.add_child(Node.s32('class', profile.get_int('class'))) base.add_child(Node.s32('class_ar', profile.get_int('class_ar'))) base.add_child(Node.s32('getrfl', -1)) base.add_child(Node.s32('upper_pt', profile.get_int('upper_pt'))) # 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 lobbyinfo = self.data.local.lobby.get_play_session_info(self.game, self.version, link.other_userid) if lobbyinfo is None: lobbyinfo = ValidatedDict() r = Node.void('r') rival.add_child(r) r.add_child(Node.s32('slot_id', slotid)) r.add_child(Node.s32('id', rprofile.extid)) r.add_child(Node.string('name', rprofile.get_str('name'))) r.add_child(Node.s32('icon', profile.get_dict('config').get_int('icon_id'))) r.add_child(Node.s32('m_level', profile.get_int('mg'))) r.add_child(Node.s32('class', profile.get_int('class'))) r.add_child(Node.s32('class_ar', profile.get_int('class_ar'))) r.add_child(Node.bool('friend', True)) r.add_child(Node.bool('target', False)) r.add_child(Node.u32('time', lobbyinfo.get_int('time'))) r.add_child(Node.u8_array('ga', lobbyinfo.get_int_array('ga', 4))) r.add_child(Node.u16('gp', lobbyinfo.get_int('gp'))) r.add_child(Node.u8_array('ipn', lobbyinfo.get_int_array('la', 4))) r.add_child(Node.u8_array('pnid', lobbyinfo.get_int_array('pnid', 16))) slotid = slotid + 1 # Stamps stamp = Node.void('stamp') stampdict = profile.get_dict('stamp') pdata.add_child(stamp) stamp.add_child(Node.s32_array('stmpcnt', stampdict.get_int_array('stmpcnt', 10))) stamp.add_child(Node.s64('area', stampdict.get_int('area'))) stamp.add_child(Node.s64('prfvst', stampdict.get_int('prfvst'))) # Configuration configdict = profile.get_dict('config') config = Node.void('config') pdata.add_child(config) config.add_child(Node.u8('msel_bgm', configdict.get_int('msel_bgm'))) config.add_child(Node.u8('narrowdown_type', configdict.get_int('narrowdown_type'))) config.add_child(Node.s16('icon_id', configdict.get_int('icon_id'))) config.add_child(Node.s16('byword_0', configdict.get_int('byword_0'))) config.add_child(Node.s16('byword_1', configdict.get_int('byword_1'))) config.add_child(Node.bool('is_auto_byword_0', configdict.get_bool('is_auto_byword_0'))) config.add_child(Node.bool('is_auto_byword_1', configdict.get_bool('is_auto_byword_1'))) config.add_child(Node.u8('mrec_type', configdict.get_int('mrec_type'))) config.add_child(Node.u8('tab_sel', configdict.get_int('tab_sel'))) config.add_child(Node.u8('card_disp', configdict.get_int('card_disp'))) config.add_child(Node.u8('score_tab_disp', configdict.get_int('score_tab_disp'))) config.add_child(Node.s16('last_music_id', configdict.get_int('last_music_id', -1))) config.add_child(Node.u8('last_note_grade', configdict.get_int('last_note_grade'))) config.add_child(Node.u8('sort_type', configdict.get_int('sort_type'))) config.add_child(Node.u8('rival_panel_type', configdict.get_int('rival_panel_type'))) config.add_child(Node.u64('random_entry_work', configdict.get_int('random_entry_work'))) config.add_child(Node.u64('custom_folder_work', configdict.get_int('custom_folder_work'))) config.add_child(Node.u8('folder_type', configdict.get_int('folder_type'))) config.add_child(Node.u8('folder_lamp_type', configdict.get_int('folder_lamp_type'))) config.add_child(Node.bool('is_tweet', configdict.get_bool('is_tweet'))) config.add_child(Node.bool('is_link_twitter', configdict.get_bool('is_link_twitter'))) # Customizations customdict = profile.get_dict('custom') custom = Node.void('custom') pdata.add_child(custom) custom.add_child(Node.u8('st_shot', customdict.get_int('st_shot'))) custom.add_child(Node.u8('st_frame', customdict.get_int('st_frame'))) custom.add_child(Node.u8('st_expl', customdict.get_int('st_expl'))) custom.add_child(Node.u8('st_bg', customdict.get_int('st_bg'))) custom.add_child(Node.u8('st_shot_vol', customdict.get_int('st_shot_vol'))) custom.add_child(Node.u8('st_bg_bri', customdict.get_int('st_bg_bri'))) custom.add_child(Node.u8('st_obj_size', customdict.get_int('st_obj_size'))) custom.add_child(Node.u8('st_jr_gauge', customdict.get_int('st_jr_gauge'))) custom.add_child(Node.u8('st_clr_gauge', customdict.get_int('st_clr_gauge'))) custom.add_child(Node.u8('st_jdg_disp', customdict.get_int('st_jdg_disp'))) custom.add_child(Node.u8('st_tm_disp', customdict.get_int('st_tm_disp'))) custom.add_child(Node.u8('st_rnd', customdict.get_int('st_rnd'))) custom.add_child(Node.u8('st_hazard', customdict.get_int('st_hazard'))) custom.add_child(Node.u8('st_clr_cond', customdict.get_int('st_clr_cond'))) custom.add_child(Node.s16_array('schat_0', customdict.get_int_array('schat_0', 10))) custom.add_child(Node.s16_array('schat_1', customdict.get_int_array('schat_1', 10))) custom.add_child(Node.u8('cheer_voice', customdict.get_int('cheer_voice'))) custom.add_child(Node.u8('same_time_note_disp', customdict.get_int('same_time_note_disp'))) # Unlocks 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])) # Announcements announce = Node.void('announce') pdata.add_child(announce) for announcement in achievements: if announcement.type[:13] != 'announcement_': continue announcementtype = int(announcement.type[13:]) info = Node.void('info') announce.add_child(info) info.add_child(Node.u8('type', announcementtype)) info.add_child(Node.u16('id', announcement.id)) info.add_child(Node.u16('param', announcement.data.get_int('param'))) info.add_child(Node.bool('bneedannounce', announcement.data.get_bool('need'))) # Dojo ranking return dojo = Node.void('dojo') pdata.add_child(dojo) for entry in achievements: if entry.type != 'dojo': continue rec = Node.void('rec') dojo.add_child(rec) rec.add_child(Node.s32('class', entry.id)) rec.add_child(Node.s32('clear_type', entry.data.get_int('clear_type'))) rec.add_child(Node.s32('total_ar', entry.data.get_int('ar'))) rec.add_child(Node.s32('total_score', entry.data.get_int('score'))) rec.add_child(Node.s32('play_count', entry.data.get_int('plays'))) rec.add_child(Node.s32('last_play_time', entry.data.get_int('play_timestamp'))) rec.add_child(Node.s32('record_update_time', entry.data.get_int('record_timestamp'))) rec.add_child(Node.s32('rank', 0)) # Player Parameters player_param = Node.void('player_param') pdata.add_child(player_param) for param in achievements: if param.type[:13] != 'player_param_': continue itemtype = int(param.type[13:]) itemnode = Node.void('item') player_param.add_child(itemnode) itemnode.add_child(Node.s32('type', itemtype)) itemnode.add_child(Node.s32('bank', param.id)) itemnode.add_child(Node.s32_array('data', param.data.get_int_array('data', 256))) # Shop score for players self.__add_shop_score(pdata) # Quest data questdict = profile.get_dict('quest') quest = Node.void('quest') pdata.add_child(quest) quest.add_child(Node.s16('eye_color', questdict.get_int('eye_color'))) quest.add_child(Node.s16('body_color', questdict.get_int('body_color'))) quest.add_child(Node.s16('item', questdict.get_int('item'))) quest.add_child(Node.string('comment', '')) # Derby settings derby = Node.void('derby') pdata.add_child(derby) derby.add_child(Node.bool('is_open', False)) # Codebreaking stuff codebreaking = Node.void('codebreaking') pdata.add_child(codebreaking) codebreaking.add_child(Node.s32('cb_id', -1)) codebreaking.add_child(Node.s32('cb_sub_id', -1)) codebreaking.add_child(Node.s32('music_id', -1)) codebreaking.add_child(Node.string('question', '')) # Unknown IIDX link crap iidx_linkage = Node.void('iidx_linkage') pdata.add_child(iidx_linkage) iidx_linkage.add_child(Node.s32('linkage_id', -1)) iidx_linkage.add_child(Node.s32('phase', -1)) iidx_linkage.add_child(Node.s64('long_bit_0', -1)) iidx_linkage.add_child(Node.s64('long_bit_1', -1)) iidx_linkage.add_child(Node.s64('long_bit_2', -1)) iidx_linkage.add_child(Node.s64('long_bit_3', -1)) iidx_linkage.add_child(Node.s64('long_bit_4', -1)) iidx_linkage.add_child(Node.s64('long_bit_5', -1)) iidx_linkage.add_child(Node.s32('add_0', -1)) iidx_linkage.add_child(Node.s32('add_1', -1)) iidx_linkage.add_child(Node.s32('add_2', -1)) iidx_linkage.add_child(Node.s32('add_3', -1)) # Unknown event crap pue = Node.void('pue') pdata.add_child(pue) pue.add_child(Node.s32('event_id', -1)) pue.add_child(Node.s32('point', -1)) pue.add_child(Node.s32('value0', -1)) pue.add_child(Node.s32('value1', -1)) pue.add_child(Node.s32('value2', -1)) pue.add_child(Node.s32('value3', -1)) pue.add_child(Node.s32('value4', -1)) pue.add_child(Node.s32('start_time', -1)) pue.add_child(Node.s32('end_time', -1)) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: game_config = self.get_game_config() newprofile = oldprofile.clone() # Save base player profile info newprofile.replace_int('lid', ID.parse_machine_id(request.child_value('pdata/account/lid'))) newprofile.replace_str('name', request.child_value('pdata/base/name')) newprofile.replace_int('exp', request.child_value('pdata/base/exp')) newprofile.replace_int('lvl', request.child_value('pdata/base/lvl')) newprofile.replace_int('mg', request.child_value('pdata/base/mg')) newprofile.replace_int('ap', request.child_value('pdata/base/ap')) newprofile.replace_int('money', request.child_value('pdata/base/money')) newprofile.replace_int('class', request.child_value('pdata/base/class')) newprofile.replace_int('class_ar', request.child_value('pdata/base/class_ar')) newprofile.replace_int('upper_pt', request.child_value('pdata/base/upper_pt')) # Save stamps stampdict = newprofile.get_dict('stamp') stamp = request.child('pdata/stamp') if stamp: stampdict.replace_int_array('stmpcnt', 10, stamp.child_value('stmpcnt')) stampdict.replace_int('area', stamp.child_value('area')) stampdict.replace_int('prfvst', stamp.child_value('prfvst')) newprofile.replace_dict('stamp', stampdict) # Save quest stuff questdict = newprofile.get_dict('quest') quest = request.child('pdata/quest') if quest: questdict.replace_int('eye_color', quest.child_value('eye_color')) questdict.replace_int('body_color', quest.child_value('body_color')) questdict.replace_int('item', quest.child_value('item')) newprofile.replace_dict('quest', questdict) # Save player dojo dojo = request.child('pdata/dojo') if dojo: dojoid = dojo.child_value('class') clear_type = dojo.child_value('clear_type') ar = dojo.child_value('t_ar') score = dojo.child_value('t_score') # Figure out timestamp stuff data = self.data.local.user.get_achievement( self.game, self.version, userid, dojoid, 'dojo', ) or ValidatedDict() if ar >= data.get_int('ar'): # We set a new achievement rate, keep the new values record_time = Time.now() else: # We didn't, keep the old values for achievement rate, but # override score and clear_type only if they were better. record_time = data.get_int('record_timestamp') ar = data.get_int('ar') score = max(score, data.get_int('score')) clear_type = max(clear_type, data.get_int('clear_type')) play_time = Time.now() plays = data.get_int('plays') + 1 self.data.local.user.put_achievement( self.game, self.version, userid, dojoid, 'dojo', { 'clear_type': clear_type, 'ar': ar, 'score': score, 'plays': plays, 'play_timestamp': play_time, 'record_timestamp': record_time, }, ) # Save player config configdict = newprofile.get_dict('config') config = request.child('pdata/config') if config: configdict.replace_int('msel_bgm', config.child_value('msel_bgm')) configdict.replace_int('narrowdown_type', config.child_value('narrowdown_type')) configdict.replace_int('icon_id', config.child_value('icon_id')) configdict.replace_int('byword_0', config.child_value('byword_0')) configdict.replace_int('byword_1', config.child_value('byword_1')) configdict.replace_bool('is_auto_byword_0', config.child_value('is_auto_byword_0')) configdict.replace_bool('is_auto_byword_1', config.child_value('is_auto_byword_1')) configdict.replace_int('mrec_type', config.child_value('mrec_type')) configdict.replace_int('tab_sel', config.child_value('tab_sel')) configdict.replace_int('card_disp', config.child_value('card_disp')) configdict.replace_int('score_tab_disp', config.child_value('score_tab_disp')) configdict.replace_int('last_music_id', config.child_value('last_music_id')) configdict.replace_int('last_note_grade', config.child_value('last_note_grade')) configdict.replace_int('sort_type', config.child_value('sort_type')) configdict.replace_int('rival_panel_type', config.child_value('rival_panel_type')) configdict.replace_int('random_entry_work', config.child_value('random_entry_work')) configdict.replace_int('custom_folder_work', config.child_value('custom_folder_work')) configdict.replace_int('folder_type', config.child_value('folder_type')) configdict.replace_int('folder_lamp_type', config.child_value('folder_lamp_type')) configdict.replace_bool('is_tweet', config.child_value('is_tweet')) configdict.replace_bool('is_link_twitter', config.child_value('is_link_twitter')) newprofile.replace_dict('config', configdict) # Save player custom settings customdict = newprofile.get_dict('custom') custom = request.child('pdata/custom') if custom: customdict.replace_int('st_shot', custom.child_value('st_shot')) customdict.replace_int('st_frame', custom.child_value('st_frame')) customdict.replace_int('st_expl', custom.child_value('st_expl')) customdict.replace_int('st_bg', custom.child_value('st_bg')) customdict.replace_int('st_shot_vol', custom.child_value('st_shot_vol')) customdict.replace_int('st_bg_bri', custom.child_value('st_bg_bri')) customdict.replace_int('st_obj_size', custom.child_value('st_obj_size')) customdict.replace_int('st_jr_gauge', custom.child_value('st_jr_gauge')) customdict.replace_int('st_clr_gauge', custom.child_value('st_clr_gauge')) customdict.replace_int('st_jdg_disp', custom.child_value('st_jdg_disp')) customdict.replace_int('st_tm_disp', custom.child_value('st_tm_disp')) customdict.replace_int('st_rnd', custom.child_value('st_rnd')) customdict.replace_int('st_hazard', custom.child_value('st_hazard')) customdict.replace_int('st_clr_cond', custom.child_value('st_clr_cond')) customdict.replace_int_array('schat_0', 10, custom.child_value('schat_0')) customdict.replace_int_array('schat_1', 10, custom.child_value('schat_1')) customdict.replace_int('cheer_voice', custom.child_value('cheer_voice')) customdict.replace_int('same_time_note_disp', custom.child_value('same_time_note_disp')) newprofile.replace_dict('custom', customdict) # Save player parameter info params = request.child('pdata/player_param') if params: for child in params.children: if child.name != 'item': continue item_type = child.child_value('type') bank = child.child_value('bank') paramdata = child.child_value('data') or [] while len(paramdata) < 256: paramdata.append(0) self.data.local.user.put_achievement( self.game, self.version, userid, bank, f'player_param_{item_type}', { 'data': paramdata, }, ) # Save player episode info episode = request.child('pdata/episode') if episode: for child in episode.children: if child.name != 'info': continue # I assume this is copypasta, but I want to be sure extid = child.child_value('user_id') if extid != newprofile.extid: raise Exception(f'Unexpected user ID, got {extid} expecting {newprofile.extid}') episode_type = child.child_value('type') episode_value0 = child.child_value('value0') episode_value1 = child.child_value('value1') episode_text = child.child_value('text') episode_time = child.child_value('time') self.data.local.user.put_achievement( self.game, self.version, userid, episode_type, 'episode', { 'value0': episode_value0, 'value1': episode_value1, 'text': episode_text, 'time': episode_time, }, ) # Save released info 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, }, ) # Save announce info announce = request.child('pdata/announce') if announce: for child in announce.children: if child.name != 'info': continue announce_id = child.child_value('id') announce_type = child.child_value('type') param = child.child_value('param') need = child.child_value('bneedannounce') self.data.local.user.put_achievement( self.game, self.version, userid, announce_id, f'announcement_{announce_type}', { 'param': param, 'need': need, }, ) # Grab any new rivals added during this play session rivalnode = request.child('pdata/rival') if rivalnode: for child in rivalnode.children: if child.name != 'r': continue extid = child.child_value('id') other_userid = self.data.remote.user.from_extid(self.game, self.version, extid) if other_userid is None: continue self.data.local.user.put_link( self.game, self.version, userid, 'rival', other_userid, {}, ) # Grab any new records set during this play session songplays = request.child('pdata/stglog') if songplays: for child in songplays.children: if child.name != 'log': continue songid = child.child_value('mid') chart = child.child_value('ng') clear_type = child.child_value('ct') if songid == 0 and chart == 0 and clear_type == -1: # Dummy song save during profile create continue points = child.child_value('sc') achievement_rate = child.child_value('ar') param = child.child_value('param') miss_count = child.child_value('jt_ms') # Param is some random bits along with the combo type combo_type = param & 0x3 param = param ^ combo_type clear_type = self.__game_to_db_clear_type(clear_type) combo_type = self.__game_to_db_combo_type(combo_type, miss_count) self.update_score( userid, songid, chart, points, achievement_rate, clear_type, combo_type, miss_count, param=param, ) # Keep track of play statistics self.update_play_statistics(userid) return newprofile