# vim: set fileencoding=utf-8 import copy from typing import Dict, List, Optional, Tuple from bemani.backend.ddr.base import DDRBase from bemani.backend.ddr.stubs import DDRX from bemani.backend.ddr.common import ( DDRGameFriendHandler, DDRGameLockHandler, DDRGameLoadCourseHandler, DDRGameLoadHandler, DDRGameLogHandler, DDRGameMessageHandler, DDRGameNewHandler, DDRGameOldHandler, DDRGameRankingHandler, DDRGameSaveCourseHandler, DDRGameSaveHandler, DDRGameScoreHandler, DDRGameShopHandler, DDRGameTraceHandler, ) from bemani.common import Time, VersionConstants, Profile, intish from bemani.data import Score, UserID from bemani.protocol import Node class DDRX2( DDRGameFriendHandler, DDRGameLockHandler, DDRGameLoadCourseHandler, DDRGameLoadHandler, DDRGameLogHandler, DDRGameMessageHandler, DDRGameOldHandler, DDRGameNewHandler, DDRGameRankingHandler, DDRGameSaveCourseHandler, DDRGameSaveHandler, DDRGameScoreHandler, DDRGameShopHandler, DDRGameTraceHandler, DDRBase, ): name = 'DanceDanceRevolution X2' version = VersionConstants.DDR_X2 GAME_STYLE_SINGLE = 0 GAME_STYLE_DOUBLE = 1 GAME_STYLE_VERSUS = 2 GAME_RANK_AAA = 1 GAME_RANK_AA = 2 GAME_RANK_A = 3 GAME_RANK_B = 4 GAME_RANK_C = 5 GAME_RANK_D = 6 GAME_RANK_E = 7 GAME_CHART_SINGLE_BEGINNER = 0 GAME_CHART_SINGLE_BASIC = 1 GAME_CHART_SINGLE_DIFFICULT = 2 GAME_CHART_SINGLE_EXPERT = 3 GAME_CHART_SINGLE_CHALLENGE = 4 GAME_CHART_DOUBLE_BASIC = 5 GAME_CHART_DOUBLE_DIFFICULT = 6 GAME_CHART_DOUBLE_EXPERT = 7 GAME_CHART_DOUBLE_CHALLENGE = 8 GAME_HALO_NONE = 0 GAME_HALO_FULL_COMBO = 1 GAME_HALO_PERFECT_COMBO = 2 GAME_HALO_MARVELOUS_COMBO = 3 GAME_MAX_SONGS = 600 def previous_version(self) -> Optional[DDRBase]: return DDRX(self.data, self.config, self.model) def game_to_db_rank(self, game_rank: int) -> int: return { self.GAME_RANK_AAA: self.RANK_AAA, self.GAME_RANK_AA: self.RANK_AA, self.GAME_RANK_A: self.RANK_A, self.GAME_RANK_B: self.RANK_B, self.GAME_RANK_C: self.RANK_C, self.GAME_RANK_D: self.RANK_D, self.GAME_RANK_E: self.RANK_E, }[game_rank] def db_to_game_rank(self, db_rank: int) -> int: return { self.RANK_AAA: self.GAME_RANK_AAA, self.RANK_AA_PLUS: self.GAME_RANK_AA, self.RANK_AA: self.GAME_RANK_AA, self.RANK_AA_MINUS: self.GAME_RANK_A, self.RANK_A_PLUS: self.GAME_RANK_A, self.RANK_A: self.GAME_RANK_A, self.RANK_A_MINUS: self.GAME_RANK_B, self.RANK_B_PLUS: self.GAME_RANK_B, self.RANK_B: self.GAME_RANK_B, self.RANK_B_MINUS: self.GAME_RANK_C, self.RANK_C_PLUS: self.GAME_RANK_C, self.RANK_C: self.GAME_RANK_C, self.RANK_C_MINUS: self.GAME_RANK_D, self.RANK_D_PLUS: self.GAME_RANK_D, self.RANK_D: self.GAME_RANK_D, self.RANK_E: self.GAME_RANK_E, }[db_rank] def game_to_db_chart(self, game_chart: int) -> int: return { self.GAME_CHART_SINGLE_BEGINNER: self.CHART_SINGLE_BEGINNER, self.GAME_CHART_SINGLE_BASIC: self.CHART_SINGLE_BASIC, self.GAME_CHART_SINGLE_DIFFICULT: self.CHART_SINGLE_DIFFICULT, self.GAME_CHART_SINGLE_EXPERT: self.CHART_SINGLE_EXPERT, self.GAME_CHART_SINGLE_CHALLENGE: self.CHART_SINGLE_CHALLENGE, self.GAME_CHART_DOUBLE_BASIC: self.CHART_DOUBLE_BASIC, self.GAME_CHART_DOUBLE_DIFFICULT: self.CHART_DOUBLE_DIFFICULT, self.GAME_CHART_DOUBLE_EXPERT: self.CHART_DOUBLE_EXPERT, self.GAME_CHART_DOUBLE_CHALLENGE: self.CHART_DOUBLE_CHALLENGE, }[game_chart] def db_to_game_chart(self, db_chart: int) -> int: return { self.CHART_SINGLE_BEGINNER: self.GAME_CHART_SINGLE_BEGINNER, self.CHART_SINGLE_BASIC: self.GAME_CHART_SINGLE_BASIC, self.CHART_SINGLE_DIFFICULT: self.GAME_CHART_SINGLE_DIFFICULT, self.CHART_SINGLE_EXPERT: self.GAME_CHART_SINGLE_EXPERT, self.CHART_SINGLE_CHALLENGE: self.GAME_CHART_SINGLE_CHALLENGE, self.CHART_DOUBLE_BASIC: self.GAME_CHART_DOUBLE_BASIC, self.CHART_DOUBLE_DIFFICULT: self.GAME_CHART_DOUBLE_DIFFICULT, self.CHART_DOUBLE_EXPERT: self.GAME_CHART_DOUBLE_EXPERT, self.CHART_DOUBLE_CHALLENGE: self.GAME_CHART_DOUBLE_CHALLENGE, }[db_chart] def db_to_game_halo(self, db_halo: int) -> int: if db_halo == self.HALO_MARVELOUS_FULL_COMBO: combo_type = self.GAME_HALO_MARVELOUS_COMBO elif db_halo == self.HALO_PERFECT_FULL_COMBO: combo_type = self.GAME_HALO_PERFECT_COMBO elif db_halo == self.HALO_GREAT_FULL_COMBO: combo_type = self.GAME_HALO_FULL_COMBO else: combo_type = self.GAME_HALO_NONE return combo_type def handle_game_common_request(self, request: Node) -> Node: game = Node.void('game') for flagid in range(256): flag = Node.void('flag') game.add_child(flag) flag.set_attribute('id', str(flagid)) flag.set_attribute('s2', '0') flag.set_attribute('s1', '0') flag.set_attribute('t', '0') hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS) counts_by_reflink = [0] * self.GAME_MAX_SONGS for (reflink, plays) in hit_chart: if reflink >= 0 and reflink < self.GAME_MAX_SONGS: counts_by_reflink[reflink] = plays game.add_child(Node.u32_array('cnt_music', counts_by_reflink)) return game def handle_game_hiscore_request(self, request: Node) -> Node: # This is almost identical to X3 and above, except X3 added a 'code' field # that isn't present here. In the interest of correctness, keep a separate # implementation here. records = self.data.remote.music.get_all_records(self.game, self.music_version) sortedrecords: Dict[int, Dict[int, Tuple[UserID, Score]]] = {} missing_profiles = [] for (userid, score) in records: if score.id not in sortedrecords: sortedrecords[score.id] = {} sortedrecords[score.id][score.chart] = (userid, score) missing_profiles.append(userid) users = {userid: profile for (userid, profile) in self.get_any_profiles(missing_profiles)} game = Node.void('game') for song in sortedrecords: music = Node.void('music') game.add_child(music) music.set_attribute('reclink_num', str(song)) for chart in sortedrecords[song]: userid, score = sortedrecords[song][chart] try: gamechart = self.db_to_game_chart(chart) except KeyError: # Don't support this chart in this game continue gamerank = self.db_to_game_rank(score.data.get_int('rank')) combo_type = self.db_to_game_halo(score.data.get_int('halo')) typenode = Node.void('type') music.add_child(typenode) typenode.set_attribute('diff', str(gamechart)) typenode.add_child(Node.string('name', users[userid].get_str('name'))) typenode.add_child(Node.u32('score', score.points)) typenode.add_child(Node.u16('area', users[userid].get_int('area', 51))) typenode.add_child(Node.u8('rank', gamerank)) typenode.add_child(Node.u8('combo_type', combo_type)) return game def handle_game_load_m_request(self, request: Node) -> Node: extid = intish(request.attribute('code')) refid = request.attribute('refid') if extid is not None: # Rival score loading userid = self.data.remote.user.from_extid(self.game, self.version, extid) else: # Self score loading userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: scores = self.data.remote.music.get_scores(self.game, self.music_version, userid) else: scores = [] sortedscores: Dict[int, Dict[int, Score]] = {} for score in scores: if score.id not in sortedscores: sortedscores[score.id] = {} sortedscores[score.id][score.chart] = score game = Node.void('game') for song in sortedscores: music = Node.void('music') game.add_child(music) music.set_attribute('reclink', str(song)) for chart in sortedscores[song]: score = sortedscores[song][chart] try: gamechart = self.db_to_game_chart(chart) except KeyError: # Don't support this chart in this game continue gamerank = self.db_to_game_rank(score.data.get_int('rank')) combo_type = self.db_to_game_halo(score.data.get_int('halo')) typenode = Node.void('type') music.add_child(typenode) typenode.set_attribute('diff', str(gamechart)) typenode.add_child(Node.u32('score', score.points)) typenode.add_child(Node.u16('count', score.plays)) typenode.add_child(Node.u8('rank', gamerank)) typenode.add_child(Node.u8('combo_type', combo_type)) return game def handle_game_save_m_request(self, request: Node) -> Node: refid = request.attribute('refid') songid = int(request.attribute('mid')) chart = self.game_to_db_chart(int(request.attribute('mtype'))) # Calculate statistics data = request.child('data') points = int(data.attribute('score')) combo = int(data.attribute('combo')) rank = self.game_to_db_rank(int(data.attribute('rank'))) if int(data.attribute('full')) == 0: halo = self.HALO_NONE elif int(data.attribute('perf')) == 0: halo = self.HALO_GREAT_FULL_COMBO elif points < 1000000: halo = self.HALO_PERFECT_FULL_COMBO else: halo = self.HALO_MARVELOUS_FULL_COMBO trace = request.child_value('trace') # Save the score, regardless of whether we have a refid. If we save # an anonymous score, it only goes into the DB to count against the # number of plays for that song/chart. userid = self.data.remote.user.from_refid(self.game, self.version, refid) self.update_score( userid, songid, chart, points, rank, halo, combo, trace, ) # No response needed game = Node.void('game') return game def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void('game') # Look up play stats we bridge to every mix play_stats = self.get_play_statistics(userid) # Basic game settings root.add_child(Node.string('seq', '')) root.add_child(Node.u32('code', profile.extid)) root.add_child(Node.string('name', profile.get_str('name'))) root.add_child(Node.u8('area', profile.get_int('area', 51))) root.add_child(Node.u32('cnt_s', play_stats.get_int('single_plays'))) root.add_child(Node.u32('cnt_d', play_stats.get_int('double_plays'))) root.add_child(Node.u32('cnt_b', play_stats.get_int('battle_plays'))) # This could be wrong, its a guess root.add_child(Node.u32('cnt_m0', play_stats.get_int('cnt_m0'))) root.add_child(Node.u32('cnt_m1', play_stats.get_int('cnt_m1'))) root.add_child(Node.u32('cnt_m2', play_stats.get_int('cnt_m2'))) root.add_child(Node.u32('cnt_m3', play_stats.get_int('cnt_m3'))) root.add_child(Node.u32('exp', play_stats.get_int('exp'))) root.add_child(Node.u32('exp_o', profile.get_int('exp_o'))) root.add_child(Node.u32('star', profile.get_int('star'))) root.add_child(Node.u32('star_c', profile.get_int('star_c'))) root.add_child(Node.u8('combo', profile.get_int('combo', 0))) root.add_child(Node.u8('timing_diff', profile.get_int('early_late', 0))) # Character stuff chara = Node.void('chara') root.add_child(chara) if 'chara' in profile: chara.set_attribute('my', str(profile.get_int('chara'))) root.add_child(Node.u8_array('chara_opt', profile.get_int_array('chara_opt', 96))) # Drill rankings if 'title' in profile: title = Node.void('title') root.add_child(title) titledict = profile.get_dict('title') if 't' in titledict: title.set_attribute('t', str(titledict.get_int('t'))) if 's' in titledict: title.set_attribute('s', str(titledict.get_int('s'))) if 'd' in titledict: title.set_attribute('d', str(titledict.get_int('d'))) if 'title_gr' in profile: title_gr = Node.void('title_gr') root.add_child(title_gr) title_grdict = profile.get_dict('title_gr') if 't' in title_grdict: title_gr.set_attribute('t', str(title_grdict.get_int('t'))) if 's' in title_grdict: title_gr.set_attribute('s', str(title_grdict.get_int('s'))) if 'd' in title_grdict: title_gr.set_attribute('d', str(title_grdict.get_int('d'))) # Event progrses if 'event' in profile: event = Node.void('event') root.add_child(event) event_dict = profile.get_dict('event') if 'diff_sum' in event_dict: event.set_attribute('diff_sum', str(event_dict.get_int('diff_sum'))) if 'welcome' in event_dict: event.set_attribute('welcome', str(event_dict.get_int('welcome'))) if 'e_flags' in event_dict: event.set_attribute('e_flags', str(event_dict.get_int('e_flags'))) if 'e_panel' in profile: e_panel = Node.void('e_panel') root.add_child(e_panel) e_panel_dict = profile.get_dict('e_panel') if 'play_id' in e_panel_dict: e_panel.set_attribute('play_id', str(e_panel_dict.get_int('play_id'))) e_panel.add_child(Node.u8_array('cell', e_panel_dict.get_int_array('cell', 24))) e_panel.add_child(Node.u8_array('panel_state', e_panel_dict.get_int_array('panel_state', 6))) if 'e_pix' in profile: e_pix = Node.void('e_pix') root.add_child(e_pix) e_pix_dict = profile.get_dict('e_pix') if 'max_distance' in e_pix_dict: e_pix.set_attribute('max_distance', str(e_pix_dict.get_int('max_distance'))) if 'max_planet' in e_pix_dict: e_pix.set_attribute('max_planet', str(e_pix_dict.get_int('max_planet'))) if 'total_distance' in e_pix_dict: e_pix.set_attribute('total_distance', str(e_pix_dict.get_int('total_distance'))) if 'total_planet' in e_pix_dict: e_pix.set_attribute('total_planet', str(e_pix_dict.get_int('total_planet'))) if 'border_character' in e_pix_dict: e_pix.set_attribute('border_character', str(e_pix_dict.get_int('border_character'))) if 'border_balloon' in e_pix_dict: e_pix.set_attribute('border_balloon', str(e_pix_dict.get_int('border_balloon'))) if 'border_music_aftr' in e_pix_dict: e_pix.set_attribute('border_music_aftr', str(e_pix_dict.get_int('border_music_aftr'))) if 'border_music_meii' in e_pix_dict: e_pix.set_attribute('border_music_meii', str(e_pix_dict.get_int('border_music_meii'))) if 'border_music_dirt' in e_pix_dict: e_pix.set_attribute('border_music_dirt', str(e_pix_dict.get_int('border_music_dirt'))) if 'flags' in e_pix_dict: e_pix.set_attribute('flags', str(e_pix_dict.get_int('flags'))) # Calorie mode if 'weight' in profile: workouts = self.data.local.user.get_time_based_achievements( self.game, self.version, userid, achievementtype='workout', since=Time.now() - Time.SECONDS_IN_DAY, ) total = sum([w.data.get_int('calories') for w in workouts]) workout = Node.void('workout') root.add_child(workout) workout.set_attribute('weight', str(profile.get_int('weight'))) workout.set_attribute('day', str(total)) workout.set_attribute('disp', '1') # Last cursor settings last = Node.void('last') root.add_child(last) lastdict = profile.get_dict('last') last.set_attribute('fri', str(lastdict.get_int('fri'))) last.set_attribute('style', str(lastdict.get_int('style'))) last.set_attribute('mode', str(lastdict.get_int('mode'))) last.set_attribute('cate', str(lastdict.get_int('cate'))) last.set_attribute('sort', str(lastdict.get_int('sort'))) last.set_attribute('mid', str(lastdict.get_int('mid'))) last.set_attribute('mtype', str(lastdict.get_int('mtype'))) last.set_attribute('cid', str(lastdict.get_int('cid'))) last.set_attribute('ctype', str(lastdict.get_int('ctype'))) last.set_attribute('sid', str(lastdict.get_int('sid'))) # Groove gauge level-ups gr_s = Node.void('gr_s') root.add_child(gr_s) index = 1 for entry in profile.get_int_array('gr_s', 5): gr_s.set_attribute(f'gr{index}', str(entry)) index = index + 1 gr_d = Node.void('gr_d') root.add_child(gr_d) index = 1 for entry in profile.get_int_array('gr_d', 5): gr_d.set_attribute(f'gr{index}', str(entry)) index = index + 1 # Options in menus root.add_child(Node.s16_array('opt', profile.get_int_array('opt', 16))) root.add_child(Node.s16_array('opt_ex', profile.get_int_array('opt_ex', 16))) # Unlock flags root.add_child(Node.u8_array('flag', profile.get_int_array('flag', 256, [1] * 256))) # Ranking display? root.add_child(Node.u16_array('rank', profile.get_int_array('rank', 100))) # Rivals links = self.data.local.user.get_links(self.game, self.version, userid) for link in links: if link.type[:7] != 'friend_': continue pos = int(link.type[7:]) friend = self.get_profile(link.other_userid) play_stats = self.get_play_statistics(link.other_userid) if friend is not None: friendnode = Node.void('friend') root.add_child(friendnode) friendnode.set_attribute('pos', str(pos)) friendnode.set_attribute('vs', '0') friendnode.set_attribute('up', '0') friendnode.add_child(Node.u32('code', friend.extid)) friendnode.add_child(Node.string('name', friend.get_str('name'))) friendnode.add_child(Node.u8('area', friend.get_int('area', 51))) friendnode.add_child(Node.u32('exp', play_stats.get_int('exp'))) friendnode.add_child(Node.u32('star', friend.get_int('star'))) # Drill rankings if 'title' in friend: title = Node.void('title') friendnode.add_child(title) titledict = friend.get_dict('title') if 't' in titledict: title.set_attribute('t', str(titledict.get_int('t'))) if 's' in titledict: title.set_attribute('s', str(titledict.get_int('s'))) if 'd' in titledict: title.set_attribute('d', str(titledict.get_int('d'))) if 'title_gr' in friend: title_gr = Node.void('title_gr') friendnode.add_child(title_gr) title_grdict = friend.get_dict('title_gr') if 't' in title_grdict: title_gr.set_attribute('t', str(title_grdict.get_int('t'))) if 's' in title_grdict: title_gr.set_attribute('s', str(title_grdict.get_int('s'))) if 'd' in title_grdict: title_gr.set_attribute('d', str(title_grdict.get_int('d'))) # Groove gauge level-ups gr_s = Node.void('gr_s') friendnode.add_child(gr_s) index = 1 for entry in friend.get_int_array('gr_s', 5): gr_s.set_attribute(f'gr{index}', str(entry)) index = index + 1 gr_d = Node.void('gr_d') friendnode.add_child(gr_d) index = 1 for entry in friend.get_int_array('gr_d', 5): gr_d.set_attribute(f'gr{index}', str(entry)) index = index + 1 return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: newprofile = copy.deepcopy(oldprofile) play_stats = self.get_play_statistics(userid) # Grab last node and accessories so we can make decisions based on type last = request.child('last') lastdict = newprofile.get_dict('last') mode = int(last.attribute('mode')) style = int(last.attribute('style')) is_dp = style == self.GAME_STYLE_DOUBLE # Drill rankings title = request.child('title') title_gr = request.child('title_gr') titledict = newprofile.get_dict('title') title_grdict = newprofile.get_dict('title_gr') # Groove radar level ups gr = request.child('gr') # Set the correct values depending on if we're single or double play if is_dp: play_stats.increment_int('double_plays') if gr is not None: newprofile.replace_int_array( 'gr_d', 5, [ intish(gr.attribute('gr1')), intish(gr.attribute('gr2')), intish(gr.attribute('gr3')), intish(gr.attribute('gr4')), intish(gr.attribute('gr5')), ], ) if title is not None: titledict.replace_int('d', title.value) newprofile.replace_dict('title', titledict) if title_gr is not None: title_grdict.replace_int('d', title.value) newprofile.replace_dict('title_gr', title_grdict) else: play_stats.increment_int('single_plays') if gr is not None: newprofile.replace_int_array( 'gr_s', 5, [ intish(gr.attribute('gr1')), intish(gr.attribute('gr2')), intish(gr.attribute('gr3')), intish(gr.attribute('gr4')), intish(gr.attribute('gr5')), ], ) if title is not None: titledict.replace_int('s', title.value) newprofile.replace_dict('title', titledict) if title_gr is not None: title_grdict.replace_int('s', title.value) newprofile.replace_dict('title_gr', title_grdict) play_stats.increment_int(f'cnt_m{mode}') # Update last attributes lastdict.replace_int('fri', intish(last.attribute('fri'))) lastdict.replace_int('style', intish(last.attribute('style'))) lastdict.replace_int('mode', intish(last.attribute('mode'))) lastdict.replace_int('cate', intish(last.attribute('cate'))) lastdict.replace_int('sort', intish(last.attribute('sort'))) lastdict.replace_int('mid', intish(last.attribute('mid'))) lastdict.replace_int('mtype', intish(last.attribute('mtype'))) lastdict.replace_int('cid', intish(last.attribute('cid'))) lastdict.replace_int('ctype', intish(last.attribute('ctype'))) lastdict.replace_int('sid', intish(last.attribute('sid'))) newprofile.replace_dict('last', lastdict) # Grab character options chara = request.child('chara') if chara is not None: newprofile.replace_int('chara', intish(chara.attribute('my'))) newprofile.replace_int_array('chara_opt', 96, request.child_value('chara_opt')) # Track event progress event = request.child('event') if event is not None: event_dict = newprofile.get_dict('event') event_dict.replace_int('diff_sum', intish(event.attribute('diff_sum'))) event_dict.replace_int('e_flags', intish(event.attribute('e_flags'))) event_dict.replace_int('welcome', intish(event.attribute('welcome'))) newprofile.replace_dict('event', event_dict) e_panel = request.child('e_panel') if e_panel is not None: e_panel_dict = newprofile.get_dict('e_panel') e_panel_dict.replace_int('play_id', intish(e_panel.attribute('play_id'))) e_panel_dict.replace_int_array('cell', 24, e_panel.child_value('cell')) e_panel_dict.replace_int_array('panel_state', 6, e_panel.child_value('panel_state')) newprofile.replace_dict('e_panel', e_panel_dict) e_pix = request.child('e_pix') if e_pix is not None: e_pix_dict = newprofile.get_dict('e_pix') max_distance = e_pix_dict.get_int('max_distance') max_planet = e_pix_dict.get_int('max_planet') total_distance = e_pix_dict.get_int('total_distance') total_planet = e_pix_dict.get_int('total_planet') cur_distance = intish(e_pix.attribute('this_distance')) cur_planet = intish(e_pix.attribute('this_planet')) if cur_distance is not None: max_distance = max(max_distance, cur_distance) total_distance += cur_distance if cur_planet is not None: max_planet = max(max_planet, cur_planet) total_planet += cur_planet e_pix_dict.replace_int('max_distance', max_distance) e_pix_dict.replace_int('max_planet', max_planet) e_pix_dict.replace_int('total_distance', total_distance) e_pix_dict.replace_int('total_planet', total_planet) e_pix_dict.replace_int('flags', intish(e_pix.attribute('flags'))) newprofile.replace_dict('e_pix', e_pix_dict) # Options opt = request.child('opt') if opt is not None: # A bug in old versions of AVS returns the wrong number for set newprofile.replace_int_array('opt', 16, opt.value[:16]) # Experience and stars exp = request.child_value('exp') if exp is not None: play_stats.replace_int('exp', play_stats.get_int('exp') + exp) star = request.child_value('star') if star is not None: newprofile.replace_int('star', newprofile.get_int('star') + star) star_c = request.child_value('star_c') if star_c is not None: newprofile.replace_int('star_c', newprofile.get_int('star_c') + exp) # Update game flags for child in request.children: if child.name != 'flag': continue try: value = int(child.attribute('data')) offset = int(child.attribute('no')) except ValueError: continue flags = newprofile.get_int_array('flag', 256, [1] * 256) if offset < 0 or offset >= len(flags): continue flags[offset] = value newprofile.replace_int_array('flag', 256, flags) # Workout mode support newweight = -1 oldweight = newprofile.get_int('weight') for child in request.children: if child.name != 'weight': continue newweight = child.value if newweight < 0: newweight = oldweight # Either update or unset the weight depending on the game if newweight == 0: # Weight is unset or we declined to use this feature, remove from profile if 'weight' in newprofile: del newprofile['weight'] else: # Weight has been set or previously retrieved, we should save calories newprofile.replace_int('weight', newweight) total = 0 for child in request.children: if child.name != 'calory': continue total += child.value self.data.local.user.put_time_based_achievement( self.game, self.version, userid, 0, 'workout', { 'calories': total, 'weight': newweight, }, ) # Look up old friends oldfriends: List[Optional[UserID]] = [None] * 10 links = self.data.local.user.get_links(self.game, self.version, userid) for link in links: if link.type[:7] != 'friend_': continue pos = int(link.type[7:]) oldfriends[pos] = link.other_userid # Save any rivals that were added/removed/changed newfriends = oldfriends[:] for child in request.children: if child.name != 'friend': continue code = int(child.attribute('code')) pos = int(child.attribute('pos')) if pos >= 0 and pos < 10: if code == 0: # We cleared this friend newfriends[pos] = None else: # Try looking up the userid newfriends[pos] = self.data.remote.user.from_extid(self.game, self.version, code) # Diff the set of links to determine updates for i in range(10): if newfriends[i] == oldfriends[i]: continue if newfriends[i] is None: # Kill the rival in this location self.data.local.user.destroy_link( self.game, self.version, userid, f'friend_{i}', oldfriends[i], ) elif oldfriends[i] is None: # Add rival in this location self.data.local.user.put_link( self.game, self.version, userid, f'friend_{i}', newfriends[i], {}, ) else: # Changed the rival here, kill the old one, add the new one self.data.local.user.destroy_link( self.game, self.version, userid, f'friend_{i}', oldfriends[i], ) self.data.local.user.put_link( self.game, self.version, userid, f'friend_{i}', newfriends[i], {}, ) # Keep track of play statistics self.update_play_statistics(userid, play_stats) return newprofile