def update_score( self, userid: UserID, songid: int, chart: int, points: int, achievement_rate: int, clear_type: int, combo_type: int, miss_count: int, combo: Optional[int] = None, stats: Optional[Dict[str, int]] = None, param: Optional[int] = None, kflag: Optional[int] = None, ) -> None: """ Given various pieces of a score, update the user's high score and score history in a controlled manner, so all games in Reflec series can expect the same attributes in a score. Note that the clear_types passed here are expected to be converted from game identifier to our internal identifier, so that any game in the series may convert them back. """ # Range check clear type if clear_type not in [ self.CLEAR_TYPE_NO_PLAY, self.CLEAR_TYPE_FAILED, self.CLEAR_TYPE_CLEARED, self.CLEAR_TYPE_HARD_CLEARED, self.CLEAR_TYPE_S_HARD_CLEARED, ]: raise Exception(f"Invalid clear_type value {clear_type}") # Range check combo type if combo_type not in [ self.COMBO_TYPE_NONE, self.COMBO_TYPE_ALMOST_COMBO, self.COMBO_TYPE_FULL_COMBO, self.COMBO_TYPE_FULL_COMBO_ALL_JUST, ]: raise Exception(f"Invalid combo_type value {combo_type}") oldscore = self.data.local.music.get_score( self.game, self.version, userid, songid, chart, ) # Score history is verbatum, instead of highest score now = Time.now() history = ValidatedDict({}) oldpoints = points if oldscore is None: # If it is a new score, create a new dictionary to add to scoredata = ValidatedDict({}) highscore = True else: # Set the score to any new record achieved highscore = points >= oldscore.points points = max(points, oldscore.points) scoredata = oldscore.data # Update the last played time scoredata.replace_int('last_played_time', now) # Replace clear type with highest value and timestamps if clear_type >= scoredata.get_int('clear_type'): scoredata.replace_int( 'clear_type', max(scoredata.get_int('clear_type'), clear_type)) scoredata.replace_int('best_clear_type_time', now) history.replace_int('clear_type', clear_type) # Replace combo type with highest value and timestamps if combo_type >= scoredata.get_int('combo_type'): scoredata.replace_int( 'combo_type', max(scoredata.get_int('combo_type'), combo_type)) scoredata.replace_int('best_clear_type_time', now) history.replace_int('combo_type', combo_type) # Update the combo for this song if combo is not None: scoredata.replace_int('combo', max(scoredata.get_int('combo'), combo)) history.replace_int('combo', combo) # Update the param for this song if param is not None: scoredata.replace_int('param', max(scoredata.get_int('param'), param)) history.replace_int('param', param) # Update the kflag for this song if kflag is not None: scoredata.replace_int('kflag', max(scoredata.get_int('kflag'), kflag)) history.replace_int('kflag', kflag) # Update win/lost/draw stats for this song if stats is not None: scoredata.replace_dict('stats', stats) history.replace_dict('stats', stats) # Update the achievement rate with timestamps if achievement_rate >= scoredata.get_int('achievement_rate'): scoredata.replace_int( 'achievement_rate', max(scoredata.get_int('achievement_rate'), achievement_rate)) scoredata.replace_int('best_achievement_rate_time', now) history.replace_int('achievement_rate', achievement_rate) # Update the miss count with timestamps, either if it was lowered, or if the old value was blank. # If the new value is -1 (we didn't get a miss count this time), never update the old value. if miss_count >= 0: if miss_count <= scoredata.get_int( 'miss_count', 999999) or scoredata.get_int('miss_count') == -1: scoredata.replace_int( 'miss_count', min(scoredata.get_int('miss_count', 999999), miss_count)) scoredata.replace_int('best_miss_count_time', now) history.replace_int('miss_count', miss_count) # Look up where this score was earned lid = self.get_machine_id() # Reflec Beat happens to send all songs that were played by a player # at the end of the round. It sends timestamps for the songs, but as of # Colette they were identical for each song in the round. So, if a user # plays the same song/chart# more than once in a round, we will end up # failing to store the attempt since we don't allow two of the same # attempt at the same time for the same user and song/chart. So, bump # the timestamp by one second and retry well past the maximum number of # songs. for bump in range(10): timestamp = now + bump # Write the new score back self.data.local.music.put_score( self.game, self.version, userid, songid, chart, lid, points, scoredata, highscore, timestamp=timestamp, ) try: # Save the history of this score too self.data.local.music.put_attempt( self.game, self.version, userid, songid, chart, lid, oldpoints, history, highscore, timestamp=timestamp, ) except ScoreSaveException: # Try again one second in the future continue # We saved successfully break
def format_scores(self, userid: UserID, profile: ValidatedDict, scores: List[Score]) -> Node: scores = self.data.remote.music.get_scores(self.game, self.version, userid) root = Node.void('gametop') datanode = Node.void('data') root.add_child(datanode) player = Node.void('player') datanode.add_child(player) playdata = Node.void('playdata') player.add_child(playdata) playdata.set_attribute('count', str(len(scores))) music = ValidatedDict() for score in scores: data = music.get_dict(str(score.id)) play_cnt = data.get_int_array('play_cnt', 3) clear_cnt = data.get_int_array('clear_cnt', 3) clear_flags = data.get_int_array('clear_flags', 3) fc_cnt = data.get_int_array('fc_cnt', 3) ex_cnt = data.get_int_array('ex_cnt', 3) points = data.get_int_array('points', 3) # Replace data for this chart type play_cnt[score.chart] = score.plays clear_cnt[score.chart] = score.data.get_int('clear_count') fc_cnt[score.chart] = score.data.get_int('full_combo_count') ex_cnt[score.chart] = score.data.get_int('excellent_count') points[score.chart] = score.points # Format the clear flags clear_flags[score.chart] = self.GAME_FLAG_BIT_PLAYED if score.data.get_int('clear_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_CLEARED if score.data.get_int('full_combo_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_FULL_COMBO if score.data.get_int('excellent_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_EXCELLENT # Save chart data back data.replace_int_array('play_cnt', 3, play_cnt) data.replace_int_array('clear_cnt', 3, clear_cnt) data.replace_int_array('clear_flags', 3, clear_flags) data.replace_int_array('fc_cnt', 3, fc_cnt) data.replace_int_array('ex_cnt', 3, ex_cnt) data.replace_int_array('points', 3, points) # Update the ghost (untyped) ghost = data.get('ghost', [None, None, None]) ghost[score.chart] = score.data.get('ghost') data['ghost'] = ghost # Save it back music.replace_dict(str(score.id), data) for scoreid in music: scoredata = music[scoreid] musicdata = Node.void('musicdata') playdata.add_child(musicdata) musicdata.set_attribute('music_id', scoreid) musicdata.add_child(Node.s32_array('play_cnt', scoredata.get_int_array('play_cnt', 3))) musicdata.add_child(Node.s32_array('clear_cnt', scoredata.get_int_array('clear_cnt', 3))) musicdata.add_child(Node.s32_array('fc_cnt', scoredata.get_int_array('fc_cnt', 3))) musicdata.add_child(Node.s32_array('ex_cnt', scoredata.get_int_array('ex_cnt', 3))) musicdata.add_child(Node.s32_array('score', scoredata.get_int_array('points', 3))) musicdata.add_child(Node.s8_array('clear', scoredata.get_int_array('clear_flags', 3))) ghosts = scoredata.get('ghost', [None, None, None]) for i in range(len(ghosts)): ghost = ghosts[i] if ghost is None: continue bar = Node.u8_array('bar', ghost) musicdata.add_child(bar) bar.set_attribute('seq', str(i)) return root
def update_score( self, userid: UserID, songid: int, chart: int, points: int, medal: int, combo: Optional[int] = None, stats: Optional[Dict[str, int]] = None, ) -> None: """ Given various pieces of a score, update the user's high score and score history in a controlled manner, so all games in Pop'n series can expect the same attributes in a score. Note that the medals passed here are expected to be converted from game identifier to our internal identifier, so that any game in the series may convert them back. In this way, a song played on Pop'n 22 that exists in Pop'n 19 will still have scores/medals going back all versions. """ # Range check medals if medal not in [ self.PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_EASY_CLEAR, self.PLAY_MEDAL_CIRCLE_CLEARED, self.PLAY_MEDAL_DIAMOND_CLEARED, self.PLAY_MEDAL_STAR_CLEARED, self.PLAY_MEDAL_CIRCLE_FULL_COMBO, self.PLAY_MEDAL_DIAMOND_FULL_COMBO, self.PLAY_MEDAL_STAR_FULL_COMBO, self.PLAY_MEDAL_PERFECT, ]: raise Exception("Invalid medal value {}".format(medal)) oldscore = self.data.local.music.get_score( self.game, self.version, userid, songid, chart, ) # Score history is verbatum, instead of highest score history = ValidatedDict({}) oldpoints = points if oldscore is None: # If it is a new score, create a new dictionary to add to scoredata = ValidatedDict({}) raised = True highscore = True else: # Set the score to any new record achieved raised = points > oldscore.points highscore = points >= oldscore.points points = max(points, oldscore.points) scoredata = oldscore.data # Replace medal with highest value scoredata.replace_int('medal', max(scoredata.get_int('medal'), medal)) history.replace_int('medal', medal) if stats is not None: if raised: # We have stats, and there's a new high score, update the stats scoredata.replace_dict('stats', stats) history.replace_dict('stats', stats) if combo is not None: # If we have a combo, replace it scoredata.replace_int('combo', max(scoredata.get_int('combo'), combo)) history.replace_int('combo', combo) # Look up where this score was earned lid = self.get_machine_id() # Pop'n Music for all versions before Lapistoria sends all of the songs # a player played at the end of the round. It doesn't send timestamps # for those songs (Jubeat does). So, if a user plays the same song/chart # more than once in a round, we will end up failing to store the attempt # since we don't allow two of the same attempt at the same time for the # same user and song/chart. So, bump the timestamp by one second and retry # well past the maximum number of songs. now = Time.now() for bump in range(10): timestamp = now + bump # Write the new score back self.data.local.music.put_score( self.game, self.version, userid, songid, chart, lid, points, scoredata, highscore, timestamp=timestamp, ) try: # Save the history of this score too self.data.local.music.put_attempt( self.game, self.version, userid, songid, chart, lid, oldpoints, history, raised, timestamp=timestamp, ) except ScoreSaveException: # Try again one second in the future continue # We saved successfully break
def update_score( self, userid: UserID, timestamp: int, songid: int, chart: int, points: int, medal: int, combo: int, ghost: Optional[List[int]] = None, stats: Optional[Dict[str, int]] = None, music_rate: int = None, ) -> None: """ Given various pieces of a score, update the user's high score and score history in a controlled manner, so all games in Jubeat series can expect the same attributes in a score. """ # Range check medals if medal not in [ self.PLAY_MEDAL_FAILED, self.PLAY_MEDAL_CLEARED, self.PLAY_MEDAL_NEARLY_FULL_COMBO, self.PLAY_MEDAL_FULL_COMBO, self.PLAY_MEDAL_NEARLY_EXCELLENT, self.PLAY_MEDAL_EXCELLENT, ]: raise Exception(f"Invalid medal value {medal}") oldscore = self.data.local.music.get_score( self.game, self.music_version, userid, songid, chart, ) # Score history is verbatum, instead of highest score history = ValidatedDict({}) oldpoints = points if oldscore is None: # If it is a new score, create a new dictionary to add to scoredata = ValidatedDict({}) raised = True highscore = True else: # Set the score to any new record achieved raised = points > oldscore.points highscore = points >= oldscore.points points = max(oldscore.points, points) scoredata = oldscore.data # Replace medal with highest value scoredata.replace_int('medal', max(scoredata.get_int('medal'), medal)) history.replace_int('medal', medal) # Increment counters based on medal if medal == self.PLAY_MEDAL_CLEARED: scoredata.increment_int('clear_count') if medal == self.PLAY_MEDAL_FULL_COMBO: scoredata.increment_int('full_combo_count') if medal == self.PLAY_MEDAL_EXCELLENT: scoredata.increment_int('excellent_count') # If we have a combo, replace it scoredata.replace_int('combo', max(scoredata.get_int('combo'), combo)) history.replace_int('combo', combo) if stats is not None: if raised: # We have stats, and there's a new high score, update the stats scoredata.replace_dict('stats', stats) history.replace_dict('stats', stats) if ghost is not None: # Update the ghost regardless, but don't bother with it in history scoredata.replace_int_array('ghost', len(ghost), ghost) if music_rate is not None: if oldscore is not None: if music_rate > oldscore.data.get_int('music_rate'): scoredata.replace_int('music_rate', music_rate) else: scoredata.replace_int('music_rate', music_rate) history.replace_int('music_rate', music_rate) # Look up where this score was earned lid = self.get_machine_id() # Write the new score back self.data.local.music.put_score( self.game, self.music_version, userid, songid, chart, lid, points, scoredata, highscore, timestamp=timestamp, ) # Save the history of this score too self.data.local.music.put_attempt( self.game, self.music_version, userid, songid, chart, lid, oldpoints, history, raised, timestamp=timestamp, )
def update_score( self, userid: Optional[UserID], songid: int, chart: int, points: int, clear_type: int, grade: int, combo: int, stats: Optional[Dict[str, int]] = None, ) -> None: """ Given various pieces of a score, update the user's high score and score history in a controlled manner, so all games in SDVX series can expect the same attributes in a score. """ # Range check clear type if clear_type not in [ self.CLEAR_TYPE_NO_PLAY, self.CLEAR_TYPE_FAILED, self.CLEAR_TYPE_CLEAR, self.CLEAR_TYPE_HARD_CLEAR, self.CLEAR_TYPE_ULTIMATE_CHAIN, self.CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN, ]: raise Exception(f"Invalid clear type value {clear_type}") # Range check grade if grade not in [ self.GRADE_NO_PLAY, self.GRADE_D, self.GRADE_C, self.GRADE_B, self.GRADE_A, self.GRADE_A_PLUS, self.GRADE_AA, self.GRADE_AA_PLUS, self.GRADE_AAA, self.GRADE_AAA_PLUS, self.GRADE_S, ]: raise Exception(f"Invalid clear type value {grade}") if userid is not None: oldscore = self.data.local.music.get_score( self.game, self.version, userid, songid, chart, ) else: oldscore = None # Score history is verbatum, instead of highest score history = ValidatedDict({}) oldpoints = points if oldscore is None: # If it is a new score, create a new dictionary to add to scoredata = ValidatedDict({}) raised = True highscore = True else: # Set the score to any new record achieved raised = points > oldscore.points highscore = points >= oldscore.points points = max(oldscore.points, points) scoredata = oldscore.data # Replace clear type and grade scoredata.replace_int('clear_type', max(scoredata.get_int('clear_type'), clear_type)) history.replace_int('clear_type', clear_type) scoredata.replace_int('grade', max(scoredata.get_int('grade'), grade)) history.replace_int('grade', grade) # If we have a combo, replace it scoredata.replace_int('combo', max(scoredata.get_int('combo'), combo)) history.replace_int('combo', combo) # If we have play stats, replace it if stats is not None: if raised: # We have stats, and there's a new high score, update the stats scoredata.replace_dict('stats', stats) history.replace_dict('stats', stats) # Look up where this score was earned lid = self.get_machine_id() if userid is not None: # Write the new score back self.data.local.music.put_score( self.game, self.version, userid, songid, chart, lid, points, scoredata, highscore, ) # Save the history of this score too self.data.local.music.put_attempt( self.game, self.version, userid, songid, chart, lid, oldpoints, history, raised, )
def format_scores(self, userid: UserID, profile: ValidatedDict, scores: List[Score]) -> Node: root = Node.void('gametop') datanode = Node.void('data') root.add_child(datanode) player = Node.void('player') datanode.add_child(player) player.add_child(Node.s32('jid', profile.get_int('extid'))) playdata = Node.void('playdata') player.add_child(playdata) playdata.set_attribute('count', str(len(scores))) music = ValidatedDict() for score in scores: chart = self.db_to_game_chart(score.chart) data = music.get_dict(str(score.id)) play_cnt = data.get_int_array('play_cnt', 3) clear_cnt = data.get_int_array('clear_cnt', 3) clear_flags = data.get_int_array('clear_flags', 3) fc_cnt = data.get_int_array('fc_cnt', 3) ex_cnt = data.get_int_array('ex_cnt', 3) points = data.get_int_array('points', 3) # This means that we already assigned a value and it was greater than current # This is possible because we iterate through both hard mode and normal mode scores # and treat them equally. # TODO: generalize score merging code into a library since this does not account for # having a full combo in hard mode but not in normal. if points[chart] >= score.points: continue # Replace data for this chart type play_cnt[chart] = score.plays clear_cnt[chart] = score.data.get_int('clear_count') fc_cnt[chart] = score.data.get_int('full_combo_count') ex_cnt[chart] = score.data.get_int('excellent_count') points[chart] = score.points # Format the clear flags clear_flags[chart] = self.GAME_FLAG_BIT_PLAYED if score.data.get_int('clear_count') > 0: clear_flags[chart] |= self.GAME_FLAG_BIT_CLEARED if score.data.get_int('full_combo_count') > 0: clear_flags[chart] |= self.GAME_FLAG_BIT_FULL_COMBO if score.data.get_int('excellent_count') > 0: clear_flags[chart] |= self.GAME_FLAG_BIT_EXCELLENT # Save chart data back data.replace_int_array('play_cnt', 3, play_cnt) data.replace_int_array('clear_cnt', 3, clear_cnt) data.replace_int_array('clear_flags', 3, clear_flags) data.replace_int_array('fc_cnt', 3, fc_cnt) data.replace_int_array('ex_cnt', 3, ex_cnt) data.replace_int_array('points', 3, points) # Update the ghost (untyped) ghost = data.get('ghost', [None, None, None]) ghost[chart] = score.data.get('ghost') data['ghost'] = ghost # Save it back music.replace_dict(str(score.id), data) for scoreid in music: scoredata = music[scoreid] musicdata = Node.void('musicdata') playdata.add_child(musicdata) musicdata.set_attribute('music_id', scoreid) musicdata.add_child( Node.s32_array('play_cnt', scoredata.get_int_array('play_cnt', 3))) musicdata.add_child( Node.s32_array('clear_cnt', scoredata.get_int_array('clear_cnt', 3))) musicdata.add_child( Node.s32_array('fc_cnt', scoredata.get_int_array('fc_cnt', 3))) musicdata.add_child( Node.s32_array('ex_cnt', scoredata.get_int_array('ex_cnt', 3))) musicdata.add_child( Node.s32_array('score', scoredata.get_int_array('points', 3))) musicdata.add_child( Node.s8_array('clear', scoredata.get_int_array('clear_flags', 3))) ghosts = scoredata.get('ghost', [None, None, None]) for i in range(len(ghosts)): ghost = ghosts[i] if ghost is None: continue bar = Node.u8_array('bar', ghost) musicdata.add_child(bar) bar.set_attribute('seq', str(i)) return root