From af03a481346e29e0e8ababc3e97f7b4ea4a8b261 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 13 Jan 2023 17:50:01 +0800 Subject: [PATCH 1/6] [Enhance] Cloud save update scores & capture auth error - Restore the feature that cloud save can be used to cover best scores - Capture error that the request does not have `Authorization` in header --- latest version/core/download.py | 2 +- latest version/core/operation.py | 107 ++++++++++++++++++++++++++++++- latest version/core/save.py | 16 ++++- latest version/core/score.py | 2 +- latest version/core/user.py | 9 +-- latest version/main.py | 6 ++ latest version/server/auth.py | 5 +- latest version/web/index.py | 9 ++- latest version/web/system.py | 55 ---------------- 9 files changed, 142 insertions(+), 69 deletions(-) diff --git a/latest version/core/download.py b/latest version/core/download.py index 45b6ab9..dfa9ff9 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -167,7 +167,7 @@ def hash(self) -> str: class DownloadList(UserDownload): ''' - 下载列表类\ + 下载列表类 properties: `user` - `User`类或子类的实例 ''' diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 8522f8b..5e34046 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -1,18 +1,20 @@ +from .save import SaveData from .sql import Connect, Sql from .score import Score from .download import DownloadList +from .user import User class BaseOperation: name: str = None - def __init__(self): + def __init__(self, *args, **kwargs): pass - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> None: return self.run(*args, **kwargs) - def run(self, *args, **kwargs): + def run(self, *args, **kwargs) -> None: raise NotImplementedError @@ -65,3 +67,102 @@ class RefreshSongFileCache(BaseOperation): def run(self): DownloadList.clear_all_cache() DownloadList.initialize_cache() + + +class SaveUpdateScore(BaseOperation): + ''' + 云存档更新成绩,是覆盖式更新\ + 提供user参数时,只更新该用户的成绩,否则更新所有用户的成绩 + ''' + name = 'save_update_score' + + def __init__(self, user=None): + self.user = user + + def run(self, user=None): + ''' + parameter: + `user` - `User`类或子类的实例 + ''' + if user is not None: + self.user = user + if self.user is not None and self.user.user_id is not None: + self._one_user_update() + else: + self._all_update() + + def _one_user_update(self): + with Connect() as c: + save = SaveData(c) + save.select_scores(self.user) + + clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type'] + for i in save.clearlamps_data} + + song_id_1 = [i['song_id'] for i in save.scores_data] + song_id_2 = [i['song_id'] for i in save.clearlamps_data] + song_id = list(set(song_id_1 + song_id_2)) + + c.execute( + f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id) + x = c.fetchall() + song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]] + for i in x} # chart const * 10 + + new_scores = [] + for i in save.scores_data: + rating = 0 + if i['song_id'] in song_chart_const: + rating = Score.calculate_rating( + song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + if rating < 0: + rating = 0 + + y = f'{i["song_id"]}{i["difficulty"]}' + if y in clear_state: + clear_type = clear_state[y] + else: + 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)) + + c.executemany( + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + + def _all_update(self): + with Connect() as c: + c.execute( + f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''') + song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]] + for i in c.fetchall()} # chart const * 10 + c.execute('''select user_id from user_save''') + for y in c.fetchall(): + user = User() + user.user_id = y[0] + save = SaveData(c) + save.select_scores(user) + + clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type'] + for i in save.clearlamps_data} + + new_scores = [] + for i in save.scores_data: + rating = 0 + if i['song_id'] in song_chart_const: + rating = Score.calculate_rating( + song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + if rating < 0: + rating = 0 + + y = f'{i["song_id"]}{i["difficulty"]}' + if y in clear_state: + clear_type = clear_state[y] + else: + 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)) + + c.executemany( + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) diff --git a/latest version/core/save.py b/latest version/core/save.py index ac7d7cd..bd6cccf 100644 --- a/latest version/core/save.py +++ b/latest version/core/save.py @@ -3,7 +3,7 @@ from .config_manager import Config from .constant import Constant -from .error import InputError +from .error import InputError, NoData from .util import md5 @@ -54,6 +54,20 @@ def to_dict(self): } } + def select_scores(self, user) -> None: + ''' + parameter: `user` - `User`类或子类的实例 + ''' + self.user = user + self.c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', + {'a': user.user_id}) + x = self.c.fetchone() + if not x: + raise NoData(f'User `{user.user_id}` has no cloud save data') + + self.scores_data: list = json.loads(x[0])[""] + self.clearlamps_data: list = json.loads(x[1])[""] + def select_all(self, user) -> None: ''' parameter: `user` - `User`类或子类的实例 diff --git a/latest version/core/score.py b/latest version/core/score.py index 9e8a209..36c1d5d 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -102,7 +102,7 @@ def is_valid(self) -> bool: return True @staticmethod - def calculate_rating(defnum: int, score: int) -> float: + def calculate_rating(defnum: float, score: int) -> float: '''计算rating,谱面定数小于等于0视为Unrank,返回值会为-1,这里的defnum = Chart const''' if not defnum or defnum <= 0: # 谱面没定数或者定数小于等于0被视作Unrank diff --git a/latest version/core/user.py b/latest version/core/user.py index 4b6ffd9..80251f7 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -243,8 +243,9 @@ def login(self, name: str = '', password: str = '', device_id: str = '', ip: str 'name': self.name}) x = self.c.fetchone() if x is None: - raise NoData('Username does not exist.', 104) + raise NoData(f'Username `{self.name}` does not exist.', 104) + self.user_id = x[0] self.now = int(time.time() * 1000) if x[2] is not None and x[2] != '': # 自动封号检查 @@ -255,14 +256,14 @@ def login(self, name: str = '', password: str = '', device_id: str = '', ip: str if x[1] == '': # 账号封禁 - raise UserBan('The account has been banned.', 106) + raise UserBan( + f'The account `{self.user_id}` has been banned.', 106) if x[1] != self.hash_pwd: raise NoAccess('Wrong password.', 104) - self.user_id = str(x[0]) self.token = base64.b64encode(hashlib.sha256( - (self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode() + (str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode() self.c.execute( '''select login_device from login where user_id = :user_id''', {"user_id": self.user_id}) diff --git a/latest version/main.py b/latest version/main.py index e0b72e3..19437b9 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -105,6 +105,12 @@ def after_request(response): f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}') return response +# @app.before_request +# def before_request(): +# print(request.path) +# print(request.headers) +# print(request.data) + def tcp_server_run(): if Config.DEPLOY_MODE == 'gevent': diff --git a/latest version/server/auth.py b/latest version/server/auth.py index efcdaf4..419b208 100644 --- a/latest version/server/auth.py +++ b/latest version/server/auth.py @@ -53,7 +53,10 @@ def wrapped_view(*args, **kwargs): with Connect() as c: try: user = UserAuth(c) - user.token = headers['Authorization'][7:] + token = headers.get('Authorization') + if not token: + raise NoAccess('No token.', -4) + user.token = token[7:] user_id = user.token_get_id() g.user = user except ArcError as e: diff --git a/latest version/web/index.py b/latest version/web/index.py index 9eb71f2..04cada1 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -2,9 +2,10 @@ import time from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshSongFileCache +from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore 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 @@ -937,7 +938,7 @@ def update_user_save(): # 全修改 if 'name' not in request.form and 'user_code' not in request.form: flag = False - web.system.update_all_save(c) + SaveUpdateScore().run() flash("全部用户存档同步成功 Successfully update all users' saves.") else: @@ -957,7 +958,9 @@ def update_user_save(): user_id = c.fetchone() if user_id: user_id = user_id[0] - web.system.update_one_save(c, user_id) + user = User() + user.user_id = user_id + SaveUpdateScore(user).run() flash("用户存档同步成功 Successfully update the user's saves.") else: diff --git a/latest version/web/system.py b/latest version/web/system.py index 90516c4..e35b57d 100644 --- a/latest version/web/system.py +++ b/latest version/web/system.py @@ -132,61 +132,6 @@ def get_all_purchase(): return re -def update_one_save(c, user_id): - # 同步指定用户存档 - # 注意,best_score表不比较,直接覆盖 - return - - # c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', { - # 'a': user_id}) - # x = c.fetchone() - # if x: - # scores = json.loads(x[0])[""] - # clearlamps = json.loads(x[1])[""] - # clear_song_id_difficulty = [] - # clear_state = [] - # for i in clearlamps: - # clear_song_id_difficulty.append(i['song_id']+str(i['difficulty'])) - # clear_state.append(i['clear_type']) - - # for i in scores: - # rating = server.arcscore.get_one_ptt( - # i['song_id'], i['difficulty'], i['score']) - # if rating < 0: - # rating = 0 - # try: - # index = clear_song_id_difficulty.index( - # i['song_id'] + str(i['difficulty'])) - # except: - # index = -1 - # if index != -1: - # clear_type = clear_state[index] - # else: - # clear_type = 0 - # c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', { - # 'a': user_id, 'b': i['song_id'], 'c': i['difficulty']}) - # c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', { - # 'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating}) - - # ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT - # c.execute('''update user set rating_ptt=:a where user_id=:b''', { - # 'a': ptt, 'b': user_id}) - - # return - - -def update_all_save(c): - # 同步所有用户存档 - - c.execute('''select user_id from user_save''') - x = c.fetchall() - if x: - for i in x: - update_one_save(c, i[0]) - - return - - def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount): # 添加一个奖励 From 9c90d6ef89c377a51f3ef785e55efd8f0a824009 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sat, 14 Jan 2023 18:32:53 +0800 Subject: [PATCH 2/6] [Enhance] Add an option about file hash pre-calc - Add an option to disable song file hash pre-calculation --- .gitignore | 5 ++++- latest version/core/config_manager.py | 2 ++ latest version/core/download.py | 19 +++++++++++-------- latest version/core/init.py | 2 ++ latest version/core/util.py | 6 ++---- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 3912616..7d6a9df 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ __pycache__/ # setting/config files latest version/config/ -latest version/config.py \ No newline at end of file +latest version/config.py + +# song data +latest version/database/songs/ diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 7c2ad2e..59359fb 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -10,6 +10,8 @@ class Config: USE_PROXY_FIX = False USE_CORS = False + SONG_FILE_HASH_PRE_CALCULATE = True + GAME_API_PREFIX = '/join/21' ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/download.py b/latest version/core/download.py index dfa9ff9..14f1edf 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -5,6 +5,7 @@ from flask import url_for +from .config_manager import Config from .constant import Constant from .error import NoAccess from .limiter import ArcLimiter @@ -184,10 +185,11 @@ def __init__(self, c_m=None, user=None) -> None: def initialize_cache(cls) -> None: '''初始化歌曲数据缓存,包括md5、文件目录遍历、解析songlist''' SonglistParser() - x = cls() - x.url_flag = False - x.add_songs() - del x + if Config.SONG_FILE_HASH_PRE_CALCULATE: + x = cls() + x.url_flag = False + x.add_songs() + del x @staticmethod def clear_all_cache() -> None: @@ -212,9 +214,10 @@ def insert_download_tokens(self) -> None: def get_one_song_file_names(song_id: str) -> list: '''获取一个歌曲文件夹下的所有合法文件名,有lru缓存''' r = [] - for i in os.listdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)): - if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and SonglistParser.is_available_file(song_id, i): - r.append(i) + for i in os.scandir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)): + file_name = i.name + if i.is_file() and SonglistParser.is_available_file(song_id, file_name): + r.append(file_name) return r def add_one_song(self, song_id: str) -> None: @@ -265,7 +268,7 @@ def add_one_song(self, song_id: str) -> None: @lru_cache() def get_all_song_ids() -> list: '''获取全歌曲文件夹列表,有lru缓存''' - return list(filter(lambda x: os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, x)), os.listdir(Constant.SONG_FILE_FOLDER_PATH))) + return [i.name for i in os.scandir(Constant.SONG_FILE_FOLDER_PATH) if i.is_dir()] def add_songs(self, song_ids: list = None) -> None: '''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲''' diff --git a/latest version/core/init.py b/latest version/core/init.py index 9bf9599..ce3eb13 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -286,6 +286,8 @@ def check_song_file(self) -> bool: self.logger.info("Start to initialize song data...") try: DownloadList.initialize_cache() + if not Config.SONG_FILE_HASH_PRE_CALCULATE: + self.logger.info('Song file hash pre-calculate is disabled.') self.logger.info('Complete!') except Exception as e: self.logger.error(format_exc()) diff --git a/latest version/core/util.py b/latest version/core/util.py index 48a6aca..b0ca0b8 100644 --- a/latest version/core/util.py +++ b/latest version/core/util.py @@ -13,13 +13,11 @@ def md5(code: str) -> str: def get_file_md5(file_path: str) -> str: - '''计算文件MD5''' - if not os.path.isfile(file_path): - return None + '''计算文件MD5,假设是文件''' myhash = hashlib.md5() with open(file_path, 'rb') as f: while True: - b = f.read(8096) + b = f.read(8192) if not b: break myhash.update(b) From 9636722709c1fee39a9605dfd0fd6c93ec75f22c Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 22 Jan 2023 18:39:15 +0800 Subject: [PATCH 3/6] [Enhance] Add API about songs - Add some API endpoints, including creating, changing, deleting song info. --- latest version/api/api_auth.py | 15 +++++++--- latest version/api/api_code.py | 1 + latest version/api/songs.py | 49 ++++++++++++++++++++++++++++++++- latest version/api/users.py | 5 ++-- latest version/core/api_user.py | 5 ++-- latest version/core/error.py | 4 ++- latest version/core/song.py | 33 ++++++++++++++++++++-- 7 files changed, 99 insertions(+), 13 deletions(-) diff --git a/latest version/api/api_auth.py b/latest version/api/api_auth.py index f71d7cf..3ac4a67 100644 --- a/latest version/api/api_auth.py +++ b/latest version/api/api_auth.py @@ -48,13 +48,14 @@ def wrapped_view(*args, **kwargs): return decorator -def request_json_handle(request, required_keys=[], optional_keys=[]): +def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False): ''' 提取post参数,返回dict,写成了修饰器\ parameters: \ `request`: `Request` - 当前请求\ `required_keys`: `list` - 必须的参数\ - `optional_keys`: `list` - 可选的参数 + `optional_keys`: `list` - 可选的参数\ + `must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改 ''' def decorator(view): @@ -67,8 +68,11 @@ def wrapped_view(*args, **kwargs): else: if request.method == 'GET' and 'query' in request.args: # 处理axios没法GET传data的问题 - json_data = loads( - b64decode(request.args['query']).decode()) + try: + json_data = loads( + b64decode(request.args['query']).decode()) + except: + raise PostError(api_error_code=-105) else: json_data = {} @@ -81,6 +85,9 @@ def wrapped_view(*args, **kwargs): if key in json_data: data[key] = json_data[key] + if must_change and not data: + return error_return(PostError('No change', api_error_code=-100)) + return view(data, *args, **kwargs) return wrapped_view diff --git a/latest version/api/api_code.py b/latest version/api/api_code.py index ded765a..27875ee 100644 --- a/latest version/api/api_code.py +++ b/latest version/api/api_code.py @@ -9,6 +9,7 @@ -1: 'See status code', # 基础错误 -2: 'No data', -3: 'No data or user', # 不确定是无数据还是无用户 + -4: 'Data exist', -100: 'Invalid post data', # 1xx数据错误 -101: 'Invalid data type', -102: 'Invalid query parameter', diff --git a/latest version/api/songs.py b/latest version/api/songs.py index 130653e..0742554 100644 --- a/latest version/api/songs.py +++ b/latest version/api/songs.py @@ -1,4 +1,4 @@ -from core.error import NoData, InputError +from core.error import DataExist, NoData, InputError from core.rank import RankList from core.song import Song from core.sql import Connect, Query, Sql @@ -21,6 +21,39 @@ def songs_song_get(user, song_id): return success_return(s.to_dict()) +@bp.route('/', methods=['PUT']) +@role_required(request, ['change']) +@request_json_handle(request, optional_keys=['name', 'charts'], must_change=True) +@api_try +def songs_song_put(data, user, song_id): + '''修改歌曲信息''' + with Connect() as c: + s = Song(c, song_id).select() + if 'name' in data: + s.name = str(data['name']) + if 'charts' in data: + for i in data['charts']: + if 'difficulty' in i and 'chart_const' in i: + s.charts[i['difficulty']].defnum = round( + i['chart_const'] * 10) + + s.update() + return success_return(s.to_dict()) + + +@bp.route('/', methods=['DELETE']) +@role_required(request, ['change']) +@api_try +def songs_song_delete(user, song_id): + '''删除歌曲信息''' + with Connect() as c: + s = Song(c, song_id) + if not s.select_exists(): + raise NoData(f'No such song: `{song_id}`') + s.delete() + return success_return() + + @bp.route('', methods=['GET']) @role_required(request, ['select', 'select_song_info']) @request_json_handle(request, optional_keys=Constant.QUERY_KEYS) @@ -43,6 +76,20 @@ def songs_get(data, user): return success_return([x.to_dict() for x in r]) +@bp.route('', methods=['POST']) +@role_required(request, ['change']) +@request_json_handle(request, ['song_id', 'charts'], ['name']) +@api_try +def songs_post(data, user): + '''添加歌曲信息''' + with Connect() as c: + s = Song(c).from_dict(data) + if s.select_exists(): + raise DataExist(f'Song `{s.song_id}` already exists') + s.insert() + return success_return(s.to_dict()) + + @bp.route('///rank', methods=['GET']) @role_required(request, ['select', 'select_song_rank', 'select_song_rank_top']) @request_json_handle(request, optional_keys=['limit']) diff --git a/latest version/api/users.py b/latest version/api/users.py index b247d5c..8962e39 100644 --- a/latest version/api/users.py +++ b/latest version/api/users.py @@ -78,7 +78,7 @@ def users_user_get(user, user_id): @bp.route('/', methods=['PUT']) @role_required(request, ['change']) -@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email']) +@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'], must_change=True) @api_try def users_user_put(data, user, user_id): '''修改一个用户''' @@ -103,8 +103,7 @@ def users_user_put(data, user, user_id): raise InputError('Ticket must be int') u.ticket = data['ticket'] r['ticket'] = u.ticket - if r: - u.update_columns(d=r) + u.update_columns(d=r) return success_return(r) diff --git a/latest version/core/api_user.py b/latest version/core/api_user.py index b953a91..ec0ba03 100644 --- a/latest version/core/api_user.py +++ b/latest version/core/api_user.py @@ -134,11 +134,12 @@ def login(self, name: str = None, password: str = None, ip: str = None) -> None: x = self.c.fetchone() if x is None: raise NoData('The user `%s` does not exist.' % - self.name, api_error_code=-201) + self.name, api_error_code=-201, status=401) if x[1] == '': raise UserBan('The user `%s` is banned.' % self.name) if self.hash_pwd != x[1]: - raise NoAccess('The password is incorrect.', api_error_code=-201) + raise NoAccess('The password is incorrect.', + api_error_code=-201, status=401) self.user_id = x[0] now = int(time() * 1000) diff --git a/latest version/core/error.py b/latest version/core/error.py index c9ada67..439cdec 100644 --- a/latest version/core/error.py +++ b/latest version/core/error.py @@ -19,7 +19,9 @@ def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data class DataExist(ArcError): '''数据存在''' - pass + + def __init__(self, message=None, error_code=108, api_error_code=-4, extra_data=None, status=200) -> None: + super().__init__(message, error_code, api_error_code, extra_data, status) class NoData(ArcError): diff --git a/latest version/core/song.py b/latest version/core/song.py index a61b04f..bea9d9d 100644 --- a/latest version/core/song.py +++ b/latest version/core/song.py @@ -43,7 +43,7 @@ def select(self) -> None: class Song: - def __init__(self, c=None, song_id=None) -> None: + def __init__(self, c=None, song_id: str = None) -> None: self.c = c self.song_id: str = song_id self.name: str = None @@ -68,10 +68,39 @@ def from_list(self, x: list) -> 'Song': self.charts[3].defnum = x[5] return self + def from_dict(self, d: dict) -> 'Song': + self.song_id = d['song_id'] + self.name = d.get('name', '') + self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart( + self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)] + for i in range(4): + self.charts[i].defnum = -10 + for chart in d['charts']: + self.charts[chart['difficulty']].defnum = round( + chart['chart_const'] * 10) + return self + + def delete(self) -> None: + self.c.execute( + '''delete from chart where song_id=?''', (self.song_id,)) + + def update(self) -> None: + '''全部更新''' + self.c.execute( + '''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.song_id)) + def insert(self) -> None: self.c.execute( '''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum)) + def select_exists(self, song_id: str = None) -> bool: + if song_id is not None: + self.song_id = song_id + + self.c.execute( + '''select exists(select * from chart where song_id=?)''', (self.song_id,)) + return bool(self.c.fetchone()[0]) + def select(self, song_id: str = None) -> 'Song': if song_id is not None: self.song_id = song_id @@ -80,6 +109,6 @@ def select(self, song_id: str = None) -> 'Song': 'a': self.song_id}) x = self.c.fetchone() if x is None: - raise NoData('The song `%s` does not exist.' % self.song_id) + raise NoData(f'The song `{self.song_id}` does not exist.') return self.from_list(x) From 88d949fc1872ce2acd30c74b9d00ea7f9d62d851 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 22 Jan 2023 22:09:15 +0800 Subject: [PATCH 4/6] [Bug fix] block headers without app version - Fix a bug that headers without `AppVersion` are allowed in client version checking. --- latest version/server/auth.py | 14 ++++++-------- latest version/server/user.py | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/latest version/server/auth.py b/latest version/server/auth.py index 419b208..ce51f85 100644 --- a/latest version/server/auth.py +++ b/latest version/server/auth.py @@ -16,10 +16,9 @@ @arc_try def login(): headers = request.headers - if 'AppVersion' in headers: # 版本检查 - if Config.ALLOW_APPVERSION: - if headers['AppVersion'] not in Config.ALLOW_APPVERSION: - raise NoAccess('Wrong app version.', 1203) + if Config.ALLOW_APPVERSION: # 版本检查 + if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: + raise NoAccess('Invalid app version.', 1203) request.form['grant_type'] with Connect() as c: @@ -45,10 +44,9 @@ def wrapped_view(*args, **kwargs): headers = request.headers - if 'AppVersion' in headers: # 版本检查 - if Config.ALLOW_APPVERSION: - if headers['AppVersion'] not in Config.ALLOW_APPVERSION: - return error_return(NoAccess('Wrong app version.', 1203)) + if Config.ALLOW_APPVERSION: # 版本检查 + if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: + return error_return(NoAccess('Invalid app version.', 1203)) with Connect() as c: try: diff --git a/latest version/server/user.py b/latest version/server/user.py index 655456a..b069ecd 100644 --- a/latest version/server/user.py +++ b/latest version/server/user.py @@ -16,10 +16,10 @@ @bp.route('', methods=['POST']) # 注册接口 @arc_try def register(): - if 'AppVersion' in request.headers: # 版本检查 - if Config.ALLOW_APPVERSION: - if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION: - raise NoAccess('Wrong app version.', 1203) + headers = request.headers + if Config.ALLOW_APPVERSION: # 版本检查 + if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: + raise NoAccess('Invalid app version.', 1203) with Connect() as c: new_user = UserRegister(c) From 9fbdcd5edbdab628dc17d8bf7db81c3bbf9aa0cf Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 24 Jan 2023 16:45:09 +0800 Subject: [PATCH 5/6] [Refactor][Enhance] unlock items & Steps' difficulty restrict - Refactor some codes about unlocking or locking some users' packs and singles - Add support for restricting songs' difficulty in the map's steps of world mode --- latest version/core/operation.py | 88 +++++++++++++++++++++++++++++--- latest version/core/world.py | 12 +++-- latest version/web/index.py | 8 +-- latest version/web/system.py | 35 ------------- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 5e34046..bd52bab 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -1,12 +1,12 @@ +from .download import DownloadList from .save import SaveData -from .sql import Connect, Sql from .score import Score -from .download import DownloadList +from .sql import Connect, Sql from .user import User class BaseOperation: - name: str = None + _name: str = None def __init__(self, *args, **kwargs): pass @@ -14,6 +14,9 @@ def __init__(self, *args, **kwargs): def __call__(self, *args, **kwargs) -> None: return self.run(*args, **kwargs) + def set_params(self, *args, **kwargs) -> None: + pass + def run(self, *args, **kwargs) -> None: raise NotImplementedError @@ -22,7 +25,7 @@ class RefreshAllScoreRating(BaseOperation): ''' 刷新所有成绩的评分 ''' - name = 'refresh_all_score_rating' + _name = 'refresh_all_score_rating' def run(self): # 追求效率,不用Song类,尽量不用对象 @@ -61,8 +64,9 @@ def run(self): class RefreshSongFileCache(BaseOperation): ''' 刷新歌曲文件缓存,包括文件hash缓存重建、文件目录重遍历、songlist重解析 + 注意在设置里预先计算关闭的情况下,文件hash不会计算 ''' - name = 'refresh_song_file_cache' + _name = 'refresh_song_file_cache' def run(self): DownloadList.clear_all_cache() @@ -74,11 +78,16 @@ class SaveUpdateScore(BaseOperation): 云存档更新成绩,是覆盖式更新\ 提供user参数时,只更新该用户的成绩,否则更新所有用户的成绩 ''' - name = 'save_update_score' + _name = 'save_update_score' def __init__(self, user=None): self.user = user + def set_params(self, user_id: int = None, *args, **kwargs): + if user_id is not None: + self.user = User() + self.user.user_id = int(user_id) + def run(self, user=None): ''' parameter: @@ -166,3 +175,70 @@ def _all_update(self): c.executemany( '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + + +class UnlockUserItem(BaseOperation): + ''' + 全解锁/锁定用户物品\ + 提供user参数时,只更新该用户的,否则更新所有用户的 + ''' + _name = 'unlock_user_item' + + ALLOW_TYPES = ['single', 'pack', 'world_song', + 'course_banner', 'world_unlock'] + + def __init__(self, user=None, method: str = 'unlock', item_types: list = ['single', 'pack']): + self.user = user + self.set_params(method=method, item_types=item_types) + + def set_params(self, user_id: int = None, method: str = 'unlock', item_types: list = ['single', 'pack'], *args, **kwargs): + if user_id is not None: + self.user = User() + self.user.user_id = int(user_id) + if method in ['unlock', 'lock']: + self.method = method + if isinstance(item_types, list) and all([i in self.ALLOW_TYPES for i in item_types]): + self.item_types = item_types + + def run(self): + if self.user is not None and self.user.user_id is not None: + if self.method == 'unlock': + self._one_user_insert() + else: + self._one_user_delete() + else: + if self.method == 'unlock': + self._all_insert() + else: + self._all_delete() + + def _one_user_insert(self): + with Connect() as c: + c.execute( + f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types) + sql_list = [(self.user.user_id, i[0], i[1]) + for i in c.fetchall()] + c.executemany( + '''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list) + + def _all_insert(self): + with Connect() as c: + c.execute('''select user_id from user''') + x = c.fetchall() + c.execute( + f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types) + y = c.fetchall() + sql_list = [(i[0], j[0], j[1]) + for i in x for j in y] + c.executemany( + '''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list) + + def _one_user_delete(self): + with Connect() as c: + c.execute( + f'''delete from user_item where user_id = ? and type in ({','.join(['?'] * len(self.item_types))})''', (self.user.user_id, *self.item_types)) + + def _all_delete(self): + with Connect() as c: + c.execute( + f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types) diff --git a/latest version/core/world.py b/latest version/core/world.py index c8aa274..03872c2 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -50,6 +50,7 @@ def __init__(self) -> None: self.restrict_id: str = None self.restrict_ids: list = [] self.restrict_type: str = None + self.restrict_difficulty: int = None self.step_type: list = None self.speed_limit_value: int = None self.plus_stamina_value: int = None @@ -61,12 +62,14 @@ def to_dict(self) -> dict: } if self.items: r['items'] = [i.to_dict() for i in self.items] - if self.restrict_id: - r['restrict_id'] = self.restrict_id - if self.restrict_ids: - r['restrict_ids'] = self.restrict_ids if self.restrict_type: r['restrict_type'] = self.restrict_type + if self.restrict_id: + r['restrict_id'] = self.restrict_id + if self.restrict_ids: + r['restrict_ids'] = self.restrict_ids + if self.restrict_difficulty is not None: + r['restrict_difficulty'] = self.restrict_difficulty if self.step_type: r['step_type'] = self.step_type if self.speed_limit_value: @@ -82,6 +85,7 @@ def from_dict(self, d: dict) -> 'Step': self.restrict_id = d.get('restrict_id') self.restrict_ids = d.get('restrict_ids') self.restrict_type = d.get('restrict_type') + self.restrict_difficulty = d.get('restrict_difficulty') self.step_type = d.get('step_type') self.speed_limit_value = d.get('speed_limit_value') self.plus_stamina_value = d.get('plus_stamina_value') diff --git a/latest version/web/index.py b/latest version/web/index.py index 04cada1..68729bf 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -2,7 +2,7 @@ import time from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore +from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem from core.rank import RankList from core.sql import Connect from core.user import User @@ -606,7 +606,7 @@ def edit_user_purchase(): if 'name' not in request.form and 'user_code' not in request.form: flag = False if method == '0': - web.system.unlock_all_user_item(c) + UnlockUserItem().run() else: c.execute( '''delete from user_item where type in ('pack', 'single')''') @@ -632,7 +632,9 @@ def edit_user_purchase(): user_id = user_id[0] if method == '0': - web.system.unlock_user_item(c, user_id) + x = UnlockUserItem() + x.set_params(user_id=user_id) + x.run() else: c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', { 'user_id': user_id}) diff --git a/latest version/web/system.py b/latest version/web/system.py index e35b57d..8d5c4a2 100644 --- a/latest version/web/system.py +++ b/latest version/web/system.py @@ -40,41 +40,6 @@ def update_user_char(c): (j[0], i[0], i[1], exp, i[2], 0)) -def unlock_all_user_item(c): - # 解锁所有用户购买 - - c.execute('''select user_id from user''') - x = c.fetchall() - c.execute('''select item_id, type from purchase_item''') - y = c.fetchall() - if x and y: - for i in x: - for j in y: - c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', { - 'a': i[0], 'b': j[0], 'c': j[1]}) - if c.fetchone() == (0,) and j[1] != 'character': - c.execute('''insert into user_item values(:a,:b,:c,1)''', { - 'a': i[0], 'b': j[0], 'c': j[1]}) - - return - - -def unlock_user_item(c, user_id): - # 解锁用户购买 - - c.execute('''select item_id, type from purchase_item''') - y = c.fetchall() - - for j in y: - c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', { - 'a': user_id, 'b': j[0], 'c': j[1]}) - if c.fetchone() == (0,) and j[1] != 'character': - c.execute('''insert into user_item values(:a,:b,:c,1)''', { - 'a': user_id, 'b': j[0], 'c': j[1]}) - - return - - def get_all_item(): # 所有物品数据查询 with Connect() as c: From fbd5d8362681a40c4c52de446f19de0e1c93e396 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Thu, 26 Jan 2023 21:15:09 +0800 Subject: [PATCH 6/6] Update to v2.10.3 [Enhance] map local restrict - Add support for locally restricting songs or challenges in the map of world mode --- README.md | 22 +-- latest version/api/api_code.py | 3 +- latest version/api/songs.py | 5 +- latest version/core/constant.py | 2 +- latest version/core/purchase.py | 2 + latest version/core/world.py | 9 +- latest version/database/init/arc_data.py | 16 +- latest version/database/init/packs.json | 30 +++- latest version/database/init/singles.json | 198 +++++++++++++--------- latest version/web/index.py | 2 +- 10 files changed, 180 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index d4600b7..cd6bb35 100644 --- a/README.md +++ b/README.md @@ -73,17 +73,17 @@ It is just so interesting. What it can do is under exploration. > > Tips: When updating, please keep the original database in case of data loss. -### Version 2.10.2 - -- 适用于Arcaea 4.1.7版本 For Arcaea 4.1.7 -- 新搭档 **红(冬日)** 已解锁 Unlock the character **Kou(Winter)**. -- 新增记录数据库来记录全部的游玩历史分数 Add a log database to record all playing scores. -- 新增设置选项,可选择阻止或接受unranked成绩 Add a config option that can be used to forbid unranked scores. -- 为自定义异常添加简明的warning日志 Add brief warning logs for custom exceptions. -- 修复flask应用启动前出现异常,日志无法正确地指出异常的问题 Fix a bug that if an exception is raised before flask app runs, logger will not work well. -- 现在初始化文件中JSON文件可以是模块支持的其它编码格式 Now initial files can be other encoding types which are supported by JSON module. -- `run.bat`在报错时会停下而不是一闪而过了 Make the `run.bat` script pause when meeting an error. #82 -- 新增API接口查询单谱排行 Add an API endpoint for getting the rank list of a song's chart. #81 +### Version 2.10.3 + +- 适用于Arcaea 4.2.0版本 For Arcaea 4.2.0 +- 新搭档 **拉格兰(Aria)** 已解锁 Unlock the character **Lagrange(Aria)**. (Lack of its values) +- 新搭档 **忘却(Apophenia)** 已解锁 Unlock the character **Lethe(Apophenia)**. +- 新增选项取消歌曲文件哈希预计算 Add an option to disable song file hash pre-calculation. +- 新增对世界模式中地图本地限制歌曲解锁或挑战解锁以及地图中台阶上限制歌曲难度的支持 Add support for restricting songs' difficulty in the map's steps of world mode and locally restricting unlocking songs or challenges in the map of world mode. +- 恢复使用云存档覆盖成绩的功能 Restore the feature that cloud save can be used to cover best scores. +- 捕获`Authorization`不在请求头导致的报错 Capture error that the request does not have `Authorization` in header. +- 修复客户端版本校验中请求头不存在`AppVersion`也能通过校验的逻辑错误 Fix a bug that headers without `AppVersion` are allowed in client version checking. +- 新增增删改歌曲信息的API接口 Add some API endpoints, including creating, changing, deleting song info. ## 运行环境与依赖 Running environment and requirements diff --git a/latest version/api/api_code.py b/latest version/api/api_code.py index 27875ee..a0fbf8c 100644 --- a/latest version/api/api_code.py +++ b/latest version/api/api_code.py @@ -1,6 +1,7 @@ -from core.error import ArcError from flask import jsonify +from core.error import ArcError + default_error = ArcError('Unknown Error') diff --git a/latest version/api/songs.py b/latest version/api/songs.py index 0742554..23b2856 100644 --- a/latest version/api/songs.py +++ b/latest version/api/songs.py @@ -1,8 +1,9 @@ -from core.error import DataExist, NoData, InputError +from flask import Blueprint, request + +from core.error import DataExist, InputError, NoData from core.rank import RankList from core.song import Song from core.sql import Connect, Query, Sql -from flask import Blueprint, request from .api_auth import api_try, request_json_handle, role_required from .api_code import success_return diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 6f47a33..0cce9c4 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.10.2' +ARCAEA_SERVER_VERSION = 'v2.10.3' class Constant: diff --git a/latest version/core/purchase.py b/latest version/core/purchase.py index 082ab46..27feeab 100644 --- a/latest version/core/purchase.py +++ b/latest version/core/purchase.py @@ -22,6 +22,8 @@ def __init__(self, c=None, user=None): self.items: list = [] + # TODO: "discount_reason": "extend" + @property def price_displayed(self) -> int: ''' diff --git a/latest version/core/world.py b/latest version/core/world.py index 03872c2..1b7c3cf 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -106,7 +106,7 @@ def __init__(self, map_id: str = None) -> None: self.available_from: int = None self.available_to: int = None self.is_repeatable: bool = None - self.require_id: str = None + self.require_id: 'str | list[str]' = None self.require_type: str = None self.require_value: int = None self.coordinate: str = None @@ -115,6 +115,9 @@ def __init__(self, map_id: str = None) -> None: self.steps: list = [] self.__rewards: list = None + self.require_localunlock_songid: str = None + self.require_localunlock_challengeid: str = None + @property def rewards(self) -> list: if self.__rewards is None: @@ -155,6 +158,8 @@ def to_dict(self) -> dict: 'custom_bg': self.custom_bg, 'stamina_cost': self.stamina_cost, 'step_count': self.step_count, + 'require_localunlock_songid': self.require_localunlock_songid, + 'require_localunlock_challengeid': self.require_localunlock_challengeid, 'steps': [s.to_dict() for s in self.steps], } @@ -174,6 +179,8 @@ def from_dict(self, raw_dict: dict) -> 'Map': self.coordinate = raw_dict.get('coordinate') self.custom_bg = raw_dict.get('custom_bg', '') self.stamina_cost = raw_dict.get('stamina_cost') + self.require_localunlock_songid = raw_dict.get('require_localunlock_songid', '') + self.require_localunlock_challengeid = raw_dict.get('require_localunlock_challengeid', '') self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')] return self diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index a2eabaa..e985bb1 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -1,24 +1,24 @@ class InitData: char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin', - 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)'] + 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)'] skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0] + 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0] frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, - 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30] + 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 0, 57] prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, - 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50] + 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 0, 70] overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, - 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50] + 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 0, 57] frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50] @@ -64,7 +64,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase'] 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'] + "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'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index 46b30bd..1c4297a 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -327,11 +327,10 @@ "is_available": true } ], - "price": 0, + "price": 700, "orig_price": 700, "discount_from": 1646784000000, - "discount_to": 1647388799000, - "discount_reason": "extend" + "discount_to": 1647388799000 }, { "name": "alice", @@ -595,6 +594,11 @@ "id": "finale", "is_available": true }, + { + "type": "pack", + "id": "epilogue", + "is_available": true + }, { "type": "core", "amount": 5, @@ -622,5 +626,25 @@ ], "orig_price": 500, "price": 500 + }, + { + "name": "extend_2", + "items": [ + { + "type": "pack", + "id": "extend_2", + "is_available": true + }, + { + "type": "core", + "amount": 7, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 700, + "price": 700, + "discount_from": 1646784000000, + "discount_to": 1647388799000 } ] \ No newline at end of file diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index c6c04ae..1707ea5 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -28,8 +28,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "yourvoiceso", @@ -48,8 +48,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "crosssoul", @@ -68,8 +68,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "impurebird", @@ -88,8 +88,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "auxesia", @@ -128,8 +128,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "yozakurafubuki", @@ -148,8 +148,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "surrender", @@ -168,8 +168,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "metallicpunisher", @@ -224,8 +224,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "callmyname", @@ -244,8 +244,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "fallensquare", @@ -264,8 +264,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "dropdead", @@ -304,8 +304,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "astraltale", @@ -324,8 +324,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "phantasia", @@ -380,8 +380,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "dottodot", @@ -400,8 +400,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "dreadnought", @@ -420,8 +420,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "mirzam", @@ -440,8 +440,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "heavenlycaress", @@ -460,8 +460,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "filament", @@ -480,8 +480,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "avantraze", @@ -518,8 +518,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "saikyostronger", @@ -556,8 +556,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "einherjar", @@ -576,8 +576,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "laqryma", @@ -596,8 +596,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "amygdata", @@ -616,8 +616,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "altale", @@ -636,8 +636,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "feelssoright", @@ -656,8 +656,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "scarletcage", @@ -712,8 +712,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "badtek", @@ -732,8 +732,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "maliciousmischance", @@ -788,8 +788,8 @@ ], "price": 100, "orig_price": 100, - "discount_from": 1615248000000, - "discount_to": 1615852799000 + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "xeraphinite", @@ -807,7 +807,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "xanatos", @@ -825,7 +827,9 @@ } ], "price": 100, - "orig_price": 100 + "orig_price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "attraqtia", @@ -843,7 +847,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "gimmedablood", @@ -879,7 +885,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "theultimacy", @@ -897,7 +905,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "rekkaresonanc", @@ -945,7 +955,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "eveninginscarlet", @@ -981,7 +993,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "goldenslaughterer", @@ -999,7 +1013,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "redolentshape", @@ -1017,7 +1033,9 @@ } ], "orig_price": 100, - "price": 100 + "price": 100, + "discount_from": 1646784000000, + "discount_to": 1647388799000 }, { "name": "summerfireworks", @@ -1056,11 +1074,11 @@ "price": 100 }, { - "name": "internetoverdose", + "name": "kissinglucifer", "items": [ { "type": "single", - "id": "internetoverdose", + "id": "kissinglucifer", "is_available": true }, { @@ -1074,11 +1092,11 @@ "price": 100 }, { - "name": "macromod", + "name": "neowings", "items": [ { "type": "single", - "id": "macromod", + "id": "neowings", "is_available": true }, { @@ -1092,11 +1110,11 @@ "price": 100 }, { - "name": "neowings", + "name": "macromod", "items": [ { "type": "single", - "id": "neowings", + "id": "macromod", "is_available": true }, { @@ -1110,11 +1128,11 @@ "price": 100 }, { - "name": "kissinglucifer", + "name": "internetoverdose", "items": [ { "type": "single", - "id": "kissinglucifer", + "id": "internetoverdose", "is_available": true }, { @@ -1128,11 +1146,11 @@ "price": 100 }, { - "name": "headbonkache", + "name": "aurgelmir", "items": [ { "type": "single", - "id": "headbonkache", + "id": "aurgelmir", "is_available": true }, { @@ -1146,11 +1164,11 @@ "price": 100 }, { - "name": "aurgelmir", + "name": "headbonkache", "items": [ { "type": "single", - "id": "aurgelmir", + "id": "headbonkache", "is_available": true }, { @@ -1218,11 +1236,11 @@ "price": 100 }, { - "name": "freemyself", + "name": "cocorocosmetic", "items": [ { "type": "single", - "id": "freemyself", + "id": "cocorocosmetic", "is_available": true }, { @@ -1236,11 +1254,11 @@ "price": 100 }, { - "name": "cocorocosmetic", + "name": "freemyself", "items": [ { "type": "single", - "id": "cocorocosmetic", + "id": "freemyself", "is_available": true }, { @@ -1270,5 +1288,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "nullapophenia", + "items": [ + { + "type": "single", + "id": "nullapophenia", + "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/web/index.py b/latest version/web/index.py index 68729bf..5398d07 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -422,7 +422,7 @@ def all_character(): def change_character(): # 修改角色数据 skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror'] return render_template('web/changechar.html', skill_ids=skill_ids)