From 2a08c9cd14534805b2c042bf9d14bdcc7edb14f1 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 14 Jun 2024 16:06:26 +0800 Subject: [PATCH] [Enhance] World rank score mechanism - Adjust world rank mechanism to be closer to the official one. Note: You need to refresh rating in web admin backend after updating, and the users need to get a better or new score to refresh world rank. --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 4 +- latest version/core/operation.py | 37 +++++++++++----- latest version/core/score.py | 32 +++++++++++--- latest version/core/sql.py | 9 +++- latest version/core/user.py | 18 +++++--- latest version/core/world.py | 4 ++ latest version/database/init/arc_data.py | 2 +- latest version/database/init/singles.json | 54 +++++++++++++++++++++++ latest version/database/init/tables.sql | 3 +- latest version/server/purchase.py | 2 + 11 files changed, 137 insertions(+), 30 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 2264f62..9816022 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/hanami/29' # str | list[str] + GAME_API_PREFIX = '/natsugakuru/30' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 64b0a4c..e98694b 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.12' -ARCAEA_DATABASE_VERSION = 'v2.11.3.11' +ARCAEA_SERVER_VERSION = 'v2.11.3.13' +ARCAEA_DATABASE_VERSION = 'v2.11.3.13' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 741d4af..62b2143 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -27,6 +27,7 @@ class BaseOperation: class RefreshAllScoreRating(BaseOperation): ''' 刷新所有成绩的评分 + 包括 score_v2 ''' _name = 'refresh_all_score_rating' @@ -44,11 +45,11 @@ class RefreshAllScoreRating(BaseOperation): for i in x: for j in range(0, 4): - defnum = -10 # 没在库里的全部当做定数-10 + defnum = -10 # 没在库里的全部当做定数 -10 if i[j+1] is not None and i[j+1] > 0: defnum = float(i[j+1]) / 10 - c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', { + c.execute('''select user_id, score, shiny_perfect_count, perfect_count, near_count, miss_count from best_score where song_id=:a and difficulty=:b''', { 'a': i[0], 'b': j}) y = c.fetchall() values = [] @@ -56,10 +57,12 @@ class RefreshAllScoreRating(BaseOperation): for k in y: ptt = Score.calculate_rating(defnum, k[1]) ptt = max(ptt, 0) - values.append((ptt,)) + score_v2 = Score.calculate_score_v2( + defnum, k[2], k[3], k[4], k[5]) + values.append((ptt, score_v2,)) where_values.append((k[0], i[0], j)) if values: - Sql(c).update_many('best_score', ['rating'], values, [ + Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [ 'user_id', 'song_id', 'difficulty'], where_values) @@ -133,11 +136,16 @@ class SaveUpdateScore(BaseOperation): new_scores = [] for i in save.scores_data: rating = 0 + score_v2 = 0 if i['song_id'] in song_chart_const: - rating = Score.calculate_rating( - song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + defnum = song_chart_const[i['song_id'] + ][i['difficulty']] / 10 + rating = Score.calculate_rating(defnum, i['score']) rating = max(rating, 0) + score_v2 = Score.calculate_score_v2( + defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count']) + y = f'{i["song_id"]}{i["difficulty"]}' if y in clear_state: clear_type = clear_state[y] @@ -145,10 +153,10 @@ class SaveUpdateScore(BaseOperation): clear_type = 0 new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'], - i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating)) + i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2)) c.executemany( - '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) def _all_update(self): with Connect() as c: @@ -169,11 +177,16 @@ class SaveUpdateScore(BaseOperation): new_scores = [] for i in save.scores_data: rating = 0 + score_v2 = 0 if i['song_id'] in song_chart_const: - rating = Score.calculate_rating( - song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + defnum = song_chart_const[i['song_id'] + ][i['difficulty']] / 10 + rating = Score.calculate_rating(defnum, i['score']) rating = max(rating, 0) + score_v2 = Score.calculate_score_v2( + defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count']) + y = f'{i["song_id"]}{i["difficulty"]}' if y in clear_state: clear_type = clear_state[y] @@ -181,10 +194,10 @@ class SaveUpdateScore(BaseOperation): clear_type = 0 new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'], - i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating)) + i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2)) c.executemany( - '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) class UnlockUserItem(BaseOperation): diff --git a/latest version/core/score.py b/latest version/core/score.py index 485e480..095409b 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -30,6 +30,7 @@ class Score: self.best_clear_type: int = None self.clear_type: int = None self.rating: float = None + self.score_v2: float = None # for `world_rank_score` of global rank def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int): self.score = int(score) if score is not None else 0 @@ -124,12 +125,31 @@ class Score: return ptt + @staticmethod + def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int) -> float: + # 计算score_v2 refer: https://www.bilibili.com/video/BV1ys421u7BY + # 谱面定数小于等于 0 视为 unranked,返回值会为 0 + if not defnum or defnum <= 0: + return 0 + + all_note = perfect_count + near_count + miss_count + if all_note == 0: + return 0 + shiny_ratio = shiny_perfect_count / all_note + score_ratio = (perfect_count + near_count/2) / \ + all_note + shiny_perfect_count / 10000000 + acc_rating = max(0, min(shiny_ratio - 0.9, 0.095)) / 9.5 * 25 + score_rating = max(0, min(score_ratio - 0.99, 0.01)) * 75 + return defnum * (acc_rating + score_rating) + def get_rating_by_calc(self) -> float: - # 通过计算得到本成绩的rating + # 通过计算得到本成绩的 rating & score_v2 if not self.song.defnum: self.song.c = self.c self.song.select() self.rating = self.calculate_rating(self.song.chart_const, self.score) + self.score_v2 = self.calculate_score_v2( + self.song.chart_const, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count) return self.rating def to_dict(self) -> dict: @@ -181,6 +201,7 @@ class UserScore(Score): self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12]) self.best_clear_type = int(x[11]) self.rating = float(x[13]) + self.score_v2 = float(x[14]) return self @@ -430,8 +451,9 @@ class UserPlay(UserScore): x = self.c.fetchone() if not x: self.new_best_protect_flag = True # 初见保护 - self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', { - 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating}) + self.c.execute('''insert into best_score values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (self.user.user_id, self.song.song_id, self.song.difficulty, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, + self.health, self.modifier, self.time_played, self.clear_type, self.clear_type, self.rating, self.score_v2)) self.user.update_global_rank() else: self.new_best_protect_flag = False @@ -440,8 +462,8 @@ class UserPlay(UserScore): 'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty}) if self.score >= int(x[0]): # best成绩更新 self.new_best_protect_flag = True - self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', { - 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played}) + self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m, score_v2 = :n where user_id = :a and song_id = :b and difficulty = :c ''', { + 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played, 'n': self.score_v2}) self.user.update_global_rank() self.ptt = Potential(self.c, self.user) diff --git a/latest version/core/sql.py b/latest version/core/sql.py index ae7784c..9e81edf 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -351,7 +351,8 @@ class Sql: class DatabaseMigrator: SPECIAL_UPDATE_VERSION = { - '2.11.3.11': '_version_2_11_3_11' + '2.11.3.11': '_version_2_11_3_11', + '2.11.3.13': '_version_2_11_3.13' } def __init__(self, c1_path: str, c2_path: str) -> None: @@ -463,6 +464,12 @@ class DatabaseMigrator: self.c2.executemany( '''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list) + def _version_2_11_3_13(self): + ''' + 2.11.3.13 版本特殊更新,world_rank_score 机制调整,需清空用户分数 + ''' + self.c1.execute('''update user set world_rank_score = 0''') + class LogDatabaseMigrator: diff --git a/latest version/core/user.py b/latest version/core/user.py index 3ccc979..05ab4ee 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -528,7 +528,11 @@ class UserInfo(User): 'world_mode_locked_end_ts': self.world_mode_locked_end_ts, 'locked_char_ids': [], # [1] 'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(), - 'pick_ticket': self.pick_ticket + 'pick_ticket': self.pick_ticket, + + # 'custom_banner': 'online_banner_2024_06', + # 'subscription_multiplier': 114, + # 'memory_boost_ticket': 5, } def from_list(self, x: list) -> 'UserInfo': @@ -648,8 +652,8 @@ class UserInfo(User): '''用户世界排名,如果超过设定最大值,返回0''' if self.world_rank_score is None: self.select_user_one_column('world_rank_score', 0) - if self.world_rank_score is None: - return 0 + if not self.world_rank_score: + return 0 self.c.execute( '''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,)) @@ -665,14 +669,14 @@ class UserInfo(User): self.c.execute( ''' with user_scores as ( - select song_id, difficulty, score from best_score where user_id = ? and difficulty in (2, 3, 4) + select song_id, difficulty, score_v2 from best_score where user_id = ? and difficulty in (2, 3, 4) ) select sum(a) from( - select sum(score) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0) + select sum(score_v2) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0) union - select sum(score) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0) + select sum(score_v2) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0) union - select sum(score) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0) + select sum(score_v2) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0) ) ''', (self.user_id,) diff --git a/latest version/core/world.py b/latest version/core/world.py index d1e934e..c87fc53 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -265,6 +265,7 @@ class UserMap(Map): r['curr_capture'] = self.curr_capture r['is_locked'] = self.is_locked r['user_id'] = self.user.user_id + # memory_boost_ticket if not has_steps: del r['steps'] if has_rewards: @@ -668,6 +669,9 @@ class BaseWorldPlay(WorldSkillMixin): 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts, 'beyond_boost_gauge': self.user.beyond_boost_gauge, # 'wpaid': 'helloworld', # world play id ??? + # progress_before_sub_boost + # progress_sub_boost_amount + # subscription_multiply } if self.character_used.skill_id_displayed == 'skill_maya': diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 54c1e34..d762b70 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -70,7 +70,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index cef6ad3..e963d38 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1774,5 +1774,59 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "backtobasics", + "items": [ + { + "type": "single", + "id": "backtobasics", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "tasogare", + "items": [ + { + "type": "single", + "id": "tasogare", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "beautifuldreamer", + "items": [ + { + "type": "single", + "id": "beautifuldreamer", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 88c8e17..a2bb234 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -61,7 +61,8 @@ modifier int, time_played int, best_clear_type int, clear_type int, -rating real, +rating real default 0, +score_v2 real default 0, primary key(user_id, song_id, difficulty) ); create table if not exists user_char(user_id int, diff --git a/latest version/server/purchase.py b/latest version/server/purchase.py index 083eeb8..1e80da1 100644 --- a/latest version/server/purchase.py +++ b/latest version/server/purchase.py @@ -106,6 +106,8 @@ def buy_special(user_id): x.discount_from = -1 x.discount_to = -1 x.items = [ItemFactory(c).get_item(item_id)] + # request.form['ticket_used'] == 'true' + # memory_boost_ticket: x-1 x.buy() r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}