[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.
This commit is contained in:
Lost-MSth
2024-04-30 00:27:23 +08:00
parent efedd96908
commit 5c539bdf59
10 changed files with 201 additions and 215 deletions

View File

@@ -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'

View File

@@ -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)

View File

@@ -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:

View File

@@ -19,6 +19,9 @@ class Chart:
'chart_const': self.chart_const
}
def to_tuple(self) -> tuple:
return (self.song_id, self.difficulty)
@property
def chart_const(self) -> float:
return self.defnum / 10 if self.defnum else -1

View File

@@ -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<index>: text, rating<index>: 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:

View File

@@ -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):

View File

@@ -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 != '']))

View File

@@ -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,

View File

@@ -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:

View File

@@ -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