def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict: newprofile = copy.deepcopy(oldprofile) # Update experience and in-game currencies earned_gamecoin_packet = request.child_value('earned_gamecoin_packet') if earned_gamecoin_packet is not None: newprofile.replace_int('packet', newprofile.get_int('packet') + earned_gamecoin_packet) earned_gamecoin_block = request.child_value('earned_gamecoin_block') if earned_gamecoin_block is not None: newprofile.replace_int('block', newprofile.get_int('block') + earned_gamecoin_block) gain_exp = request.child_value('gain_exp') if gain_exp is not None: newprofile.replace_int('exp', newprofile.get_int('exp') + gain_exp) # Miscelaneous stuff newprofile.replace_int('m_user_cnt', request.child_value('m_user_cnt')) # Update user's unlock status if we aren't force unlocked game_config = self.get_game_config() if not game_config.get_bool('force_unlock_cards'): have_item = request.child_value('have_item') if have_item is not None: newprofile.replace_int_array('have_item', 512, [1 if x else 0 for x in have_item]) if not game_config.get_bool('force_unlock_songs'): have_note = request.child_value('have_note') if have_note is not None: newprofile.replace_int_array('have_note', 512, [1 if x else 0 for x in have_note]) # Grab last information. lastdict = newprofile.get_dict('last') lastdict.replace_int('headphone', request.child_value('headphone')) lastdict.replace_int('hispeed', request.child_value('hispeed')) lastdict.replace_int('appeal_id', request.child_value('appeal_id')) lastdict.replace_int('frame0', request.child_value('frame0')) lastdict.replace_int('frame1', request.child_value('frame1')) lastdict.replace_int('frame2', request.child_value('frame2')) lastdict.replace_int('frame3', request.child_value('frame3')) lastdict.replace_int('frame4', request.child_value('frame4')) last = request.child('last') if last is not None: lastdict.replace_int('music_id', intish(last.attribute('music_id'))) lastdict.replace_int('music_type', intish(last.attribute('music_type'))) lastdict.replace_int('sort_type', intish(last.attribute('sort_type'))) # Save back last information gleaned from results newprofile.replace_dict('last', lastdict) # Keep track of play statistics self.update_play_statistics(userid) return newprofile
def handle_game_load_c_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) coursedata = [0] * 3200 if userid is not None: for course in self.data.local.user.get_achievements(self.game, self.version, userid): if course.type != 'course': continue # Grab course ID and chart (kinda pointless because we add it right back up # below, but it is more documented/readable this way. courseid = int(course.id / 4) coursechart = course.id % 4 # Populate course data index = ((courseid * 4) + coursechart) * 8 if index >= 0 and index <= (len(coursedata) - 8): coursedata[index + 0] = int(course.data.get_int('score') / 10000) coursedata[index + 1] = course.data.get_int('score') % 10000 coursedata[index + 2] = course.data.get_int('combo') coursedata[index + 3] = self.db_to_game_rank(course.data.get_int('rank')) coursedata[index + 5] = course.data.get_int('stage') coursedata[index + 6] = course.data.get_int('combo_type') game = Node.void('game') game.add_child(Node.u16_array('course', coursedata)) return game
def handle_game_trace_request(self, request: Node) -> Node: # This is almost identical to 2013 and below, except it will never # even try to request course traces, so we fork from common functionality. extid = int(request.attribute('code')) chart = int(request.attribute('type')) mid = intish(request.attribute('mid')) # Base packet is just game, if we find something we add to it game = Node.void('game') # Rival trace loading userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is None: # Nothing to load return game # Load trace from song score songscore = self.data.remote.music.get_score( self.game, self.music_version, userid, mid, self.game_to_db_chart(chart), ) if songscore is not None and 'trace' in songscore.data: game.add_child(Node.u32('size', len(songscore.data['trace']))) game.add_child(Node.u8_array('trace', songscore.data['trace'])) return game
def handle_game_trace_request(self, request: Node) -> Node: extid = int(request.attribute('code')) chart = int(request.attribute('type')) cid = intish(request.attribute('cid')) mid = intish(request.attribute('mid')) # Base packet is just game, if we find something we add to it game = Node.void('game') # Rival trace loading userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is None: # Nothing to load return game if mid is not None: # Load trace from song score songscore = self.data.remote.music.get_score( self.game, self.music_version, userid, mid, self.game_to_db_chart(chart), ) if songscore is not None and 'trace' in songscore.data: game.add_child(Node.u32('size', len(songscore.data['trace']))) game.add_child(Node.u8_array('trace', songscore.data['trace'])) elif cid is not None: # Load trace from achievement coursescore = self.data.local.user.get_achievement( self.game, self.version, userid, (cid * 4) + chart, 'course', ) if coursescore is not None and 'trace' in coursescore: game.add_child(Node.u32('size', len(coursescore['trace']))) game.add_child(Node.u8_array('trace', coursescore['trace'])) # Nothing found, return empty 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)) # The game optionally receives hard, life8, life4, risky, assist_clear, normal_clear # u8 values too, and saves music scores with these set, but the UI doesn't appear to # do anything with them, so we don't care. return game
def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node: playerdata = Node.void('playerdata') refid = request.child_value('data/refid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: profile = self.get_profile(userid) or ValidatedDict() usergamedata = profile.get_dict('usergamedata') for record in request.child('data/record').children: if record.name != 'd': continue strdata = base64.b64decode(record.value) bindata = base64.b64decode(record.child_value('bin1')) # Grab and format the profile objects strdatalist = strdata.split(b',') profiletype = strdatalist[1].decode('utf-8') strdatalist = strdatalist[2:] # Extract relevant bits for frontend/API if profiletype == 'COMMON': profile.replace_str('name', strdatalist[self.GAME_COMMON_NAME_OFFSET].decode('ascii')) profile.replace_int('area', intish(strdatalist[self.GAME_COMMON_AREA_OFFSET].decode('ascii'), 16)) profile.replace_bool('workout_mode', int(strdatalist[self.GAME_COMMON_WEIGHT_DISPLAY_OFFSET].decode('ascii'), 16) != 0) profile.replace_int('weight', int(float(strdatalist[self.GAME_COMMON_WEIGHT_OFFSET].decode('ascii')) * 10)) profile.replace_int('character', int(strdatalist[self.GAME_COMMON_CHARACTER_OFFSET].decode('ascii'), 16)) if profiletype == 'OPTION': profile.replace_int('combo', int(strdatalist[self.GAME_OPTION_COMBO_POSITION_OFFSET].decode('ascii'), 16)) profile.replace_int('early_late', int(strdatalist[self.GAME_OPTION_FAST_SLOW_OFFSET].decode('ascii'), 16)) profile.replace_int('arrowskin', int(strdatalist[self.GAME_OPTION_ARROW_SKIN_OFFSET].decode('ascii'), 16)) profile.replace_int('guidelines', int(strdatalist[self.GAME_OPTION_GUIDELINE_OFFSET].decode('ascii'), 16)) profile.replace_int('filter', int(strdatalist[self.GAME_OPTION_FILTER_OFFSET].decode('ascii'), 16)) usergamedata[profiletype] = { 'strdata': b','.join(strdatalist), 'bindata': bindata, } profile.replace_dict('usergamedata', usergamedata) self.put_profile(userid, profile) playerdata.add_child(Node.s32('result', 0)) return playerdata
def handle_game_load_daily_request(self, request: Node) -> Node: extid = intish(request.attribute('code')) refid = request.attribute('refid') game = Node.void('game') profiledict = None if extid is not None: # Rival daily loading userid = self.data.remote.user.from_extid(self.game, self.version, extid) else: # Self daily loading userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: profiledict = self.get_profile(userid) if profiledict is not None: play_stats = self.get_play_statistics(userid) # Day play counts last_play_date = play_stats.get_int_array('last_play_date', 3) today_play_date = Time.todays_date() if ( last_play_date[0] == today_play_date[0] and last_play_date[1] == today_play_date[1] and last_play_date[2] == today_play_date[2] ): today_count = play_stats.get_int('today_plays', 0) else: today_count = 0 daycount = Node.void('daycount') game.add_child(daycount) daycount.set_attribute('playcount', str(today_count)) # Daily combo stuff, unclear how this works dailycombo = Node.void('dailycombo') game.add_child(dailycombo) dailycombo.set_attribute('daily_combo', str(0)) dailycombo.set_attribute('daily_combo_lv', str(0)) return game
def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict: 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}') # Result stars result_star = request.child('result_star') if result_star is not None: newprofile.replace_int_array( 'result_stars', 9, [ intish(result_star.attribute('slot1')), intish(result_star.attribute('slot2')), intish(result_star.attribute('slot3')), intish(result_star.attribute('slot4')), intish(result_star.attribute('slot5')), intish(result_star.attribute('slot6')), intish(result_star.attribute('slot7')), intish(result_star.attribute('slot8')), intish(result_star.attribute('slot9')), ], ) # Target stuff target = request.child('target') if target is not None: newprofile.replace_int('target_flag', intish(target.attribute('flag'))) newprofile.replace_int('target_setnum', intish(target.attribute('setnum'))) # Update last attributes lastdict.replace_int('rival1', intish(last.attribute('rival1'))) lastdict.replace_int('rival2', intish(last.attribute('rival2'))) lastdict.replace_int('rival3', intish(last.attribute('rival3'))) 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'))) chara_opt = request.child('chara_opt') if chara_opt is not None: # A bug in old versions of AVS returns the wrong number for set newprofile.replace_int_array('chara_opt', 96, chara_opt.value[:96]) # 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], {}, ) # Play area counter shop_area = int(request.attribute('shop_area')) if shop_area >= 0 and shop_area < 55: areas = newprofile.get_int_array('play_area', 55) areas[shop_area] = areas[shop_area] + 1 newprofile.replace_int_array('play_area', 55, areas) # Keep track of play statistics self.update_play_statistics(userid, play_stats) return newprofile
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) old_scores = [ score for score in self.data.local.user.get_achievements(self.game, self.music_version, userid) if score.type == '2ndmix' ] else: scores = [] old_scores = [] sortedscores: Dict[int, Dict[int, Dict[str, Union[Score, Achievement]]]] = {} for score in scores: if score.id not in sortedscores: sortedscores[score.id] = {} if score.chart not in sortedscores[score.id]: sortedscores[score.id][score.chart] = {} sortedscores[score.id][score.chart]['score'] = score for oldscore in old_scores: songid = int(oldscore.id / 100) chart = int(oldscore.id % 100) if songid not in sortedscores: sortedscores[songid] = {} if chart not in sortedscores[songid]: sortedscores[songid][chart] = {} sortedscores[songid][chart]['oldscore'] = oldscore 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]: try: gamechart = self.db_to_game_chart(chart) except KeyError: # Don't support this chart in this game continue scoredict = sortedscores[song][chart] if 'score' in scoredict: # We played the normal version of this song gamerank = self.db_to_game_rank(scoredict['score'].data.get_int('rank')) combo_type = self.db_to_game_halo(scoredict['score'].data.get_int('halo')) points = scoredict['score'].points # type: ignore plays = scoredict['score'].plays # type: ignore else: # We only played 2nd mix version of this song gamerank = 0 combo_type = self.GAME_HALO_NONE points = 0 plays = 0 if 'oldscore' in scoredict: # We played the 2nd mix version of this song oldpoints = scoredict['oldscore'].data.get_int('points') oldrank = scoredict['oldscore'].data.get_int('rank') oldplays = scoredict['oldscore'].data.get_int('plays') else: oldpoints = 0 oldrank = 0 oldplays = 0 typenode = Node.void('type') music.add_child(typenode) typenode.set_attribute('diff', str(gamechart)) typenode.add_child(Node.u32('score', points)) typenode.add_child(Node.u16('count', plays)) typenode.add_child(Node.u8('rank', gamerank)) typenode.add_child(Node.u8('combo_type', combo_type)) typenode.add_child(Node.u32('score_2nd', oldpoints)) typenode.add_child(Node.u8('rank_2nd', oldrank)) typenode.add_child(Node.u16('cnt_2nd', oldplays)) return game
def handle_game_friend_request(self, request: Node) -> Node: extid = intish(request.attribute('code')) userid = None friend = None if extid is not None: # Rival score loading userid = self.data.remote.user.from_extid(self.game, self.version, extid) if userid is not None: friend = self.get_profile(userid) play_stats = self.get_play_statistics(userid) if friend is None: # Return an empty node to tell the game we don't have a player here game = Node.void('game') return game game = Node.void('game') game.set_attribute('data', '1') game.add_child(Node.u32('code', friend.get_int('extid'))) game.add_child(Node.string('name', friend.get_str('name'))) game.add_child(Node.u8('area', friend.get_int('area', 51))) game.add_child(Node.u32('exp', play_stats.get_int('exp'))) game.add_child(Node.u32('star', friend.get_int('star'))) # Drill rankings if 'title' in friend: title = Node.void('title') game.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') game.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') game.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') game.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 game