From 5c539bdf5946eb72376d3e27305a8dffe0f6fab8 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 30 Apr 2024 00:27:23 +0800 Subject: [PATCH] [Enhance] Recent 30 - Update Recent 30 mechanism. - Alter Recent 30 table structure. Note: 1. This is a TEST version. Maybe there are many bugs. 2. This special version is a line of demarcation. --- latest version/core/constant.py | 3 +- latest version/core/init.py | 13 +- latest version/core/score.py | 192 +++++++++++------------- latest version/core/song.py | 3 + latest version/core/sql.py | 60 +++++++- latest version/core/user.py | 3 +- latest version/core/util.py | 9 +- latest version/database/init/tables.sql | 77 ++-------- latest version/web/index.py | 20 ++- latest version/web/webscore.py | 36 +---- 10 files changed, 201 insertions(+), 215 deletions(-) diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 988af3b..f9c1a87 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.10' +ARCAEA_SERVER_VERSION = 'v2.11.3.11' +ARCAEA_DATABASE_VERSION = 'v2.11.3.11' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/init.py b/latest version/core/init.py index a8152a9..cf0b8c4 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -8,7 +8,7 @@ from traceback import format_exc from core.bundle import BundleParser from core.config_manager import Config -from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION +from core.constant import ARCAEA_DATABASE_VERSION, ARCAEA_LOG_DATBASE_VERSION from core.course import Course from core.download import DownloadList from core.purchase import Purchase @@ -46,7 +46,7 @@ class DatabaseInit: with open(self.sql_path, 'r', encoding='utf-8') as f: self.c.executescript(f.read()) self.c.execute('''insert into config values("version", :a);''', { - 'a': ARCAEA_SERVER_VERSION}) + 'a': ARCAEA_DATABASE_VERSION}) def character_init(self) -> None: '''初始化搭档信息''' @@ -141,8 +141,7 @@ class DatabaseInit: character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email) values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email) ''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email}) - self.c.execute('''insert into recent30(user_id) values(:user_id)''', { - 'user_id': x.user_id}) + self.c.execute( '''insert into user_role values(?, "admin")''', (x.user_id,)) @@ -175,7 +174,7 @@ class LogDatabaseInit: with open(self.sql_path, 'r') as f: self.c.executescript(f.read()) self.c.execute( - '''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION}) + '''insert into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION}) def init(self) -> None: with Connect(self.db_path) as c: @@ -270,12 +269,12 @@ class FileChecker: except: x = None # 数据库自动更新,不强求 - if not x or x[0] != ARCAEA_SERVER_VERSION: + if not x or x[0] != ARCAEA_DATABASE_VERSION: self.logger.warning( f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}') try: self.logger.info( - f'Try to update the file `{db_path}` to version {ARCAEA_SERVER_VERSION}.') + f'Try to update the file `{db_path}` to version {ARCAEA_DATABASE_VERSION}.') if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH): os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH) diff --git a/latest version/core/score.py b/latest version/core/score.py index 5ad712e..485e480 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -385,53 +385,9 @@ class UserPlay(UserScore): (self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token)) def clear_play_state(self) -> None: - self.c.execute('''delete from songplay_token where user_id=:a ''', { + self.c.execute('''delete from songplay_token where user_id=:a''', { 'a': self.user.user_id}) - def update_recent30(self) -> None: - '''更新此分数对应用户的recent30''' - old_recent_10 = self.ptt.recent_10 - if self.is_protected: - old_r30 = self.ptt.r30.copy() - old_s30 = self.ptt.s30.copy() - - # 寻找pop位置 - songs = list(set(self.ptt.s30)) - if '' in self.ptt.s30: - r30_id = 29 - else: - n = len(songs) - if n >= 11: - r30_id = 29 - elif self.song.song_id_difficulty not in songs and n == 10: - r30_id = 29 - elif self.song.song_id_difficulty in songs and n == 10: - i = 29 - while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0: - i -= 1 - r30_id = i - elif self.song.song_id_difficulty not in songs and n == 9: - i = 29 - while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0: - i -= 1 - r30_id = i - else: - r30_id = 29 - - self.ptt.recent_30_update( - r30_id, self.rating, self.song.song_id_difficulty) - if self.is_protected and old_recent_10 > self.ptt.recent_10: - if self.song.song_id_difficulty in old_s30: - # 发现重复歌曲,更新到最高rating - index = old_s30.index(self.song.song_id_difficulty) - if old_r30[index] < self.rating: - old_r30[index] = self.rating - - self.ptt.r30 = old_r30 - self.ptt.s30 = old_s30 - - self.ptt.insert_recent_30() - def record_score(self) -> None: '''向log数据库记录分数,请注意列名不同''' logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played, @@ -490,7 +446,7 @@ class UserPlay(UserScore): self.ptt = Potential(self.c, self.user) if not self.unrank_flag: - self.update_recent30() + self.ptt.r30_push_score(self) # 总PTT更新 user_rating_ptt = self.ptt.value @@ -527,9 +483,8 @@ class Potential: self.c = c self.user = user - self.r30: 'list[float]' = None - self.s30: 'list[str]' = None - self.songs_selected: list = None + self.r30_tuples: 'list[tuple[int, str, int, float]]' = None + self.r30: 'list[Score]' = None self.b30: list = None @@ -545,75 +500,110 @@ class Potential: 'a': self.user.user_id}) return sum(x[0] for x in self.c.fetchall()) - def select_recent_30(self) -> None: + def select_recent_30_tuple(self) -> None: '''获取用户recent30数据''' self.c.execute( - '''select * from recent30 where user_id = :a''', {'a': self.user.user_id}) - x = self.c.fetchone() - if not x: - raise NoData( - f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3) + '''select r_index, song_id, difficulty, rating from recent30 where user_id = ? order by time_played DESC''', (self.user.user_id,)) + + self.r30_tuples = [x for x in self.c.fetchall() if x[1] != ''] + + def select_recent_30(self) -> None: + self.c.execute( + '''select song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, time_played, clear_type, rating from recent30 where user_id = ? order by time_played DESC''', (self.user.user_id,)) + self.r30 = [] - self.s30 = [] - if not x: - return None - for i in range(1, 61, 2): - if x[i] is not None: - self.r30.append(float(x[i])) - self.s30.append(x[i+1]) - else: - self.r30.append(0) - self.s30.append('') + for x in self.c.fetchall(): + if x[0] == '': + continue + s = Score() + s.song.set_chart(x[0], x[1]) + s.set_score(*x[2:-1]) + s.rating = x[-1] + self.r30.append(s) @property def recent_10(self) -> float: '''获取用户recent10的总潜力值''' - if self.r30 is None: - self.select_recent_30() + if self.r30_tuples is None: + self.select_recent_30_tuple() - rating_sum = 0 - r30, s30 = (list(t) for t in zip( - *sorted(zip(self.r30, self.s30), reverse=True))) + max_dict = {} + for x in self.r30_tuples: + if (x[1], x[2]) not in max_dict or max_dict[(x[1], x[2])] < x[3]: + max_dict[(x[1], x[2])] = x[3] - self.songs_selected = [] - i = 0 - while len(self.songs_selected) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None: - if s30[i] not in self.songs_selected: - rating_sum += r30[i] - self.songs_selected.append(s30[i]) - i += 1 - return rating_sum + top_10_rating = sorted(max_dict.values(), reverse=True)[:10] + return sum(top_10_rating) def recent_30_to_dict_list(self) -> list: if self.r30 is None: self.select_recent_30() - r = [] - for x, y in zip(self.s30, self.r30): - if x: - r.append({ - 'song_id': x[:-1], - 'difficulty': int(x[-1]), - 'rating': y - }) - return r - def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None: - self.r30.pop(pop_index) - self.s30.pop(pop_index) - self.r30.insert(0, rating) - self.s30.insert(0, song_id_difficulty) + return [x.to_dict() for x in self.r30] - def insert_recent_30(self) -> None: - '''更新r30表数据''' - sql = '''update recent30 set r0=?,song_id0=?,r1=?,song_id1=?,r2=?,song_id2=?,r3=?,song_id3=?,r4=?,song_id4=?,r5=?,song_id5=?,r6=?,song_id6=?,r7=?,song_id7=?,r8=?,song_id8=?,r9=?,song_id9=?,r10=?,song_id10=?,r11=?,song_id11=?,r12=?,song_id12=?,r13=?,song_id13=?,r14=?,song_id14=?,r15=?,song_id15=?,r16=?,song_id16=?,r17=?,song_id17=?,r18=?,song_id18=?,r19=?,song_id19=?,r20=?,song_id20=?,r21=?,song_id21=?,r22=?,song_id22=?,r23=?,song_id23=?,r24=?,song_id24=?,r25=?,song_id25=?,r26=?,song_id26=?,r27=?,song_id27=?,r28=?,song_id28=?,r29=?,song_id29=? where user_id=?''' - sql_list = [] - for i in range(30): - sql_list.append(self.r30[i]) - sql_list.append(self.s30[i]) + def update_one_r30(self, r_index: int, user_score: 'UserPlay | UserScore') -> None: + '''更新数据表中的一条数据''' + self.c.execute('''insert or replace into recent30 values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + (self.user.user_id, r_index, user_score.time_played, user_score.song.song_id, user_score.song.difficulty, + user_score.score, user_score.shiny_perfect_count, user_score.perfect_count, user_score.near_count, user_score.miss_count, user_score.health, user_score.modifier, user_score.clear_type, user_score.rating)) - sql_list.append(self.user.user_id) + def r30_push_score(self, user_score: 'UserPlay | UserScore') -> None: + '''根据新成绩调整 r30''' + if self.r30_tuples is None: + self.select_recent_30_tuple() - self.c.execute(sql, sql_list) + if len(self.r30_tuples) < 30: + self.update_one_r30(len(self.r30_tuples), user_score) + return None + + if user_score.is_protected: + # 保护,替换最低的最旧的成绩 + f_tuples = list( + filter(lambda x: x[-1] <= user_score.rating, self.r30_tuples)) + f_tuples.reverse() # 从旧到新 + f_tuples = sorted(f_tuples, key=lambda x: x[-1]) + if not f_tuples: + # 找不到更低的成绩,不更新 + return None + + unique_songs: 'dict[tuple[str, int], list[tuple[int, int, float]]]' = {} + for i, x in enumerate(self.r30_tuples): + unique_songs.setdefault((x[1], x[2]), []).append((i, x[0], x[3])) + + new_song = user_score.song.to_tuple() + + if len(unique_songs) >= 11 or (len(unique_songs) == 10 and new_song not in unique_songs): + if user_score.is_protected: + # 保护,替换最低的最旧的成绩 + self.update_one_r30(f_tuples[0][0], user_score) + else: + self.update_one_r30(self.r30_tuples[-1][0], user_score) + return None + + filtered_songs = dict(filter(lambda x: len( + x[1]) > 1, unique_songs.items())) # 过滤掉只有单个成绩的 + + if new_song in unique_songs and new_song not in filtered_songs: + # 如果新成绩有相同谱面的唯一成绩在 r30 中,则它也应该有可能被替换 + filtered_songs[new_song] = unique_songs[new_song] + + if user_score.is_protected: + # 保护,替换最低的最旧的成绩,此时需在 filtered_songs 中 + for x in f_tuples: + if (x[1], x[2]) in filtered_songs: + self.update_one_r30(x[0], user_score) + return None + else: + # 找到符合条件的最旧成绩 + max_idx = -1 + max_r_index = -1 + for x in filtered_songs.values(): + for y in x: + if y[0] > max_idx: + max_idx = y[0] + max_r_index = y[1] + + self.update_one_r30(max_r_index, user_score) class UserScoreList: diff --git a/latest version/core/song.py b/latest version/core/song.py index d6af16f..8571a45 100644 --- a/latest version/core/song.py +++ b/latest version/core/song.py @@ -18,6 +18,9 @@ class Chart: 'difficulty': self.difficulty, 'chart_const': self.chart_const } + + def to_tuple(self) -> tuple: + return (self.song_id, self.difficulty) @property def chart_const(self) -> float: diff --git a/latest version/core/sql.py b/latest version/core/sql.py index ee4388f..ae7784c 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -6,6 +6,7 @@ from atexit import register from .config_manager import Config from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant from .error import ArcError, InputError +from .util import parse_version class Connect: @@ -349,10 +350,19 @@ class Sql: class DatabaseMigrator: + SPECIAL_UPDATE_VERSION = { + '2.11.3.11': '_version_2_11_3_11' + } + def __init__(self, c1_path: str, c2_path: str) -> None: self.c1_path = c1_path self.c2_path = c2_path + self.c1 = None + self.c2 = None + + self.tables = Constant.DATABASE_MIGRATE_TABLES + @staticmethod def update_one_table(c1, c2, table_name: str) -> bool: '''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖''' @@ -397,7 +407,11 @@ class DatabaseMigrator: ''' with Connect(self.c2_path) as c2: with Connect(self.c1_path) as c1: - for i in Constant.DATABASE_MIGRATE_TABLES: + self.c1 = c1 + self.c2 = c2 + self.special_update() + + for i in self.tables: self.update_one_table(c1, c2, i) if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA: @@ -405,6 +419,50 @@ class DatabaseMigrator: self.update_user_char_full(c2) # 更新user_char_full + def special_update(self): + old_version = self.c1.execute( + '''select value from config where id = "version"''').fetchone() + new_version = self.c2.execute( + '''select value from config where id = "version"''').fetchone() + old_version = old_version[0] if old_version else '0.0.0' + new_version = new_version[0] if new_version else '0.0.0' + old_version = parse_version(old_version) + new_version = parse_version(new_version) + + for k, v in self.SPECIAL_UPDATE_VERSION.items(): + if old_version < parse_version(k) <= new_version: + getattr(self, v)() + + def _version_2_11_3_11(self): + ''' + 2.11.3.11 版本特殊更新,调整 recent30 表结构 + recent30 表从 (user_id: int PK, song_id: text, rating: real, ...) \ + 更改为 (user_id: int PK, r_index: int PK, time_played: int, song_id: text, difficulty: int, score: int, sp, p, n, m, hp, mod, clear_type, rating: real) + ''' + + self.tables = [x for x in self.tables if x != 'recent30'] + + x = self.c1.execute('''select * from recent30''') + sql_list = [] + for i in x: + user_id = int(i[0]) + for j in range(30): + rating = i[1 + j * 2] + rating = float(rating) if rating else 0 + song_id_difficulty = i[2 + j * 2] + if song_id_difficulty: + song_id = song_id_difficulty[:-1] + difficulty = int(song_id_difficulty[-1]) + else: + song_id = '' + difficulty = 0 + + sql_list.append( + (user_id, j, 100-j, song_id, difficulty, rating)) + + self.c2.executemany( + '''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list) + class LogDatabaseMigrator: diff --git a/latest version/core/user.py b/latest version/core/user.py index 7b93d25..914f154 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -153,8 +153,7 @@ class UserRegister(User): character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email) values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email) ''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email}) - self.c.execute('''insert into recent30(user_id) values(:user_id)''', { - 'user_id': self.user_id}) + class UserLogin(User): diff --git a/latest version/core/util.py b/latest version/core/util.py index 24c0021..b0ed808 100644 --- a/latest version/core/util.py +++ b/latest version/core/util.py @@ -1,9 +1,10 @@ import hashlib import os -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from datetime import date from time import mktime +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + def aes_gcm_128_encrypt(key, plaintext, associated_data): iv = os.urandom(12) @@ -65,3 +66,9 @@ def try_rename(path: str, new_path: str) -> str: def get_today_timestamp(): '''相对于本机本地时间的今天0点的时间戳''' return int(mktime(date.today().timetuple())) + + +def parse_version(s: str) -> 'list[int]': + '''解析版本号''' + s_number = "".join(x for x in s if x.isdigit() or x == '.') + return list(map(int, [x for x in s_number.split('.') if x != ''])) diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index cbabb07..88c8e17 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -107,67 +107,22 @@ type text, amount int, primary key(character_id, item_id, type) ); -create table if not exists recent30(user_id int primary key, -r0 real, -song_id0 text, -r1 real, -song_id1 text, -r2 real, -song_id2 text, -r3 real, -song_id3 text, -r4 real, -song_id4 text, -r5 real, -song_id5 text, -r6 real, -song_id6 text, -r7 real, -song_id7 text, -r8 real, -song_id8 text, -r9 real, -song_id9 text, -r10 real, -song_id10 text, -r11 real, -song_id11 text, -r12 real, -song_id12 text, -r13 real, -song_id13 text, -r14 real, -song_id14 text, -r15 real, -song_id15 text, -r16 real, -song_id16 text, -r17 real, -song_id17 text, -r18 real, -song_id18 text, -r19 real, -song_id19 text, -r20 real, -song_id20 text, -r21 real, -song_id21 text, -r22 real, -song_id22 text, -r23 real, -song_id23 text, -r24 real, -song_id24 text, -r25 real, -song_id25 text, -r26 real, -song_id26 text, -r27 real, -song_id27 text, -r28 real, -song_id28 text, -r29 real, -song_id29 text +create table if not exists recent30( +user_id int, +r_index int, +time_played int, +song_id text, +difficulty int, +score int default 0, +shiny_perfect_count int default 0, +perfect_count int default 0, +near_count int default 0, +miss_count int default 0, +health int default 0, +modifier int default 0, +clear_type int default 0, +rating real default 0, +primary key(user_id, r_index) ); create table if not exists user_world(user_id int, map_id text, diff --git a/latest version/web/index.py b/latest version/web/index.py index d908402..cd8e18e 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -1,16 +1,19 @@ import os import time -from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshBundleCache, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore -from core.rank import RankList -from core.sql import Connect -from core.user import User from flask import Blueprint, flash, redirect, render_template, request, url_for from werkzeug.utils import secure_filename import web.system import web.webscore +from core.init import FileChecker +from core.operation import (DeleteUserScore, RefreshAllScoreRating, + RefreshBundleCache, RefreshSongFileCache, + SaveUpdateScore, UnlockUserItem) +from core.rank import RankList +from core.score import Potential +from core.sql import Connect +from core.user import User from web.login import login_required UPLOAD_FOLDER = 'database' @@ -97,8 +100,11 @@ def single_player_ptt(): user_id = user_id[0] user = web.webscore.get_user(c, user_id) posts = web.webscore.get_user_score(c, user_id, 30) - recent, recentptt = web.webscore.get_user_recent30( - c, user_id) + u = User() + u.user_id = user_id + p = Potential(c, u) + recentptt = p.recent_10 / 10 + recent = p.recent_30_to_dict_list() if not posts: error = '无成绩 No score.' else: diff --git a/latest version/web/webscore.py b/latest version/web/webscore.py index 517a69c..485aad7 100644 --- a/latest version/web/webscore.py +++ b/latest version/web/webscore.py @@ -1,5 +1,7 @@ import time +from core.score import Potential + def get_user_score(c, user_id, limit=-1, offset=0): # 返回用户的所有歌曲数据,带排名,返回字典列表 @@ -74,37 +76,3 @@ def get_user(c, user_id): } return r - - -def get_user_recent30(c, user_id): - # 获取玩家recent30信息并计算recent10的ptt,返回字典列表和一个值 - c.execute('''select * from recent30 where user_id=:a''', {'a': user_id}) - sumr = 0 - x = c.fetchone() - r = [] - if x is not None: - r30 = [] - s30 = [] - for i in range(1, 61, 2): - if x[i] is not None: - r30.append(float(x[i])) - s30.append(x[i+1]) - else: - r30.append(0) - s30.append('') - r30, s30 = (list(t) for t in zip(*sorted(zip(r30, s30), reverse=True))) - songs = [] - i = 0 - while len(songs) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None: - if s30[i] not in songs: - sumr += r30[i] - songs.append(s30[i]) - i += 1 - for i in range(0, 30): - if s30[i]: - r.append({ - 'song_id': s30[i][:-1], - 'difficulty': int(s30[i][-1]), - 'rating': r30[i] - }) - return r, sumr / 10