# vim: set fileencoding=utf-8 import random from typing import Optional, Dict, List, Tuple, Any from typing_extensions import Final from bemani.backend.iidx.base import IIDXBase from bemani.backend.iidx.stubs import IIDXLincle from bemani.common import Profile, ValidatedDict, VersionConstants, Time, ID from bemani.data import Data, UserID from bemani.protocol import Node class IIDXTricoro(IIDXBase): name: str = 'Beatmania IIDX Tricoro' version: int = VersionConstants.IIDX_TRICORO GAME_CLTYPE_SINGLE: Final[int] = 0 GAME_CLTYPE_DOUBLE: Final[int] = 1 DAN_STAGES_SINGLE: Final[int] = 4 DAN_STAGES_DOUBLE: Final[int] = 3 GAME_CLEAR_STATUS_NO_PLAY: Final[int] = 0 GAME_CLEAR_STATUS_FAILED: Final[int] = 1 GAME_CLEAR_STATUS_ASSIST_CLEAR: Final[int] = 2 GAME_CLEAR_STATUS_EASY_CLEAR: Final[int] = 3 GAME_CLEAR_STATUS_CLEAR: Final[int] = 4 GAME_CLEAR_STATUS_HARD_CLEAR: Final[int] = 5 GAME_CLEAR_STATUS_EX_HARD_CLEAR: Final[int] = 6 GAME_CLEAR_STATUS_FULL_COMBO: Final[int] = 7 GAME_GHOST_TYPE_RIVAL: Final[int] = 1 GAME_GHOST_TYPE_GLOBAL_TOP: Final[int] = 2 GAME_GHOST_TYPE_GLOBAL_AVERAGE: Final[int] = 3 GAME_GHOST_TYPE_LOCAL_TOP: Final[int] = 4 GAME_GHOST_TYPE_LOCAL_AVERAGE: Final[int] = 5 GAME_GHOST_TYPE_DAN_TOP: Final[int] = 6 GAME_GHOST_TYPE_DAN_AVERAGE: Final[int] = 7 GAME_GHOST_TYPE_RIVAL_TOP: Final[int] = 8 GAME_GHOST_TYPE_RIVAL_AVERAGE: Final[int] = 9 GAME_GHOST_LENGTH: Final[int] = 64 GAME_SP_DAN_RANK_7_KYU: Final[int] = 0 GAME_SP_DAN_RANK_6_KYU: Final[int] = 1 GAME_SP_DAN_RANK_5_KYU: Final[int] = 2 GAME_SP_DAN_RANK_4_KYU: Final[int] = 3 GAME_SP_DAN_RANK_3_KYU: Final[int] = 4 GAME_SP_DAN_RANK_2_KYU: Final[int] = 5 GAME_SP_DAN_RANK_1_KYU: Final[int] = 6 GAME_SP_DAN_RANK_1_DAN: Final[int] = 7 GAME_SP_DAN_RANK_2_DAN: Final[int] = 8 GAME_SP_DAN_RANK_3_DAN: Final[int] = 9 GAME_SP_DAN_RANK_4_DAN: Final[int] = 10 GAME_SP_DAN_RANK_5_DAN: Final[int] = 11 GAME_SP_DAN_RANK_6_DAN: Final[int] = 12 GAME_SP_DAN_RANK_7_DAN: Final[int] = 13 GAME_SP_DAN_RANK_8_DAN: Final[int] = 14 GAME_SP_DAN_RANK_9_DAN: Final[int] = 15 GAME_SP_DAN_RANK_10_DAN: Final[int] = 16 GAME_SP_DAN_RANK_KAIDEN: Final[int] = 17 GAME_DP_DAN_RANK_5_KYU: Final[int] = 0 GAME_DP_DAN_RANK_4_KYU: Final[int] = 1 GAME_DP_DAN_RANK_3_KYU: Final[int] = 2 GAME_DP_DAN_RANK_2_KYU: Final[int] = 3 GAME_DP_DAN_RANK_1_KYU: Final[int] = 4 GAME_DP_DAN_RANK_1_DAN: Final[int] = 5 GAME_DP_DAN_RANK_2_DAN: Final[int] = 6 GAME_DP_DAN_RANK_3_DAN: Final[int] = 7 GAME_DP_DAN_RANK_4_DAN: Final[int] = 8 GAME_DP_DAN_RANK_5_DAN: Final[int] = 9 GAME_DP_DAN_RANK_6_DAN: Final[int] = 10 GAME_DP_DAN_RANK_7_DAN: Final[int] = 11 GAME_DP_DAN_RANK_8_DAN: Final[int] = 12 GAME_DP_DAN_RANK_9_DAN: Final[int] = 13 GAME_DP_DAN_RANK_10_DAN: Final[int] = 14 GAME_DP_DAN_RANK_KAIDEN: Final[int] = 15 FAVORITE_LIST_LENGTH: Final[int] = 20 def previous_version(self) -> Optional[IIDXBase]: return IIDXLincle(self.data, self.config, self.model) @classmethod def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]: """ Insert dailies into the DB. """ events = [] if data.local.network.should_schedule(cls.game, cls.version, 'daily_charts', 'daily'): # Generate a new list of three dailies. start_time, end_time = data.local.network.get_schedule_duration('daily') all_songs = list(set([song.id for song in data.local.music.get_all_songs(cls.game, cls.version)])) if len(all_songs) >= 3: daily_songs = random.sample(all_songs, 3) data.local.game.put_time_sensitive_settings( cls.game, cls.version, 'dailies', { 'start_time': start_time, 'end_time': end_time, 'music': daily_songs, }, ) events.append(( 'iidx_daily_charts', { 'version': cls.version, 'music': daily_songs, }, )) # Mark that we did some actual work here. data.local.network.mark_scheduled(cls.game, cls.version, 'daily_charts', 'daily') return events @classmethod def get_settings(cls) -> Dict[str, Any]: """ Return all of our front-end modifiably settings. """ return { 'bools': [ { 'name': 'Global Shop Ranking', 'tip': 'Return network-wide ranking instead of shop ranking on results screen.', 'category': 'game_config', 'setting': 'global_shop_ranking', }, { 'name': 'Events In Omnimix', 'tip': 'Allow events to be enabled at all for Omnimix.', 'category': 'game_config', 'setting': 'omnimix_events_enabled', }, ], } def db_to_game_status(self, db_status: int) -> int: return { self.CLEAR_STATUS_NO_PLAY: self.GAME_CLEAR_STATUS_NO_PLAY, self.CLEAR_STATUS_FAILED: self.GAME_CLEAR_STATUS_FAILED, self.CLEAR_STATUS_ASSIST_CLEAR: self.GAME_CLEAR_STATUS_ASSIST_CLEAR, self.CLEAR_STATUS_EASY_CLEAR: self.GAME_CLEAR_STATUS_EASY_CLEAR, self.CLEAR_STATUS_CLEAR: self.GAME_CLEAR_STATUS_CLEAR, self.CLEAR_STATUS_HARD_CLEAR: self.GAME_CLEAR_STATUS_HARD_CLEAR, self.CLEAR_STATUS_EX_HARD_CLEAR: self.GAME_CLEAR_STATUS_EX_HARD_CLEAR, self.CLEAR_STATUS_FULL_COMBO: self.GAME_CLEAR_STATUS_FULL_COMBO, }[db_status] def game_to_db_status(self, game_status: int) -> int: return { self.GAME_CLEAR_STATUS_NO_PLAY: self.CLEAR_STATUS_NO_PLAY, self.GAME_CLEAR_STATUS_FAILED: self.CLEAR_STATUS_FAILED, self.GAME_CLEAR_STATUS_ASSIST_CLEAR: self.CLEAR_STATUS_ASSIST_CLEAR, self.GAME_CLEAR_STATUS_EASY_CLEAR: self.CLEAR_STATUS_EASY_CLEAR, self.GAME_CLEAR_STATUS_CLEAR: self.CLEAR_STATUS_CLEAR, self.GAME_CLEAR_STATUS_HARD_CLEAR: self.CLEAR_STATUS_HARD_CLEAR, self.GAME_CLEAR_STATUS_EX_HARD_CLEAR: self.CLEAR_STATUS_EX_HARD_CLEAR, self.GAME_CLEAR_STATUS_FULL_COMBO: self.CLEAR_STATUS_FULL_COMBO, }[game_status] def db_to_game_rank(self, db_dan: int, cltype: int) -> int: # Special case for no DAN rank if db_dan == -1: return -1 if cltype == self.GAME_CLTYPE_SINGLE: return { self.DAN_RANK_7_KYU: self.GAME_SP_DAN_RANK_7_KYU, self.DAN_RANK_6_KYU: self.GAME_SP_DAN_RANK_6_KYU, self.DAN_RANK_5_KYU: self.GAME_SP_DAN_RANK_5_KYU, self.DAN_RANK_4_KYU: self.GAME_SP_DAN_RANK_4_KYU, self.DAN_RANK_3_KYU: self.GAME_SP_DAN_RANK_3_KYU, self.DAN_RANK_2_KYU: self.GAME_SP_DAN_RANK_2_KYU, self.DAN_RANK_1_KYU: self.GAME_SP_DAN_RANK_1_KYU, self.DAN_RANK_1_DAN: self.GAME_SP_DAN_RANK_1_DAN, self.DAN_RANK_2_DAN: self.GAME_SP_DAN_RANK_2_DAN, self.DAN_RANK_3_DAN: self.GAME_SP_DAN_RANK_3_DAN, self.DAN_RANK_4_DAN: self.GAME_SP_DAN_RANK_4_DAN, self.DAN_RANK_5_DAN: self.GAME_SP_DAN_RANK_5_DAN, self.DAN_RANK_6_DAN: self.GAME_SP_DAN_RANK_6_DAN, self.DAN_RANK_7_DAN: self.GAME_SP_DAN_RANK_7_DAN, self.DAN_RANK_8_DAN: self.GAME_SP_DAN_RANK_8_DAN, self.DAN_RANK_9_DAN: self.GAME_SP_DAN_RANK_9_DAN, self.DAN_RANK_10_DAN: self.GAME_SP_DAN_RANK_10_DAN, self.DAN_RANK_KAIDEN: self.GAME_SP_DAN_RANK_KAIDEN, }[db_dan] elif cltype == self.GAME_CLTYPE_DOUBLE: return { self.DAN_RANK_5_KYU: self.GAME_DP_DAN_RANK_5_KYU, self.DAN_RANK_4_KYU: self.GAME_DP_DAN_RANK_4_KYU, self.DAN_RANK_3_KYU: self.GAME_DP_DAN_RANK_3_KYU, self.DAN_RANK_2_KYU: self.GAME_DP_DAN_RANK_2_KYU, self.DAN_RANK_1_KYU: self.GAME_DP_DAN_RANK_1_KYU, self.DAN_RANK_1_DAN: self.GAME_DP_DAN_RANK_1_DAN, self.DAN_RANK_2_DAN: self.GAME_DP_DAN_RANK_2_DAN, self.DAN_RANK_3_DAN: self.GAME_DP_DAN_RANK_3_DAN, self.DAN_RANK_4_DAN: self.GAME_DP_DAN_RANK_4_DAN, self.DAN_RANK_5_DAN: self.GAME_DP_DAN_RANK_5_DAN, self.DAN_RANK_6_DAN: self.GAME_DP_DAN_RANK_6_DAN, self.DAN_RANK_7_DAN: self.GAME_DP_DAN_RANK_7_DAN, self.DAN_RANK_8_DAN: self.GAME_DP_DAN_RANK_8_DAN, self.DAN_RANK_9_DAN: self.GAME_DP_DAN_RANK_9_DAN, self.DAN_RANK_10_DAN: self.GAME_DP_DAN_RANK_10_DAN, self.DAN_RANK_KAIDEN: self.GAME_DP_DAN_RANK_KAIDEN, }[db_dan] else: raise Exception('Invalid cltype!') def game_to_db_rank(self, game_dan: int, cltype: int) -> int: # Special case for no DAN rank if game_dan == -1: return -1 if cltype == self.GAME_CLTYPE_SINGLE: return { self.GAME_SP_DAN_RANK_7_KYU: self.DAN_RANK_7_KYU, self.GAME_SP_DAN_RANK_6_KYU: self.DAN_RANK_6_KYU, self.GAME_SP_DAN_RANK_5_KYU: self.DAN_RANK_5_KYU, self.GAME_SP_DAN_RANK_4_KYU: self.DAN_RANK_4_KYU, self.GAME_SP_DAN_RANK_3_KYU: self.DAN_RANK_3_KYU, self.GAME_SP_DAN_RANK_2_KYU: self.DAN_RANK_2_KYU, self.GAME_SP_DAN_RANK_1_KYU: self.DAN_RANK_1_KYU, self.GAME_SP_DAN_RANK_1_DAN: self.DAN_RANK_1_DAN, self.GAME_SP_DAN_RANK_2_DAN: self.DAN_RANK_2_DAN, self.GAME_SP_DAN_RANK_3_DAN: self.DAN_RANK_3_DAN, self.GAME_SP_DAN_RANK_4_DAN: self.DAN_RANK_4_DAN, self.GAME_SP_DAN_RANK_5_DAN: self.DAN_RANK_5_DAN, self.GAME_SP_DAN_RANK_6_DAN: self.DAN_RANK_6_DAN, self.GAME_SP_DAN_RANK_7_DAN: self.DAN_RANK_7_DAN, self.GAME_SP_DAN_RANK_8_DAN: self.DAN_RANK_8_DAN, self.GAME_SP_DAN_RANK_9_DAN: self.DAN_RANK_9_DAN, self.GAME_SP_DAN_RANK_10_DAN: self.DAN_RANK_10_DAN, self.GAME_SP_DAN_RANK_KAIDEN: self.DAN_RANK_KAIDEN, }[game_dan] elif cltype == self.GAME_CLTYPE_DOUBLE: return { self.GAME_DP_DAN_RANK_5_KYU: self.DAN_RANK_5_KYU, self.GAME_DP_DAN_RANK_4_KYU: self.DAN_RANK_4_KYU, self.GAME_DP_DAN_RANK_3_KYU: self.DAN_RANK_3_KYU, self.GAME_DP_DAN_RANK_2_KYU: self.DAN_RANK_2_KYU, self.GAME_DP_DAN_RANK_1_KYU: self.DAN_RANK_1_KYU, self.GAME_DP_DAN_RANK_1_DAN: self.DAN_RANK_1_DAN, self.GAME_DP_DAN_RANK_2_DAN: self.DAN_RANK_2_DAN, self.GAME_DP_DAN_RANK_3_DAN: self.DAN_RANK_3_DAN, self.GAME_DP_DAN_RANK_4_DAN: self.DAN_RANK_4_DAN, self.GAME_DP_DAN_RANK_5_DAN: self.DAN_RANK_5_DAN, self.GAME_DP_DAN_RANK_6_DAN: self.DAN_RANK_6_DAN, self.GAME_DP_DAN_RANK_7_DAN: self.DAN_RANK_7_DAN, self.GAME_DP_DAN_RANK_8_DAN: self.DAN_RANK_8_DAN, self.GAME_DP_DAN_RANK_9_DAN: self.DAN_RANK_9_DAN, self.GAME_DP_DAN_RANK_10_DAN: self.DAN_RANK_10_DAN, self.GAME_DP_DAN_RANK_KAIDEN: self.DAN_RANK_KAIDEN, }[game_dan] else: raise Exception('Invalid cltype!') def handle_shop_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'getname': root = Node.void('shop') root.set_attribute('cls_opt', '0') machine = self.data.local.machine.get_machine(self.config.machine.pcbid) root.set_attribute('opname', machine.name) root.set_attribute('pid', str(self.get_machine_region())) return root if method == 'savename': self.update_machine_name(request.attribute('opname')) root = Node.void('shop') return root if method == 'sentinfo': root = Node.void('shop') return root if method == 'getconvention': root = Node.void('shop') machine = self.data.local.machine.get_machine(self.config.machine.pcbid) if machine.arcade is not None: course = self.data.local.machine.get_settings(machine.arcade, self.game, self.music_version, 'shop_course') else: course = None if course is None: course = ValidatedDict() root.set_attribute('music_0', str(course.get_int('music_0', 20032))) root.set_attribute('music_1', str(course.get_int('music_1', 20009))) root.set_attribute('music_2', str(course.get_int('music_2', 20015))) root.set_attribute('music_3', str(course.get_int('music_3', 20064))) root.add_child(Node.bool('valid', course.get_bool('valid'))) return root if method == 'setconvention': root = Node.void('shop') machine = self.data.local.machine.get_machine(self.config.machine.pcbid) if machine.arcade is not None: course = ValidatedDict() course.replace_int('music_0', request.child_value('music_0')) course.replace_int('music_1', request.child_value('music_1')) course.replace_int('music_2', request.child_value('music_2')) course.replace_int('music_3', request.child_value('music_3')) course.replace_bool('valid', request.child_value('valid')) self.data.local.machine.put_settings(machine.arcade, self.game, self.music_version, 'shop_course', course) return root # Invalid method return None def handle_ranking_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'getranker': root = Node.void('ranking') chart = int(request.attribute('clid')) if chart not in [ self.CHART_TYPE_N7, self.CHART_TYPE_H7, self.CHART_TYPE_A7, self.CHART_TYPE_N14, self.CHART_TYPE_H14, self.CHART_TYPE_A14, ]: # Chart type 6 is presumably beginner mode, but it crashes the game return root machine = self.data.local.machine.get_machine(self.config.machine.pcbid) if machine.arcade is not None: course = self.data.local.machine.get_settings(machine.arcade, self.game, self.music_version, 'shop_course') else: course = None if course is None: course = ValidatedDict() if not course.get_bool('valid'): # Shop course not enabled or not present return root convention = Node.void('convention') root.add_child(convention) convention.set_attribute('clid', str(chart)) convention.set_attribute('update_date', str(Time.now() * 1000)) # Grab all scores for each of the four songs, filter out people who haven't # set us as their arcade and then return the top 20 scores (adding all 4 songs). songids = [ course.get_int('music_0'), course.get_int('music_1'), course.get_int('music_2'), course.get_int('music_3'), ] totalscores: Dict[UserID, int] = {} profiles: Dict[UserID, Profile] = {} for songid in songids: scores = self.data.local.music.get_all_scores( self.game, self.music_version, songid=songid, songchart=chart, ) for score in scores: if score[0] not in totalscores: totalscores[score[0]] = 0 profile = self.get_any_profile(score[0]) if profile is None: profile = Profile(self.game, self.version, "", 0) profiles[score[0]] = profile totalscores[score[0]] += score[1].points topscores = sorted( [ (totalscores[userid], profiles[userid]) for userid in totalscores if self.user_joined_arcade(machine, profiles[userid]) ], key=lambda tup: tup[0], reverse=True, )[:20] rank = 0 for topscore in topscores: rank = rank + 1 detail = Node.void('detail') convention.add_child(detail) detail.set_attribute('name', topscore[1].get_str('name')) detail.set_attribute('rank', str(rank)) detail.set_attribute('score', str(topscore[0])) detail.set_attribute('pid', str(topscore[1].get_int('pid'))) qpro = topscore[1].get_dict('qpro') detail.set_attribute('head', str(qpro.get_int('head'))) detail.set_attribute('hair', str(qpro.get_int('hair'))) detail.set_attribute('face', str(qpro.get_int('face'))) detail.set_attribute('body', str(qpro.get_int('body'))) detail.set_attribute('hand', str(qpro.get_int('hand'))) return root # Invalid method return None def handle_music_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'crate': root = Node.void('music') attempts = self.get_clear_rates() all_songs = list(set([song.id for song in self.data.local.music.get_all_songs(self.game, self.music_version)])) for song in all_songs: clears = [] fcs = [] for chart in [0, 1, 2, 3, 4, 5]: placed = False if song in attempts and chart in attempts[song]: values = attempts[song][chart] if values['total'] > 0: clears.append(int((100 * values['clears']) / values['total'])) fcs.append(int((100 * values['fcs']) / values['total'])) placed = True if not placed: clears.append(101) fcs.append(101) clearnode = Node.u8_array('c', clears + fcs) clearnode.set_attribute('mid', str(song)) root.add_child(clearnode) return root if method == 'getrank': cltype = int(request.attribute('cltype')) root = Node.void('music') style = Node.void('style') root.add_child(style) style.set_attribute('type', str(cltype)) for rivalid in [-1, 0, 1, 2, 3, 4]: if rivalid == -1: attr = 'iidxid' else: attr = f'iidxid{rivalid}' try: extid = int(request.attribute(attr)) except Exception: # Invalid extid continue userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: scores = self.data.remote.music.get_scores(self.game, self.music_version, userid) # Grab score data for user/rival scoredata = self.make_score_struct( scores, self.CLEAR_TYPE_SINGLE if cltype == self.GAME_CLTYPE_SINGLE else self.CLEAR_TYPE_DOUBLE, rivalid, ) for s in scoredata: root.add_child(Node.s16_array('m', s)) # Grab most played for user/rival most_played = [ play[0] for play in self.data.local.music.get_most_played(self.game, self.music_version, userid, 20) ] if len(most_played) < 20: most_played.extend([0] * (20 - len(most_played))) best = Node.u16_array('best', most_played) best.set_attribute('rno', str(rivalid)) root.add_child(best) if rivalid == -1: # Grab beginner statuses for user only beginnerdata = self.make_beginner_struct(scores) for b in beginnerdata: root.add_child(Node.u16_array('b', b)) return root if method == 'reg': extid = int(request.attribute('iidxid')) musicid = int(request.attribute('mid')) chart = int(request.attribute('clid')) userid = self.data.remote.user.from_extid(self.game, self.version, extid) # See if we need to report global or shop scores if self.machine_joined_arcade(): game_config = self.get_game_config() global_scores = game_config.get_bool('global_shop_ranking') machine = self.data.local.machine.get_machine(self.config.machine.pcbid) else: # If we aren't in an arcade, we can only show global scores global_scores = True machine = None # First, determine our current ranking before saving the new score all_scores = sorted( self.data.remote.music.get_all_scores(game=self.game, version=self.music_version, songid=musicid, songchart=chart), key=lambda s: (s[1].points, s[1].timestamp), reverse=True, ) all_players = { uid: prof for (uid, prof) in self.get_any_profiles([s[0] for s in all_scores]) } if not global_scores: all_scores = [ score for score in all_scores if ( score[0] == userid or self.user_joined_arcade(machine, all_players[score[0]]) ) ] # Find our actual index oldindex = None for i in range(len(all_scores)): if all_scores[i][0] == userid: oldindex = i break if userid is not None: clear_status = self.game_to_db_status(int(request.attribute('cflg'))) pgreats = int(request.attribute('pgnum')) greats = int(request.attribute('gnum')) miss_count = int(request.attribute('mnum')) ghost = request.child_value('ghost') shopid = ID.parse_machine_id(request.attribute('shopconvid')) self.update_score( userid, musicid, chart, clear_status, pgreats, greats, miss_count, ghost, shopid, ) # Calculate and return statistics about this song root = Node.void('music') root.set_attribute('clid', request.attribute('clid')) root.set_attribute('mid', request.attribute('mid')) attempts = self.get_clear_rates(musicid, chart) count = attempts[musicid][chart]['total'] clear = attempts[musicid][chart]['clears'] full_combo = attempts[musicid][chart]['fcs'] if count > 0: root.set_attribute('crate', str(int((100 * clear) / count))) root.set_attribute('frate', str(int((100 * full_combo) / count))) else: root.set_attribute('crate', '0') root.set_attribute('frate', '0') if userid is not None: # Shop ranking shopdata = Node.void('shopdata') root.add_child(shopdata) shopdata.set_attribute('rank', '-1' if oldindex is None else str(oldindex + 1)) # Grab the rank of some other players on this song ranklist = Node.void('ranklist') root.add_child(ranklist) all_scores = sorted( self.data.remote.music.get_all_scores(game=self.game, version=self.music_version, songid=musicid, songchart=chart), key=lambda s: (s[1].points, s[1].timestamp), reverse=True, ) missing_players = [ uid for (uid, _) in all_scores if uid not in all_players ] for (uid, prof) in self.get_any_profiles(missing_players): all_players[uid] = prof if not global_scores: all_scores = [ score for score in all_scores if ( score[0] == userid or self.user_joined_arcade(machine, all_players[score[0]]) ) ] # Find our actual index ourindex = None for i in range(len(all_scores)): if all_scores[i][0] == userid: ourindex = i break if ourindex is None: raise Exception('Cannot find our own score after saving to DB!') start = ourindex - 4 end = ourindex + 4 if start < 0: start = 0 if end >= len(all_scores): end = len(all_scores) - 1 relevant_scores = all_scores[start:(end + 1)] record_num = start + 1 for score in relevant_scores: profile = all_players[score[0]] data = Node.void('data') ranklist.add_child(data) data.set_attribute('iidx_id', str(profile.extid)) data.set_attribute('name', profile.get_str('name')) machine_name = '' if 'shop_location' in profile: shop_id = profile.get_int('shop_location') machine = self.get_machine_by_id(shop_id) if machine is not None: machine_name = machine.name data.set_attribute('opname', machine_name) data.set_attribute('rnum', str(record_num)) data.set_attribute('score', str(score[1].points)) data.set_attribute('clflg', str(self.db_to_game_status(score[1].data.get_int('clear_status')))) data.set_attribute('pid', str(profile.get_int('pid'))) data.set_attribute('myFlg', '1' if score[0] == userid else '0') data.set_attribute('sgrade', str( self.db_to_game_rank(profile.get_int(self.DAN_RANKING_SINGLE, -1), self.GAME_CLTYPE_SINGLE), )) data.set_attribute('dgrade', str( self.db_to_game_rank(profile.get_int(self.DAN_RANKING_DOUBLE, -1), self.GAME_CLTYPE_DOUBLE), )) qpro = profile.get_dict('qpro') data.set_attribute('head', str(qpro.get_int('head'))) data.set_attribute('hair', str(qpro.get_int('hair'))) data.set_attribute('face', str(qpro.get_int('face'))) data.set_attribute('body', str(qpro.get_int('body'))) data.set_attribute('hand', str(qpro.get_int('hand'))) record_num = record_num + 1 return root if method == 'breg': extid = int(request.attribute('iidxid')) musicid = int(request.attribute('mid')) userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: clear_status = self.game_to_db_status(int(request.attribute('cflg'))) pgreats = int(request.attribute('pgnum')) greats = int(request.attribute('gnum')) self.update_score( userid, musicid, self.CHART_TYPE_B7, clear_status, pgreats, greats, -1, b'', None, ) # Return nothing. root = Node.void('music') return root if method == 'play': musicid = int(request.attribute('mid')) chart = int(request.attribute('clid')) clear_status = self.game_to_db_status(int(request.attribute('cflg'))) self.update_score( None, # No userid since its anonymous musicid, chart, clear_status, 0, # No ex score 0, # No ex score 0, # No miss count None, # No ghost None, # No shop for this user ) # Calculate and return statistics about this song root = Node.void('music') root.set_attribute('clid', request.attribute('clid')) root.set_attribute('mid', request.attribute('mid')) attempts = self.get_clear_rates(musicid, chart) count = attempts[musicid][chart]['total'] clear = attempts[musicid][chart]['clears'] full_combo = attempts[musicid][chart]['fcs'] if count > 0: root.set_attribute('crate', str(int((100 * clear) / count))) root.set_attribute('frate', str(int((100 * full_combo) / count))) else: root.set_attribute('crate', '0') root.set_attribute('frate', '0') return root if method == 'appoint': musicid = int(request.attribute('mid')) chart = int(request.attribute('clid')) ghost_type = int(request.attribute('ctype')) extid = int(request.attribute('iidxid')) userid = self.data.remote.user.from_extid(self.game, self.version, extid) root = Node.void('music') if userid is not None: # Try to look up previous ghost for user my_score = self.data.remote.music.get_score(self.game, self.music_version, userid, musicid, chart) if my_score is not None: mydata = Node.binary('mydata', my_score.data.get_bytes('ghost')) mydata.set_attribute('score', str(my_score.points)) root.add_child(mydata) ghost_score = self.get_ghost( { self.GAME_GHOST_TYPE_RIVAL: self.GHOST_TYPE_RIVAL, self.GAME_GHOST_TYPE_GLOBAL_TOP: self.GHOST_TYPE_GLOBAL_TOP, self.GAME_GHOST_TYPE_GLOBAL_AVERAGE: self.GHOST_TYPE_GLOBAL_AVERAGE, self.GAME_GHOST_TYPE_LOCAL_TOP: self.GHOST_TYPE_LOCAL_TOP, self.GAME_GHOST_TYPE_LOCAL_AVERAGE: self.GHOST_TYPE_LOCAL_AVERAGE, self.GAME_GHOST_TYPE_DAN_TOP: self.GHOST_TYPE_DAN_TOP, self.GAME_GHOST_TYPE_DAN_AVERAGE: self.GHOST_TYPE_DAN_AVERAGE, self.GAME_GHOST_TYPE_RIVAL_TOP: self.GHOST_TYPE_RIVAL_TOP, self.GAME_GHOST_TYPE_RIVAL_AVERAGE: self.GHOST_TYPE_RIVAL_AVERAGE, }.get(ghost_type, self.GHOST_TYPE_NONE), request.attribute('subtype'), self.GAME_GHOST_LENGTH, musicid, chart, userid, ) # Add ghost score if we support it if ghost_score is not None: sdata = Node.binary('sdata', ghost_score['ghost']) sdata.set_attribute('score', str(ghost_score['score'])) if 'name' in ghost_score: sdata.set_attribute('name', ghost_score['name']) if 'pid' in ghost_score: sdata.set_attribute('pid', str(ghost_score['pid'])) if 'extid' in ghost_score: sdata.set_attribute('riidxid', str(ghost_score['extid'])) root.add_child(sdata) return root # Invalid method return None def handle_pc_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'common': root = Node.void('pc') root.set_attribute('expire', '600') # TODO: Hook all of these up to config options I guess? ir = Node.void('ir') root.add_child(ir) ir.set_attribute('beat', '2') limit = Node.void('limit') root.add_child(limit) limit.set_attribute('phase', '24') # See if we configured event overrides if self.machine_joined_arcade(): game_config = self.get_game_config() omni_events = game_config.get_bool('omnimix_events_enabled') else: # If we aren't in an arcade, we turn off events omni_events = False if self.omnimix and (not omni_events): boss_phase = 0 else: # TODO: Figure out what these map to boss_phase = 0 boss = Node.void('boss') root.add_child(boss) boss.set_attribute('phase', str(boss_phase)) red = Node.void('red') root.add_child(red) red.set_attribute('phase', '0') yellow = Node.void('yellow') root.add_child(yellow) yellow.set_attribute('phase', '0') medal = Node.void('medal') root.add_child(medal) medal.set_attribute('phase', '1') cafe = Node.void('cafe') root.add_child(cafe) cafe.set_attribute('open', '1') tricolettepark = Node.void('tricolettepark') root.add_child(tricolettepark) tricolettepark.set_attribute('open', '0') return root if method == 'delete': return Node.void('pc') if method == 'playstart': return Node.void('pc') if method == 'playend': return Node.void('pc') if method == 'oldget': refid = request.attribute('rid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: oldversion = self.previous_version() profile = oldversion.get_profile(userid) else: profile = None root = Node.void('pc') root.set_attribute('status', '1' if profile is None else '0') return root if method == 'getname': refid = request.attribute('rid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: oldversion = self.previous_version() profile = oldversion.get_profile(userid) else: profile = None if profile is None: raise Exception( 'Should not get here if we have no profile, we should ' + 'have returned \'1\' in the \'oldget\' method above ' + 'which should tell the game not to present a migration.' ) root = Node.void('pc') root.set_attribute('name', profile.get_str('name')) root.set_attribute('idstr', ID.format_extid(profile.extid)) root.set_attribute('pid', str(profile.get_int('pid'))) return root if method == 'reg': refid = request.attribute('rid') name = request.attribute('name') pid = int(request.attribute('pid')) profile = self.new_profile_by_refid(refid, name, pid) root = Node.void('pc') if profile is not None: root.set_attribute('id', str(profile.extid)) root.set_attribute('id_str', ID.format_extid(profile.extid)) return root if method == 'get': refid = request.attribute('rid') root = self.get_profile_by_refid(refid) if root is None: root = Node.void('pc') return root if method == 'save': extid = int(request.attribute('iidxid')) self.put_profile_by_extid(extid, request) root = Node.void('pc') return root if method == 'visit': root = Node.void('pc') root.set_attribute('anum', '0') root.set_attribute('snum', '0') root.set_attribute('pnum', '0') root.set_attribute('aflg', '0') root.set_attribute('sflg', '0') root.set_attribute('pflg', '0') return root if method == 'shopregister': extid = int(request.child_value('iidx_id')) location = ID.parse_machine_id(request.child_value('location_id')) userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: profile = self.get_profile(userid) if profile is None: profile = Profile(self.game, self.version, "", extid) profile.replace_int('shop_location', location) self.put_profile(userid, profile) root = Node.void('pc') return root # Invalid method return None def handle_grade_request(self, request: Node) -> Optional[Node]: method = request.attribute('method') if method == 'raised': extid = int(request.attribute('iidxid')) cltype = int(request.attribute('gtype')) rank = self.game_to_db_rank(int(request.attribute('gid')), cltype) userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: percent = int(request.attribute('achi')) stages_cleared = int(request.attribute('cflg')) if cltype == self.GAME_CLTYPE_SINGLE: max_stages = self.DAN_STAGES_SINGLE else: max_stages = self.DAN_STAGES_DOUBLE cleared = stages_cleared == max_stages if cltype == self.GAME_CLTYPE_SINGLE: index = self.DAN_RANKING_SINGLE else: index = self.DAN_RANKING_DOUBLE self.update_rank( userid, index, rank, percent, cleared, stages_cleared, ) # Figure out number of players that played this ranking all_achievements = self.data.local.user.get_all_achievements(self.game, self.version, achievementid=rank, achievementtype=index) root = Node.void('grade') root.set_attribute('pnum', str(len(all_achievements))) return root # Invalid method return None def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void('pc') # Look up play stats we bridge to every mix play_stats = self.get_play_statistics(userid) # Look up judge window adjustments judge_dict = profile.get_dict('machine_judge_adjust') machine_judge = judge_dict.get_dict(self.config.machine.pcbid) # Profile data pcdata = Node.void('pcdata') root.add_child(pcdata) pcdata.set_attribute('id', str(profile.extid)) pcdata.set_attribute('idstr', ID.format_extid(profile.extid)) pcdata.set_attribute('name', profile.get_str('name')) pcdata.set_attribute('pid', str(profile.get_int('pid'))) pcdata.set_attribute('spnum', str(play_stats.get_int('single_plays'))) pcdata.set_attribute('dpnum', str(play_stats.get_int('double_plays'))) pcdata.set_attribute('sach', str(play_stats.get_int('single_dj_points'))) pcdata.set_attribute('dach', str(play_stats.get_int('double_dj_points'))) pcdata.set_attribute('help', str(profile.get_int('help'))) pcdata.set_attribute('gno', str(profile.get_int('gno'))) pcdata.set_attribute('gpos', str(profile.get_int('gpos'))) pcdata.set_attribute('timing', str(profile.get_int('timing'))) pcdata.set_attribute('sdhd', str(profile.get_int('sdhd'))) pcdata.set_attribute('sdtype', str(profile.get_int('sdtype'))) pcdata.set_attribute('notes', str(profile.get_float('notes'))) pcdata.set_attribute('pase', str(profile.get_int('pase'))) pcdata.set_attribute('sp_opt', str(profile.get_int('sp_opt'))) pcdata.set_attribute('dp_opt', str(profile.get_int('dp_opt'))) pcdata.set_attribute('dp_opt2', str(profile.get_int('dp_opt2'))) pcdata.set_attribute('mode', str(profile.get_int('mode'))) pcdata.set_attribute('pmode', str(profile.get_int('pmode'))) pcdata.set_attribute('liflen', str(profile.get_int('lift'))) pcdata.set_attribute('judge', str(profile.get_int('judge'))) pcdata.set_attribute('opstyle', str(profile.get_int('opstyle'))) pcdata.set_attribute('hispeed', str(profile.get_float('hispeed'))) pcdata.set_attribute('judgeAdj', str(machine_judge.get_int('adj'))) # Secret flags (shh!) secret_dict = profile.get_dict('secret') secret = Node.void('secret') root.add_child(secret) secret.add_child(Node.s64('flg1', secret_dict.get_int('flg1'))) secret.add_child(Node.s64('flg2', secret_dict.get_int('flg2'))) secret.add_child(Node.s64('flg3', secret_dict.get_int('flg3'))) # DAN rankings grade = Node.void('grade') root.add_child(grade) grade.set_attribute('sgid', str(self.db_to_game_rank(profile.get_int(self.DAN_RANKING_SINGLE, -1), self.GAME_CLTYPE_SINGLE))) grade.set_attribute('dgid', str(self.db_to_game_rank(profile.get_int(self.DAN_RANKING_DOUBLE, -1), self.GAME_CLTYPE_DOUBLE))) rankings = self.data.local.user.get_achievements(self.game, self.version, userid) for rank in rankings: if rank.type == self.DAN_RANKING_SINGLE: grade.add_child(Node.u8_array('g', [ self.GAME_CLTYPE_SINGLE, self.db_to_game_rank(rank.id, self.GAME_CLTYPE_SINGLE), rank.data.get_int('stages_cleared'), rank.data.get_int('percent'), ])) if rank.type == self.DAN_RANKING_DOUBLE: grade.add_child(Node.u8_array('g', [ self.GAME_CLTYPE_DOUBLE, self.db_to_game_rank(rank.id, self.GAME_CLTYPE_DOUBLE), rank.data.get_int('stages_cleared'), rank.data.get_int('percent'), ])) # User settings settings_dict = profile.get_dict('settings') skin = Node.s16_array( 'skin', [ settings_dict.get_int('frame'), settings_dict.get_int('turntable'), settings_dict.get_int('burst'), settings_dict.get_int('bgm'), settings_dict.get_int('flags'), settings_dict.get_int('towel'), settings_dict.get_int('judge_pos'), settings_dict.get_int('voice'), settings_dict.get_int('noteskin'), settings_dict.get_int('full_combo'), settings_dict.get_int('beam'), settings_dict.get_int('judge'), 0, settings_dict.get_int('disable_song_preview'), ], ) root.add_child(skin) # Qpro data qpro_dict = profile.get_dict('qpro') root.add_child(Node.u32_array( 'qprodata', [ qpro_dict.get_int('head'), qpro_dict.get_int('hair'), qpro_dict.get_int('face'), qpro_dict.get_int('hand'), qpro_dict.get_int('body'), ], )) # Rivals rlist = Node.void('rlist') root.add_child(rlist) links = self.data.local.user.get_links(self.game, self.version, userid) for link in links: rival_type = None if link.type == 'sp_rival': rival_type = '1' elif link.type == 'dp_rival': rival_type = '2' else: # No business with this link type continue other_profile = self.get_profile(link.other_userid) if other_profile is None: continue other_play_stats = self.get_play_statistics(link.other_userid) rival = Node.void('rival') rlist.add_child(rival) rival.set_attribute('spdp', rival_type) rival.set_attribute('id', str(other_profile.extid)) rival.set_attribute('id_str', ID.format_extid(other_profile.extid)) rival.set_attribute('djname', other_profile.get_str('name')) rival.set_attribute('pid', str(other_profile.get_int('pid'))) rival.set_attribute('sg', str(self.db_to_game_rank(other_profile.get_int(self.DAN_RANKING_SINGLE, -1), self.GAME_CLTYPE_SINGLE))) rival.set_attribute('dg', str(self.db_to_game_rank(other_profile.get_int(self.DAN_RANKING_DOUBLE, -1), self.GAME_CLTYPE_DOUBLE))) rival.set_attribute('sa', str(other_play_stats.get_int('single_dj_points'))) rival.set_attribute('da', str(other_play_stats.get_int('double_dj_points'))) # If the user joined a particular shop, let the game know. if 'shop_location' in other_profile: shop_id = other_profile.get_int('shop_location') machine = self.get_machine_by_id(shop_id) if machine is not None: shop = Node.void('shop') rival.add_child(shop) shop.set_attribute('name', machine.name) qprodata = Node.void('qprodata') rival.add_child(qprodata) qpro = other_profile.get_dict('qpro') qprodata.set_attribute('head', str(qpro.get_int('head'))) qprodata.set_attribute('hair', str(qpro.get_int('hair'))) qprodata.set_attribute('face', str(qpro.get_int('face'))) qprodata.set_attribute('body', str(qpro.get_int('body'))) qprodata.set_attribute('hand', str(qpro.get_int('hand'))) # If the user joined a particular shop, let the game know. if 'shop_location' in profile: shop_id = profile.get_int('shop_location') machine = self.get_machine_by_id(shop_id) if machine is not None: join_shop = Node.void('join_shop') root.add_child(join_shop) join_shop.set_attribute('joinflg', '1') join_shop.set_attribute('join_cflg', '1') join_shop.set_attribute('join_id', ID.format_machine_id(machine.id)) join_shop.set_attribute('join_name', machine.name) # Step up mode step_dict = profile.get_dict('step') step = Node.void('step') root.add_child(step) step.set_attribute('sp_ach', str(step_dict.get_int('sp_ach'))) step.set_attribute('dp_ach', str(step_dict.get_int('dp_ach'))) step.set_attribute('sp_hdpt', str(step_dict.get_int('sp_hdpt'))) step.set_attribute('dp_hdpt', str(step_dict.get_int('dp_hdpt'))) step.set_attribute('sp_level', str(step_dict.get_int('sp_level'))) step.set_attribute('dp_level', str(step_dict.get_int('dp_level'))) step.set_attribute('sp_round', str(step_dict.get_int('sp_round'))) step.set_attribute('dp_round', str(step_dict.get_int('dp_round'))) step.set_attribute('sp_mplay', str(step_dict.get_int('sp_mplay'))) step.set_attribute('dp_mplay', str(step_dict.get_int('dp_mplay'))) step.set_attribute('review', str(step_dict.get_int('review'))) if 'stamp' in step_dict: step.add_child(Node.binary('stamp', step_dict.get_bytes('stamp', bytes([0] * 36)))) if 'help' in step_dict: step.add_child(Node.binary('help', step_dict.get_bytes('help', bytes([0] * 6)))) # Daily recommendations entry = self.data.local.game.get_time_sensitive_settings(self.game, self.version, 'dailies') if entry is not None: packinfo = Node.void('packinfo') root.add_child(packinfo) pack_id = int(entry['start_time'] / 86400) packinfo.set_attribute('pack_id', str(pack_id)) packinfo.set_attribute('music_0', str(entry['music'][0])) packinfo.set_attribute('music_1', str(entry['music'][1])) packinfo.set_attribute('music_2', str(entry['music'][2])) else: # No dailies :( pack_id = None # Tran medals and shit achievements = Node.void('achievements') root.add_child(achievements) # Dailies if pack_id is None: achievements.set_attribute('pack', '0') achievements.set_attribute('pack_comp', '0') else: daily_played = self.data.local.user.get_achievement(self.game, self.version, userid, pack_id, 'daily') if daily_played is None: daily_played = ValidatedDict() achievements.set_attribute('pack', str(daily_played.get_int('pack_flg'))) achievements.set_attribute('pack_comp', str(daily_played.get_int('pack_comp'))) # Weeklies achievements.set_attribute('last_weekly', str(profile.get_int('last_weekly'))) achievements.set_attribute('weekly_num', str(profile.get_int('weekly_num'))) # Prefecture visit flag achievements.set_attribute('visit_flg', str(profile.get_int('visit_flg'))) # Number of rivals beaten achievements.set_attribute('rival_crush', str(profile.get_int('rival_crush'))) # Tran medals achievements.add_child(Node.s64_array('trophy', profile.get_int_array('trophy', 10))) # Link5 data if 'link5' in profile: # Don't provide link5 if we haven't saved it, so the game can # initialize it properly. link5_dict = profile.get_dict('link5') link5 = Node.void('link5') root.add_child(link5) for attr in [ 'qpro', 'glass', 'treasure', # not saved 'beautiful', 'quaver', 'castle', 'flip', 'titans', 'exusia', 'waxing', 'sampling', 'beachside', 'cuvelia', 'reunion', 'bad', 'turii', 'anisakis', 'second', 'whydidyou', 'china', 'fallen', 'broken', 'summer', 'sakura', 'wuv', 'survival', 'thunder', 'qproflg', # not saved 'glassflg', # not saved 'reflec_data', # not saved ]: link5.set_attribute(attr, str(link5_dict.get_int(attr))) # Track deller, orbs and baron commonboss = Node.void('commonboss') root.add_child(commonboss) commonboss.set_attribute('deller', str(profile.get_int('deller'))) commonboss.set_attribute('orb', str(profile.get_int('orbs'))) commonboss.set_attribute('baron', str(profile.get_int('baron'))) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: newprofile = oldprofile.clone() play_stats = self.get_play_statistics(userid) # Track play counts, DJ points and options cltype = int(request.attribute('cltype')) if cltype == self.GAME_CLTYPE_SINGLE: play_stats.increment_int('single_plays') play_stats.replace_int('single_dj_points', int(request.attribute('achi'))) newprofile.replace_int('sp_opt', int(request.attribute('opt'))) if cltype == self.GAME_CLTYPE_DOUBLE: play_stats.increment_int('double_plays') play_stats.replace_int('double_dj_points', int(request.attribute('achi'))) newprofile.replace_int('dp_opt', int(request.attribute('opt'))) newprofile.replace_int('dp_opt2', int(request.attribute('opt2'))) # Profile settings newprofile.replace_int('gno', int(request.attribute('gno'))) newprofile.replace_int('gpos', int(request.attribute('gpos'))) newprofile.replace_int('timing', int(request.attribute('timing'))) newprofile.replace_int('help', int(request.attribute('help'))) newprofile.replace_int('sdhd', int(request.attribute('sdhd'))) newprofile.replace_int('sdtype', int(request.attribute('sdtype'))) newprofile.replace_float('notes', float(request.attribute('notes'))) newprofile.replace_int('pase', int(request.attribute('pase'))) newprofile.replace_int('judge', int(request.attribute('judge'))) newprofile.replace_int('opstyle', int(request.attribute('opstyle'))) newprofile.replace_float('hispeed', float(request.attribute('hispeed'))) newprofile.replace_int('mode', int(request.attribute('mode'))) newprofile.replace_int('pmode', int(request.attribute('pmode'))) if 'lift' in request.attributes: newprofile.replace_int('lift', int(request.attribute('lift'))) # Update judge window adjustments per-machine judge_dict = newprofile.get_dict('machine_judge_adjust') machine_judge = judge_dict.get_dict(self.config.machine.pcbid) machine_judge.replace_int('adj', int(request.attribute('judgeAdj'))) judge_dict.replace_dict(self.config.machine.pcbid, machine_judge) newprofile.replace_dict('machine_judge_adjust', judge_dict) # Secret flags saving secret = request.child('secret') if secret is not None: secret_dict = newprofile.get_dict('secret') secret_dict.replace_int('flg1', secret.child_value('flg1')) secret_dict.replace_int('flg2', secret.child_value('flg2')) secret_dict.replace_int('flg3', secret.child_value('flg3')) newprofile.replace_dict('secret', secret_dict) # Basic achievements achievements = request.child('achievements') if achievements is not None: newprofile.replace_int('visit_flg', int(achievements.attribute('visit_flg'))) newprofile.replace_int('last_weekly', int(achievements.attribute('last_weekly'))) newprofile.replace_int('weekly_num', int(achievements.attribute('weekly_num'))) pack_id = int(achievements.attribute('pack_id')) if pack_id > 0: self.data.local.user.put_achievement( self.game, self.version, userid, pack_id, 'daily', { 'pack_flg': int(achievements.attribute('pack_flg')), 'pack_comp': int(achievements.attribute('pack_comp')), }, ) trophies = achievements.child('trophy') if trophies is not None: # We only load the first 10 in profile load. newprofile.replace_int_array('trophy', 10, trophies.value[:10]) # Deller and orb saving commonboss = request.child('commonboss') if commonboss is not None: newprofile.replace_int('deller', newprofile.get_int('deller') + int(commonboss.attribute('deller'))) orbs = newprofile.get_int('orbs') orbs = orbs + int(commonboss.attribute('orb')) newprofile.replace_int('orbs', orbs) # Step-up mode step = request.child('step') if step is not None: step_dict = newprofile.get_dict('step') if cltype == self.GAME_CLTYPE_SINGLE: step_dict.replace_int('sp_ach', int(step.attribute('sp_ach'))) step_dict.replace_int('sp_hdpt', int(step.attribute('sp_hdpt'))) step_dict.replace_int('sp_level', int(step.attribute('sp_level'))) step_dict.replace_int('sp_round', int(step.attribute('sp_round'))) step_dict.replace_int('sp_mplay', int(step.attribute('sp_mplay'))) else: step_dict.replace_int('dp_ach', int(step.attribute('dp_ach'))) step_dict.replace_int('dp_hdpt', int(step.attribute('dp_hdpt'))) step_dict.replace_int('dp_level', int(step.attribute('dp_level'))) step_dict.replace_int('dp_round', int(step.attribute('dp_round'))) step_dict.replace_int('dp_mplay', int(step.attribute('dp_mplay'))) step_dict.replace_int('review', int(step.attribute('review'))) newprofile.replace_dict('step', step_dict) # Link5 data link5 = request.child('link5') if link5 is not None: link5_dict = newprofile.get_dict('link5') for attr in [ 'qpro', 'glass', 'beautiful', 'quaver', 'castle', 'flip', 'titans', 'exusia', 'waxing', 'sampling', 'beachside', 'cuvelia', 'reunion', 'bad', 'turii', 'anisakis', 'second', 'whydidyou', 'china', 'fallen', 'broken', 'summer', 'sakura', 'wuv', 'survival', 'thunder', ]: link5_dict.replace_int(attr, int(link5.attribute(attr))) newprofile.replace_dict('link5', link5_dict) # Keep track of play statistics across all mixes self.update_play_statistics(userid, play_stats) return newprofile