21 Commits

Author SHA1 Message Date
Lost-MSth
70d27be7c7 Merge pull request #94 from Lost-MSth/dev
Update to v2.11.0
2023-03-05 22:58:19 +08:00
Lost-MSth
8d856696ca Update to v2.11.0
- Change characters' value
2023-03-05 22:56:55 +08:00
Lost-MSth
ed8d0aa73c [Enhance] BYD boost & BYD chain maps
- Add support for beyond gauge boost
- Add support for beyond chain maps
- Add support skills of uncapped ilith and mika
- Unlock four chars and uncapped ilith
- Some changes in some values' algorithms
2023-03-02 23:25:38 +08:00
Lost-MSth
a84ec560dd [Enhance] API for redeem
- Add API endpoints for redeem system
- Continue to fix the bug mentioned in 930faf508d
2023-02-28 18:28:09 +08:00
Lost-MSth
930faf508d [Refactor] Refactor for link play
- Refactor simply for link play subprogram
- Fix a logic bug that the room without anyone can be entered
2023-02-27 23:41:32 +08:00
Lost-MSth
7ece2598d1 [Enhance] API for presents & character values
- Add API endpoints for presents
- Change character value algorithm
- Update character values (I forgot in v2.10.3)
2023-02-10 18:15:53 +08:00
Lost-MSth
6f39274b99 [Enhance] API for purchases, items, operations
- Add API endpoints for purchases, items, and operations
- Header checker? :)
2023-02-08 18:18:04 +08:00
Lost-MSth
e21cf89b1b Merge pull request #90 from Lost-MSth/dev
Dev
2023-01-26 21:19:32 +08:00
Lost-MSth
fbd5d83626 Update to v2.10.3 [Enhance] map local restrict
- Add support for locally restricting songs or challenges in the map of world mode
2023-01-26 21:15:09 +08:00
Lost-MSth
9fbdcd5edb [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
2023-01-24 16:45:09 +08:00
Lost-MSth
88d949fc18 [Bug fix] block headers without app version
- Fix a bug that headers without `AppVersion` are allowed in client version checking.
2023-01-22 22:09:15 +08:00
Lost-MSth
9636722709 [Enhance] Add API about songs
- Add some API endpoints, including creating, changing, deleting song info.
2023-01-22 18:39:15 +08:00
Lost-MSth
9c90d6ef89 [Enhance] Add an option about file hash pre-calc
- Add an option to disable song file hash pre-calculation
2023-01-14 18:32:53 +08:00
Lost-MSth
af03a48134 [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
2023-01-13 17:50:01 +08:00
Lost-MSth
1fde2801ad Merge pull request #84 from Lost-MSth/dev
Update to v2.10.2
2022-12-22 16:06:02 +08:00
Lost-MSth
f250701eca Update to v2.10.2
close #77 #83 #78 #64 #80 #81 #83
2022-12-22 16:02:44 +08:00
Lost-MSth
627985d1af [Doc] Game download link & some small changes 2022-12-19 20:38:37 +08:00
Lost-MSth
7324c9bd76 [Enhance] API song ranklist
- Add an API endpoint for getting the rank list of a song's chart #81
- Make the `run.bat` script pause when meeting an error #82
2022-12-19 20:20:31 +08:00
Lost-MSth
426f65ea9e [Enhance][Refactor] unranked score & warning log
- Add an option that can be used to forbid some illegal scores
- Add brief warning logs for custom exceptions
2022-11-28 21:58:06 +08:00
Lost-MSth
a7a9a4ba3d [Enhance][Bug fix] Add log db & log bug
- Add a log database to record all playing scores
- Fix a bug that if an exception is raised before flask app runs, logger will not work well.
2022-11-26 21:03:29 +08:00
Lost-MSth
84b0e869a5 [Refactor] Init files Encoding & Refresh rating
- Now initial files can be other encoding types which are supported by JSON module
- Code refactor for refreshing all scores' rating
2022-11-24 21:40:44 +08:00
57 changed files with 2457 additions and 995 deletions

5
.gitignore vendored
View File

@@ -17,4 +17,7 @@ __pycache__/
# setting/config files # setting/config files
latest version/config/ latest version/config/
latest version/config.py latest version/config.py
# song data
latest version/database/songs/

View File

@@ -1,7 +1,9 @@
# Arcaea-server # Arcaea-server
一个微型的Arcaea本地服务器 A small local server for Arcaea 一个微型的Arcaea本地服务器 A small local server for Arcaea
## 简介 Introduction ## 简介 Introduction
这是基于Python以及Flask的微型本地Arcaea服务器可以模拟游戏的主要功能。这可能是我第一次写这种大程序若有不妥之处敬请谅解。 这是基于Python以及Flask的微型本地Arcaea服务器可以模拟游戏的主要功能。这可能是我第一次写这种大程序若有不妥之处敬请谅解。
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。 本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
@@ -14,7 +16,9 @@ This procedure is mainly used for study and research, and shall not be used for
> It looks stupid, but it works! > It looks stupid, but it works!
## 特性 Features ## 特性 Features
有以下 We have 有以下 We have
- 登录、注册 Login and registration - 登录、注册 Login and registration
- 多设备登录 Multi device login - 多设备登录 Multi device login
- 成绩上传 Score upload - 成绩上传 Score upload
@@ -24,7 +28,7 @@ This procedure is mainly used for study and research, and shall not be used for
- 段位系统 Course system - 段位系统 Course system
- Link Play - Link Play
- 好友系统 Friends - 好友系统 Friends
- 数据同步 Data synchronization - 云端存档 Cloud save
- 爬梯 Climbing steps - 爬梯 Climbing steps
- 自定义世界模式 Customizable World Mode - 自定义世界模式 Customizable World Mode
- 自定义歌曲下载 Customizable songs download - 自定义歌曲下载 Customizable songs download
@@ -40,60 +44,50 @@ This procedure is mainly used for study and research, and shall not be used for
- 服务器日志 Server log - 服务器日志 Server log
没有以下 We don't have 没有以下 We don't have
- 服务器安全性保证 Server security assurance - 服务器安全性保证 Server security assurance
可能有问题 There may be problems 可能有问题 There may be problems
- Recent 30 - Recent 30
- 一些歌曲的解锁 Some songs' unlocking - 一些歌曲的解锁 Some songs' unlocking
- 同设备多共存登录 Multiple app logins on the same device - 同设备多共存登录 Multiple app logins on the same device
## 说明 Statement ## 说明 Statement
只是很有趣,用处探索中。 只是很有趣,用处探索中。
It is just so interesting. What it can do is under exploration. It is just so interesting. What it can do is under exploration.
## 下载 Download ## 下载 Download
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases) [这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
[Arcaea-CN official](https://arcaea.lowiro.com/zh) [Arcaea-CN official](https://arcaea.lowiro.com/zh)
[Arcaea-lowi.ro](https://lowi.ro) [Arcaea-Konmai Academy](https://616.sb)
[Arcaea-RhyDown](https://rhydown.com)
## 更新日志 Update log ## 更新日志 Update log
只保留最新版本 Only keep the latest version. 只保留最新版本 Only keep the latest version.
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。 > 提醒:更新时请注意保留原先的数据库,以防数据丢失。
> >
> Tips: When updating, please keep the original database in case of data loss. > Tips: When updating, please keep the original database in case of data loss.
### Version 2.11.0
### Version 2.10.1 - 适用于Arcaea 4.3.0版本 For Arcaea 4.3.0
- 适用于Arcaea 4.1.4版本 For Arcaea 4.1.4 - 新搭档 **霞玛(大~宇~宙)**、**米露可(大~宇~宙)**、**紫黑**、**百合咲美香** 已解锁 Unlock the character **Shama(UNiVERSE)**, **Milk(UNiVERSE)**, **Shikoku**, **Mika Yurisaki**.
- 搭档 **天音**解锁 Unlock the character **Amane**. - 搭档 **依莉丝**觉醒 Uncap the character **Ilith**.
-**天音**技能提供支持 Add support for the skill of **Amane**. -觉醒 **依莉丝** 以及 **百合咲美香**技能提供支持 Add support for the skills of uncapped **Ilith** and **Mika Yurisaki**.
- 现在配置文件可以是含有部分选项的文件或模块 At present the setting file can be a module or a file with some of options. - 为 Beyond 图倍增提供支持 Add support for beyond gauge boost.
- 添加`waitress``gevent`的部署方案支持,并支持日志记录 Add deployment mode `waitress` and `gevent`, and add support for the info log recording of them. - 为 Beyond 连锁图提供支持 Add support for beyond chain maps.
- `songlist`添加解析器以指定可下载的文件 Add a parser for `songlist` to specify downloadable files. - 修复联机时无人房间仍可进入的问题 Fix a logic bug that the room without anyone can be entered in multiplayer.
- 重构数据库初始化和数据迁移部分 Code refactoring for database initialization and migration. - 对一些数值的算法进行了更改 Some changes in some values' algorithms.
- 限制用户下载频率将使用第三方限制器,替代数据库 Add a custom limiter and use it for limiting users' download rate instead of using database. - 小重构 Link Play 子程序 Refactor simply for Link Play subprogram.
> 现在需要`limits`模块 - 新增增删改兑换码、购买项目、登陆奖励、物品的API接口 Add some API endpoints, including creating, changing, deleting about redeem, purchase, login present and item.
> Now `limits` module is required.
- 为登录和API登录添加限制器 Add limiter for login and API login.
- `sqlite3`数据库调整为WAL模式并增大缓存 Change journal mode to WAL and enlarge cache size for `sqlite3` database.
- 将下载token放入内存中而不是文件数据库中 Put download token in memory database instead of filesystem database.
- 加速`best_score`表多次查询,表现为歌曲排行榜查询性能提升 Accelerate multiple querying in `best_score` table, which results in performance improvement of song ranklist query.
- 优化歌曲下载部分 Make some optimization for downloading songs.
- **修复更新recent 10时可能出现的死循环问题 Fix a bug that there is a endless loop in calculating recent 10 updating.** (due to 6fcca179182775615115cdb255b3a8223831a8a0)
- 修复课题模式成绩没有rating的问题 Fix a bug that scores in course mode cannot calculate rating.
- 修正搭档数值 Fix a character's value.
- 邮箱长度最大限制提升到64 Change the email max length to 64.
- 新增API接口来获取用户身份与权限 Add a method of API for getting users' roles and powers.
- 新增API接口来修改用户信息 Add a method of API to change the user's info.
- 为API的`GET`请求添加`query`参数支持 Add support for the `query` param in API's `GET` requests.
- 修复API的`best30`接口曲目无数据导致出错的问题 Fix a bug that `best30` of API cannot have scores whose songs are not in database.
- 修复API的`recent30`接口用户成绩数量不足导致出错的问题 Fix a bug that users with no recent scores cannot get `recent30` via API.
## 运行环境与依赖 Running environment and requirements ## 运行环境与依赖 Running environment and requirements
- Windows/Linux/Mac OS/Android... - Windows/Linux/Mac OS/Android...
- Python >= 3.6 - Python >= 3.6
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0 - Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
@@ -106,34 +100,41 @@ It is just so interesting. What it can do is under exploration.
--> -->
## 使用说明 Instruction for use ## 使用说明 Instruction for use
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use)
## 注意 Attentions ## 注意 Attentions
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F) [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
## Q&A ## Q&A
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A) [中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
## 鸣谢 Thanks ## 鸣谢 Thanks
~~歌曲数据库来自 Using song database from
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~ 歌曲数据库来自 Using song database from
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
> 从v2.9开始不再提供歌曲数据 > 从v2.9开始不再提供歌曲数据
> Since v2.9, song data will not be provided. > Since v2.9, song data will not be provided.
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369) 网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
## 联系方式 Contact ## 联系方式 Contact
如有必要,可以联系本人 Contact me if necessary 如有必要,可以联系本人 Contact me if necessary
邮箱 Emailth84292@foxmail.com 邮箱 Emailth84292@foxmail.com
## 支持一下 Support me ## 支持一下 Support me
生活不易。 Life is not easy. 生活不易。 Life is not easy.
[支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg) [支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg)
[微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png) [微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png)
## 使用许可 Use license ## 使用许可 Use license
[MIT](LICENSE) © Lost [MIT](LICENSE) © Lost

View File

@@ -1,10 +1,14 @@
from flask import Blueprint from flask import Blueprint
from . import users from . import (users, songs, token, system, items,
from . import songs purchases, presents, redeems)
from . import token
bp = Blueprint('api', __name__, url_prefix='/api/v1') bp = Blueprint('api', __name__, url_prefix='/api/v1')
bp.register_blueprint(users.bp) bp.register_blueprint(users.bp)
bp.register_blueprint(songs.bp) bp.register_blueprint(songs.bp)
bp.register_blueprint(token.bp) bp.register_blueprint(token.bp)
bp.register_blueprint(system.bp)
bp.register_blueprint(items.bp)
bp.register_blueprint(purchases.bp)
bp.register_blueprint(presents.bp)
bp.register_blueprint(redeems.bp)

View File

@@ -1,15 +1,17 @@
from functools import wraps
from traceback import format_exc
from base64 import b64decode from base64 import b64decode
from functools import wraps
from json import loads from json import loads
from traceback import format_exc
from flask import current_app
from core.api_user import APIUser from core.api_user import APIUser
from core.config_manager import Config from core.config_manager import Config
from core.error import ArcError, NoAccess, PostError from core.error import ArcError, InputError, NoAccess, PostError
from core.sql import Connect from core.sql import Connect
from flask import current_app
from .api_code import error_return from .api_code import error_return
from .constant import Constant
def role_required(request, powers=[]): def role_required(request, powers=[]):
@@ -48,13 +50,14 @@ def role_required(request, powers=[]):
return decorator 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, is_batch: bool = False):
''' '''
提取post参数返回dict写成了修饰器\ 提取post参数返回dict写成了修饰器\
parameters: \ parameters: \
`request`: `Request` - 当前请求\ `request`: `Request` - 当前请求\
`required_keys`: `list` - 必须的参数\ `required_keys`: `list` - 必须的参数\
`optional_keys`: `list` - 可选的参数 `optional_keys`: `list` - 可选的参数\
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
''' '''
def decorator(view): def decorator(view):
@@ -63,23 +66,38 @@ def request_json_handle(request, required_keys=[], optional_keys=[]):
data = {} data = {}
if request.data: if request.data:
json_data = request.json json_data = request.get_json()
else: else:
if request.method == 'GET' and 'query' in request.args: if request.method == 'GET' and 'query' in request.args:
# 处理axios没法GET传data的问题 # 处理axios没法GET传data的问题
json_data = loads( try:
b64decode(request.args['query']).decode()) json_data = loads(
b64decode(request.args['query']).decode())
except:
raise PostError(api_error_code=-105)
else: else:
json_data = {} json_data = {}
for key in required_keys: for key in required_keys:
if key not in json_data: if key not in json_data:
return error_return(PostError(f'Missing parameter: {key}', api_error_code=-100)) return error_return(InputError(f'Missing parameter: {key}', api_error_code=-100))
data[key] = json_data[key] data[key] = json_data[key]
for key in optional_keys: if is_batch:
if key in json_data: for key in Constant.PATCH_KEYS:
data[key] = json_data[key] if key in json_data:
data[key] = json_data[key]
if not isinstance(data[key], list):
return error_return(InputError(f'Parameter {key} must be a list', api_error_code=-100))
if not data:
return error_return(InputError('No change', api_error_code=-100))
else:
for key in optional_keys:
if key in json_data:
data[key] = json_data[key]
if must_change and not data:
return error_return(InputError('No change', api_error_code=-100))
return view(data, *args, **kwargs) return view(data, *args, **kwargs)

View File

@@ -1,6 +1,7 @@
from core.error import ArcError
from flask import jsonify from flask import jsonify
from core.error import ArcError
default_error = ArcError('Unknown Error') default_error = ArcError('Unknown Error')
@@ -9,6 +10,7 @@ CODE_MSG = {
-1: 'See status code', # 基础错误 -1: 'See status code', # 基础错误
-2: 'No data', -2: 'No data',
-3: 'No data or user', # 不确定是无数据还是无用户 -3: 'No data or user', # 不确定是无数据还是无用户
-4: 'Data exist',
-100: 'Invalid post data', # 1xx数据错误 -100: 'Invalid post data', # 1xx数据错误
-101: 'Invalid data type', -101: 'Invalid data type',
-102: 'Invalid query parameter', -102: 'Invalid query parameter',
@@ -16,6 +18,11 @@ CODE_MSG = {
-104: 'Invalid sort order parameter', -104: 'Invalid sort order parameter',
-105: 'Invalid URL parameter', -105: 'Invalid URL parameter',
-110: 'Invalid user_id', -110: 'Invalid user_id',
-120: 'Invalid item type',
-121: 'No such item',
-122: 'Item already exists',
-123: 'The collection already has this item',
-124: 'The collection does not have this item',
-200: 'No permission', # 2xx用户相关错误 -200: 'No permission', # 2xx用户相关错误
-201: 'Wrong username or password', -201: 'Wrong username or password',
-202: 'User is banned', -202: 'User is banned',

View File

@@ -1,2 +1,4 @@
class Constant: class Constant:
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort'] QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
PATCH_KEYS = ['create', 'update', 'remove']

115
latest version/api/items.py Normal file
View File

@@ -0,0 +1,115 @@
from flask import Blueprint, request
from core.error import DataExist, InputError, NoData
from core.item import Item, ItemFactory
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('items', __name__, url_prefix='/items')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def items_get(data, user):
'''查询全物品信息'''
with Connect() as c:
query = Query(['item_id', 'type'], ['item_id'],
['item_id']).from_dict(data)
x = Sql(c).select('item', query=query)
r: 'list[Item]' = []
for i in x:
r.append(ItemFactory.from_dict({
'item_id': i[0],
'type': i[1],
'is_available': i[2] == 1
}))
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict(has_is_available=True, has_amount=False) for x in r])
ALLOW_ITEM_TYPE = ['pack', 'single', 'world_song', 'character']
@bp.route('', methods=['POST'])
@role_required(request, ['change'])
@request_json_handle(request, required_keys=['item_id', 'type'], optional_keys=['is_available'])
@api_try
def items_post(data, user):
'''新增物品'''
if data['type'] not in ALLOW_ITEM_TYPE:
raise InputError(
f'Invalid item type: `{data["type"]}`', api_error_code=-120)
with Connect() as c:
item = ItemFactory.from_dict(data, c=c)
if item.select_exists():
raise DataExist(
f'Item `{item.item_type}`: `{item.item_id}` already exists', api_error_code=-122)
item.insert()
return success_return(item.to_dict(has_is_available=True, has_amount=False))
@bp.route('/<string:item_type>/<string:item_id>', methods=['DELETE'])
@role_required(request, ['change'])
@api_try
def items_item_delete(user, item_type, item_id):
'''删除物品'''
if item_type not in ALLOW_ITEM_TYPE:
raise InputError(
f'Invalid item type: `{item_type}`', api_error_code=-120)
with Connect() as c:
item = ItemFactory.from_dict({
'item_id': item_id,
'type': item_type
}, c=c)
if not item.select_exists():
raise NoData(
f'No such item `{item_type}`: `{item_id}`', api_error_code=-121)
item.delete()
return success_return()
@bp.route('/<string:item_type>/<string:item_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['is_available'], must_change=True)
@api_try
def items_item_put(data, user, item_type, item_id):
'''修改物品'''
if item_type not in ALLOW_ITEM_TYPE:
raise InputError(
f'Invalid item type: `{item_type}`', api_error_code=-120)
if not isinstance(data['is_available'], bool):
raise InputError('`is_available` must be a boolean',
api_error_code=-101)
with Connect() as c:
item = ItemFactory.from_dict({
'item_id': item_id,
'type': item_type,
'is_available': data['is_available']
}, c=c)
if not item.select_exists():
raise NoData(
f'No such item `{item_type}`: `{item_id}`', api_error_code=-121)
item.update()
return success_return(item.to_dict(has_is_available=True, has_amount=False))
@bp.route('/<string:item_type>/<string:item_id>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def items_item_get(user, item_type, item_id):
'''查询单个物品信息'''
with Connect() as c:
item = ItemFactory.from_dict({
'item_id': item_id,
'type': item_type
}, c=c)
item.select()
return success_return(item.to_dict(has_is_available=True, has_amount=False))

View File

@@ -0,0 +1,122 @@
from flask import Blueprint, request
from core.error import DataExist, InputError, NoData
from core.item import ItemFactory
from core.present import Present
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('presents', __name__, url_prefix='/presents')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def presents_get(data, user):
'''查询全present信息'''
with Connect() as c:
query = Query(['present_id'], ['present_id', 'description'], [
'present_id', 'expire_ts']).from_dict(data)
x = Sql(c).select('present', query=query)
r = [Present().from_list(i) for i in x]
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict(has_items=False) for x in r])
@bp.route('', methods=['POST'])
@role_required(request, ['insert'])
@request_json_handle(request, required_keys=['present_id', 'description', 'expire_ts'], optional_keys=['items'])
@api_try
def presents_post(data, user):
'''添加present注意可以有items不存在的item会自动创建'''
with Connect() as c:
p = Present(c).from_dict(data)
if p.select_exists():
raise DataExist(
f'Present `{p.present_id}` already exists')
p.insert_all()
return success_return(p.to_dict(has_items='items' in data))
@bp.route('/<string:present_id>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def presents_present_get(user, present_id: str):
'''查询单个present信息'''
with Connect() as c:
p = Present(c).select(present_id)
p.select_items()
return success_return(p.to_dict())
@bp.route('/<string:present_id>', methods=['DELETE'])
@role_required(request, ['delete'])
@api_try
def presents_present_delete(user, present_id: str):
'''删除present会连带删除present_item'''
with Connect() as c:
Present(c).select(present_id).delete_all()
return success_return()
@bp.route('/<string:present_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['description', 'expire_ts'], must_change=True)
@api_try
def presents_present_put(data, user, present_id: str):
'''更新present信息注意不能有items'''
with Connect() as c:
p = Present(c).select(present_id)
if 'description' in data:
p.description = str(data['description'])
if 'expire_ts' in data:
p.expire_ts = int(data['expire_ts'])
p.update()
return success_return(p.to_dict(has_items=False))
@bp.route('/<string:present_id>/items', methods=['GET'])
@role_required(request, ['select'])
@api_try
def presents_present_items_get(user, present_id: str):
'''查询present的items'''
with Connect() as c:
p = Present(c)
p.present_id = present_id
p.select_items()
return success_return([x.to_dict(has_is_available=True) for x in p.items])
@bp.route('/<string:present_id>/items', methods=['PATCH'])
@role_required(request, ['change'])
@request_json_handle(request, is_batch=True)
@api_try
def presents_present_items_patch(data, user, present_id: str):
'''增删改单个present的items'''
with Connect() as c:
p = Present(c)
p.present_id = present_id
p.select_items()
p.remove_items([ItemFactory.from_dict(x, c=c)
for x in data.get('remove', [])])
p.add_items([ItemFactory.from_dict(x, c=c)
for x in data.get('create', [])])
updates = data.get('update', [])
for x in updates:
if 'amount' not in x:
raise InputError('`amount` is required in `update`')
if not isinstance(x['amount'], int) or x['amount'] <= 0:
raise InputError(
'`amount` must be a positive integer', api_error_code=-101)
p.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
return success_return([x.to_dict(has_is_available=True) for x in p.items])

View File

@@ -0,0 +1,163 @@
from flask import Blueprint, request
from core.error import DataExist, InputError, NoData
from core.item import ItemFactory
from core.purchase import Purchase
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('purchases', __name__, url_prefix='/purchases')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def purchases_get(data, user):
'''查询全购买信息'''
with Connect() as c:
query = Query(['purchase_name', 'discount_reason'], ['purchase_name'], [
'purchase_name', 'price', 'orig_price', 'discount_from', 'discount_to']).from_dict(data)
x = Sql(c).select('purchase', query=query)
r = [Purchase().from_list(i) for i in x]
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict(has_items=False, show_real_price=False) for x in r])
@bp.route('', methods=['POST'])
@role_required(request, ['change'])
@request_json_handle(request, required_keys=['purchase_name', 'orig_price'], optional_keys=['price', 'discount_from', 'discount_to', 'discount_reason', 'items'])
@api_try
def purchases_post(data, user):
'''新增购买注意可以有items不存在的item会自动创建'''
with Connect() as c:
purchase = Purchase(c).from_dict(data)
if purchase.select_exists():
raise DataExist(
f'Purchase `{purchase.purchase_name}` already exists')
purchase.insert_all()
return success_return(purchase.to_dict(has_items='items' in data, show_real_price=False))
@bp.route('/<string:purchase_name>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def purchases_purchase_get(user, purchase_name: str):
'''查询单个购买信息'''
with Connect() as c:
return success_return(Purchase(c).select(purchase_name).to_dict(show_real_price=False))
@bp.route('/<string:purchase_name>', methods=['DELETE'])
@role_required(request, ['change'])
@api_try
def purchases_purchase_delete(user, purchase_name: str):
'''删除单个购买信息会连带删除purchase_item'''
with Connect() as c:
Purchase(c).select(purchase_name).delete_all()
return success_return()
@bp.route('/<string:purchase_name>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['price', 'orig_price', 'discount_from', 'discount_to', 'discount_reason'], must_change=True)
@api_try
def purchases_purchase_put(data, user, purchase_name: str):
'''修改单个购买信息注意不能有items'''
with Connect() as c:
purchase = Purchase(c).select(purchase_name)
t = ['price', 'orig_price', 'discount_from', 'discount_to']
for i in t:
if i in data:
setattr(purchase, i, int(data[i]))
if 'discount_reason' in data:
purchase.discount_reason = str(data['discount_reason'])
purchase.update()
return success_return(purchase.to_dict(has_items=False, show_real_price=False))
@bp.route('/<string:purchase_name>/items', methods=['GET'])
@role_required(request, ['select'])
@api_try
def purchases_purchase_items_get(user, purchase_name: str):
'''查询单个购买的所有items'''
with Connect() as c:
p = Purchase(c)
p.purchase_name = purchase_name
p.select_items()
return success_return([x.to_dict(has_is_available=True) for x in p.items])
# @bp.route('/<string:purchase_name>/items', methods=['POST'])
# @role_required(request, ['change'])
# @request_json_handle(request, required_keys=['item_id', 'type'], optional_keys=['amount'])
# @api_try
# def purchases_purchase_items_post(data, user, purchase_name: str):
# '''新增单个购买的批量items'''
# with Connect() as c:
# p = Purchase(c)
# p.purchase_name = purchase_name
# p.select_items()
# p.add_items([ItemFactory().from_dict(data)])
# return success_return([x.to_dict(has_is_available=True) for x in p.items])
@bp.route('/<string:purchase_name>/items', methods=['PATCH'])
@role_required(request, ['change'])
@request_json_handle(request, is_batch=True)
@api_try
def purchases_purchase_items_patch(data, user, purchase_name: str):
'''增删改单个购买的批量items'''
with Connect() as c:
p = Purchase(c)
p.purchase_name = purchase_name
p.select_items()
p.remove_items([ItemFactory.from_dict(x, c=c)
for x in data.get('remove', [])])
p.add_items([ItemFactory.from_dict(x, c=c)
for x in data.get('create', [])])
updates = data.get('update', [])
for x in updates:
if 'amount' not in x:
raise InputError('`amount` is required in `update`')
if not isinstance(x['amount'], int) or x['amount'] <= 0:
raise InputError(
'`amount` must be a positive integer', api_error_code=-101)
p.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
return success_return([x.to_dict(has_is_available=True) for x in p.items])
# @bp.route('/<string:purchase_name>/items/<string:item_type>/<string:item_id>', methods=['DELETE'])
# @role_required(request, ['change'])
# @api_try
# def purchases_purchase_items_item_delete(user, purchase_name: str, item_type: str, item_id: str):
# '''删除单个购买的单个item'''
# with Connect() as c:
# p = Purchase(c)
# p.purchase_name = purchase_name
# p.select_items()
# p.delete_items([ItemFactory().from_dict(
# {'item_type': item_type, 'item_id': item_id})])
# return success_return()
# @bp.route('/<string:purchase_name>/items/<string:item_type>/<string:item_id>', methods=['PUT'])
# @role_required(request, ['change'])
# @request_json_handle(request, optional_keys=['amount', 'is_available'], must_change=True)
# @api_try
# def purchases_purchase_items_item_put(data, user, purchase_name: str, item_type: str, item_id: str):
# '''修改单个购买的单个item'''
# with Connect() as c:
# p = Purchase(c)
# p.purchase_name = purchase_name
# pass
# return success_return()

View File

@@ -0,0 +1,119 @@
from flask import Blueprint, request
from core.error import DataExist, InputError, NoData
from core.item import ItemFactory
from core.redeem import Redeem
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('redeems', __name__, url_prefix='/redeems')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def redeems_get(data, user):
'''查询全redeem信息'''
with Connect() as c:
query = Query(['code', 'type'], ['code'], ['code']).from_dict(data)
x = Sql(c).select('redeem', query=query)
r = [Redeem().from_list(i) for i in x]
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict(has_items=False) for x in r])
@bp.route('', methods=['POST'])
@role_required(request, ['insert'])
@request_json_handle(request, required_keys=['code', 'type'], optional_keys=['items'])
@api_try
def redeems_post(data, user):
'''添加redeem注意可以有items不存在的item会自动创建'''
with Connect() as c:
r = Redeem(c).from_dict(data)
if r.select_exists():
raise DataExist(
f'redeem `{r.code}` already exists')
r.insert_all()
return success_return(r.to_dict(has_items='items' in data))
@bp.route('/<string:code>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def redeems_redeem_get(user, code: str):
'''查询单个redeem信息'''
with Connect() as c:
r = Redeem(c).select(code)
r.select_items()
return success_return(r.to_dict())
@bp.route('/<string:code>', methods=['DELETE'])
@role_required(request, ['delete'])
@api_try
def redeems_redeem_delete(user, code: str):
'''删除redeem会连带删除redeem_item'''
with Connect() as c:
Redeem(c).select(code).delete_all()
return success_return()
@bp.route('/<string:code>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['type'], must_change=True)
@api_try
def redeems_redeem_put(data, user, code: str):
'''更新redeem信息注意不能有items'''
with Connect() as c:
r = Redeem(c).select(code)
if 'type' in data:
r.redeem_type = int(data['type'])
r.update()
return success_return(r.to_dict(has_items=False))
@bp.route('/<string:code>/items', methods=['GET'])
@role_required(request, ['select'])
@api_try
def redeems_redeem_items_get(user, code: str):
'''查询redeem的items'''
with Connect() as c:
r = Redeem(c)
r.code = code
r.select_items()
return success_return([x.to_dict(has_is_available=True) for x in r.items])
@bp.route('/<string:code>/items', methods=['PATCH'])
@role_required(request, ['change'])
@request_json_handle(request, is_batch=True)
@api_try
def redeems_redeem_items_patch(data, user, code: str):
'''增删改单个redeem的items'''
with Connect() as c:
r = Redeem(c)
r.code = code
r.select_items()
r.remove_items([ItemFactory.from_dict(x, c=c)
for x in data.get('remove', [])])
r.add_items([ItemFactory.from_dict(x, c=c)
for x in data.get('create', [])])
updates = data.get('update', [])
for x in updates:
if 'amount' not in x:
raise InputError('`amount` is required in `update`')
if not isinstance(x['amount'], int) or x['amount'] <= 0:
raise InputError(
'`amount` must be a positive integer', api_error_code=-101)
r.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
return success_return([x.to_dict(has_is_available=True) for x in r.items])

View File

@@ -1,7 +1,9 @@
from core.error import NoData from flask import Blueprint, request
from core.error import DataExist, InputError, NoData
from core.rank import RankList
from core.song import Song from core.song import Song
from core.sql import Connect, Query, Sql 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_auth import api_try, request_json_handle, role_required
from .api_code import success_return from .api_code import success_return
@@ -20,6 +22,39 @@ def songs_song_get(user, song_id):
return success_return(s.to_dict()) return success_return(s.to_dict())
@bp.route('/<string:song_id>', 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('/<string:song_id>', 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']) @bp.route('', methods=['GET'])
@role_required(request, ['select', 'select_song_info']) @role_required(request, ['select', 'select_song_info'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS) @request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@@ -40,3 +75,41 @@ def songs_get(data, user):
raise NoData(api_error_code=-2) raise NoData(api_error_code=-2)
return success_return([x.to_dict() for x in r]) 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('/<string:song_id>/<int:difficulty>/rank', methods=['GET'])
@role_required(request, ['select', 'select_song_rank', 'select_song_rank_top'])
@request_json_handle(request, optional_keys=['limit'])
@api_try
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
'''查询歌曲某个难度的成绩排行榜和游戏内接口相似只允许limit'''
if difficulty not in [0, 1, 2, 3]:
raise InputError('Difficulty must be 0, 1, 2 or 3')
limit = data.get('limit', 20)
if not isinstance(limit, int):
raise InputError('Limit must be int')
if user.role.only_has_powers(['select_song_rank_top'], ['select', 'select_song_rank']):
# 限制低权限只能查询前20名
if limit > 20 or limit < 0:
limit = 20
with Connect() as c:
rank_list = RankList(c)
rank_list.song.set_chart(song_id, difficulty)
rank_list.limit = limit
# 不检查歌曲是否存在,不存在返回的是空列表
rank_list.select_top()
return success_return(rank_list.to_dict_list())

View File

@@ -0,0 +1,32 @@
from flask import Blueprint, request
from core.error import ArcError
from core.operation import BaseOperation
from .api_auth import api_try, role_required
from .api_code import success_return
bp = Blueprint('system', __name__, url_prefix='/system')
operation_dict = {i._name: i for i in BaseOperation.__subclasses__()}
@bp.route('/operations', methods=['GET'])
@role_required(request, ['system'])
@api_try
def operations_get(user):
return success_return(list(operation_dict.keys()))
@bp.route('/operations/<string:operation_name>', methods=['POST'])
@role_required(request, ['system'])
@api_try
def operations_operation_post(user, operation_name: str):
if operation_name not in operation_dict:
raise ArcError(
f'No such operation: `{operation_name}`', api_error_code=-1, status=404)
x = operation_dict[operation_name]()
x.set_params(**request.get_json())
x.run()
return success_return()

View File

@@ -1,9 +1,10 @@
from base64 import b64decode from base64 import b64decode
from flask import Blueprint, request
from core.api_user import APIUser from core.api_user import APIUser
from core.error import PostError from core.error import PostError
from core.sql import Connect from core.sql import Connect
from flask import Blueprint, request
from .api_auth import api_try, request_json_handle, role_required from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return from .api_code import success_return

View File

@@ -1,9 +1,10 @@
from flask import Blueprint, request
from core.api_user import APIUser
from core.error import InputError, NoAccess, NoData from core.error import InputError, NoAccess, NoData
from core.score import Potential, UserScoreList from core.score import Potential, UserScoreList
from core.sql import Connect, Query, Sql from core.sql import Connect, Query, Sql
from core.user import UserChanger, UserInfo, UserRegister from core.user import UserChanger, UserInfo, UserRegister
from core.api_user import APIUser
from flask import Blueprint, request
from .api_auth import api_try, request_json_handle, role_required from .api_auth import api_try, request_json_handle, role_required
from .api_code import error_return, success_return from .api_code import error_return, success_return
@@ -78,7 +79,7 @@ def users_user_get(user, user_id):
@bp.route('/<int:user_id>', methods=['PUT']) @bp.route('/<int:user_id>', methods=['PUT'])
@role_required(request, ['change']) @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 @api_try
def users_user_put(data, user, user_id): def users_user_put(data, user, user_id):
'''修改一个用户''' '''修改一个用户'''
@@ -103,8 +104,7 @@ def users_user_put(data, user, user_id):
raise InputError('Ticket must be int') raise InputError('Ticket must be int')
u.ticket = data['ticket'] u.ticket = data['ticket']
r['ticket'] = u.ticket r['ticket'] = u.ticket
if r: u.update_columns(d=r)
u.update_columns(d=r)
return success_return(r) return success_return(r)

View File

@@ -34,6 +34,19 @@ class Role:
'''判断role是否有power''' '''判断role是否有power'''
return any(power_id == i.power_id for i in self.powers) return any(power_id == i.power_id for i in self.powers)
def only_has_powers(self, power_ids: list, anti_power_ids: list = None) -> bool:
'''判断role是否全有power_ids里的power且没有anti_power_ids里的任何一个power'''
flags = [False] * len(power_ids)
if anti_power_ids is None:
anti_power_ids = []
for i in self.powers:
if i.power_id in anti_power_ids:
return False
for j, k in enumerate(power_ids):
if i.power_id == k:
flags[j] = True
return all(flags)
def select_from_id(self, role_id: int = None) -> 'Role': def select_from_id(self, role_id: int = None) -> 'Role':
'''用role_id查询role''' '''用role_id查询role'''
if role_id is not None: if role_id is not None:
@@ -121,11 +134,12 @@ class APIUser(UserOnline):
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The user `%s` does not exist.' % 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] == '': if x[1] == '':
raise UserBan('The user `%s` is banned.' % self.name) raise UserBan('The user `%s` is banned.' % self.name)
if self.hash_pwd != x[1]: 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] self.user_id = x[0]
now = int(time() * 1000) now = int(time() * 1000)

View File

@@ -70,16 +70,24 @@ class CharacterValue:
self.set_parameter(start, mid, end) self.set_parameter(start, mid, end)
@staticmethod @staticmethod
def _calc_char_value_20(level, stata, statb, lva=1, lvb=20): def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
# 计算1~20级搭档数值的核心函数返回浮点数来自https://redive.estertion.win/arcaea/calc/ # by Lost-MSth
n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946, # 4/6859 = 0.00058317539
0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1] if level <= 10:
e = n[lva] - n[lvb] return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1
a = stata - statb else:
r = a / e return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
d = stata - n[lva] * r
return d + r * n[level] # @staticmethod
# def _calc_char_value_20(level: int, stata, statb, lva=1, lvb=20) -> float:
# # 计算1~20级搭档数值的核心函数返回浮点数来自https://redive.estertion.win/arcaea/calc/
# n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
# 0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
# e = n[lva] - n[lvb]
# a = stata - statb
# r = a / e
# d = stata - n[lva] * r
# return d + r * n[level]
@staticmethod @staticmethod
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30): def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
@@ -93,7 +101,7 @@ class CharacterValue:
def get_value(self, level: Level): def get_value(self, level: Level):
if level.min_level <= level.level <= level.mid_level: if level.min_level <= level.level <= level.mid_level:
return self._calc_char_value_20(level.level, self.start, self.mid) return self._calc_char_value_20_math(level.level, self.start, self.mid)
elif level.mid_level < level.level <= level.max_level: elif level.mid_level < level.level <= level.max_level:
return self._calc_char_value_30(level.level, self.mid, self.end) return self._calc_char_value_30(level.level, self.mid, self.end)
else: else:
@@ -103,7 +111,9 @@ class CharacterValue:
class Character: class Character:
database_table_name = None database_table_name = None
def __init__(self) -> None: def __init__(self, c=None) -> None:
self.c = c
self.character_id = None self.character_id = None
self.name = None self.name = None
self.char_type = None self.char_type = None
@@ -134,10 +144,16 @@ class Character:
# 应该是只有对立这样 # 应该是只有对立这样
return self.character_id == 1 return self.character_id == 1
def to_dict(self) -> dict:
pass
def from_list(self, l: list) -> 'Character':
pass
class UserCharacter(Character): class UserCharacter(Character):
''' '''
用户角色类\ 用户角色类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char' database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
@@ -332,9 +348,10 @@ class UserCharacter(Character):
def upgrade_by_core(self, user=None, core=None): def upgrade_by_core(self, user=None, core=None):
''' '''
以太之滴升级注意这里core.amount应该是负数\ 以太之滴升级注意这里core.amount应该是负数
parameter: `user` - `User`类或子类的实例\
`core` - `ItemCore`类或子类的实例 parameter: `user` - `User`类或子类的实例
`core` - `ItemCore`类或子类的实例
''' '''
if user: if user:
self.user = user self.user = user
@@ -353,7 +370,7 @@ class UserCharacter(Character):
class UserCharacterList: class UserCharacterList:
''' '''
用户拥有角色列表类\ 用户拥有角色列表类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char' database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'

View File

@@ -10,6 +10,8 @@ class Config:
USE_PROXY_FIX = False USE_PROXY_FIX = False
USE_CORS = False USE_CORS = False
SONG_FILE_HASH_PRE_CALCULATE = True
GAME_API_PREFIX = '/join/21' GAME_API_PREFIX = '/join/21'
ALLOW_APPVERSION = [] # list[str] ALLOW_APPVERSION = [] # list[str]
@@ -50,6 +52,8 @@ class Config:
ALLOW_LOGIN_SAME_DEVICE = False ALLOW_LOGIN_SAME_DEVICE = False
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
ALLOW_SCORE_WITH_NO_SONG = True
ALLOW_INFO_LOG = False ALLOW_INFO_LOG = False
ALLOW_WARNING_LOG = False ALLOW_WARNING_LOG = False
@@ -77,6 +81,7 @@ class Config:
SQLITE_DATABASE_PATH = './database/arcaea_database.db' SQLITE_DATABASE_PATH = './database/arcaea_database.db'
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/' SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
DATABASE_INIT_PATH = './database/init/' DATABASE_INIT_PATH = './database/init/'
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
GAME_LOGIN_RATE_LIMIT = '30/5 minutes' GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
API_LOGIN_RATE_LIMIT = '10/5 minutes' API_LOGIN_RATE_LIMIT = '10/5 minutes'

View File

@@ -1,6 +1,6 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.10.1' ARCAEA_SERVER_VERSION = 'v2.11.0'
class Constant: class Constant:
@@ -22,6 +22,8 @@ class Constant:
LUNA_UNCAP_BONUS_PROGRESS = 7 LUNA_UNCAP_BONUS_PROGRESS = 7
AYU_UNCAP_BONUS_PROGRESS = 5 AYU_UNCAP_BONUS_PROGRESS = 5
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000 SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
@@ -35,6 +37,7 @@ class Constant:
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
@@ -98,4 +101,4 @@ class Constant:
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save', DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role'] 'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -5,6 +5,7 @@ from time import time
from flask import url_for from flask import url_for
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .error import NoAccess from .error import NoAccess
from .limiter import ArcLimiter from .limiter import ArcLimiter
@@ -20,15 +21,6 @@ def get_song_file_md5(song_id: str, file_name: str) -> str:
return get_file_md5(path) return get_file_md5(path)
def initialize_songfile():
'''初始化歌曲数据的md5信息'''
get_song_file_md5.cache_clear()
x = DownloadList()
x.url_flag = False
x.add_songs()
del x
class SonglistParser: class SonglistParser:
'''songlist文件解析器''' '''songlist文件解析器'''
@@ -51,7 +43,7 @@ class SonglistParser:
self.parse() self.parse()
@staticmethod @staticmethod
def is_available_file(song_id: str, file_name: str) -> list: def is_available_file(song_id: str, file_name: str) -> bool:
'''判断文件是否允许被下载''' '''判断文件是否允许被下载'''
if song_id not in SonglistParser.songs: if song_id not in SonglistParser.songs:
# songlist没有则只限制文件名 # songlist没有则只限制文件名
@@ -176,7 +168,7 @@ class UserDownload:
class DownloadList(UserDownload): class DownloadList(UserDownload):
''' '''
下载列表类\ 下载列表类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -189,13 +181,23 @@ class DownloadList(UserDownload):
self.downloads: list = [] self.downloads: list = []
self.urls: dict = {} self.urls: dict = {}
@classmethod
def initialize_cache(cls) -> None:
'''初始化歌曲数据缓存包括md5、文件目录遍历、解析songlist'''
SonglistParser()
if Config.SONG_FILE_HASH_PRE_CALCULATE:
x = cls()
x.url_flag = False
x.add_songs()
del x
@staticmethod @staticmethod
def clear_all_cache(): def clear_all_cache() -> None:
'''清除所有歌曲文件有关缓存''' '''清除所有歌曲文件有关缓存'''
get_song_file_md5.cache_clear() get_song_file_md5.cache_clear()
DownloadList.get_one_song_file_names.cache_clear() DownloadList.get_one_song_file_names.cache_clear()
DownloadList.get_all_song_ids.cache_clear() DownloadList.get_all_song_ids.cache_clear()
SonglistParser() SonglistParser.songs = {}
def clear_download_token(self) -> None: def clear_download_token(self) -> None:
'''清除过期下载链接''' '''清除过期下载链接'''
@@ -212,9 +214,10 @@ class DownloadList(UserDownload):
def get_one_song_file_names(song_id: str) -> list: def get_one_song_file_names(song_id: str) -> list:
'''获取一个歌曲文件夹下的所有合法文件名有lru缓存''' '''获取一个歌曲文件夹下的所有合法文件名有lru缓存'''
r = [] r = []
for i in os.listdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)): for i in os.scandir(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): file_name = i.name
r.append(i) if i.is_file() and SonglistParser.is_available_file(song_id, file_name):
r.append(file_name)
return r return r
def add_one_song(self, song_id: str) -> None: def add_one_song(self, song_id: str) -> None:
@@ -265,7 +268,7 @@ class DownloadList(UserDownload):
@lru_cache() @lru_cache()
def get_all_song_ids() -> list: def get_all_song_ids() -> list:
'''获取全歌曲文件夹列表有lru缓存''' '''获取全歌曲文件夹列表有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: def add_songs(self, song_ids: list = None) -> None:
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲''' '''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''

View File

@@ -19,7 +19,9 @@ class InputError(ArcError):
class DataExist(ArcError): 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): class NoData(ArcError):

View File

@@ -4,11 +4,12 @@ from importlib import import_module
from json import load from json import load
from shutil import copy, copy2 from shutil import copy, copy2
from time import time from time import time
from traceback import format_exc
from core.config_manager import Config from core.config_manager import Config
from core.constant import ARCAEA_SERVER_VERSION from core.constant import ARCAEA_SERVER_VERSION
from core.course import Course from core.course import Course
from core.download import SonglistParser from core.download import DownloadList
from core.purchase import Purchase from core.purchase import Purchase
from core.sql import Connect, DatabaseMigrator, MemoryDatabase from core.sql import Connect, DatabaseMigrator, MemoryDatabase
from core.user import UserRegister from core.user import UserRegister
@@ -49,7 +50,7 @@ class DatabaseInit:
'''初始化搭档信息''' '''初始化搭档信息'''
for i in range(0, len(self.init_data.char)): for i in range(0, len(self.init_data.char)):
skill_requires_uncap = 1 if i == 2 else 0 skill_requires_uncap = 1 if i == 2 else 0
if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43, 11, 12, 19, 5]: if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43, 11, 12, 19, 5, 10]:
max_level = 30 max_level = 30
uncapable = 1 uncapable = 1
else: else:
@@ -90,22 +91,16 @@ class DatabaseInit:
self.c.execute('''insert into item values(?,?,?)''', self.c.execute('''insert into item values(?,?,?)''',
('anni5tix', 'anni5tix', 1)) ('anni5tix', 'anni5tix', 1))
with open(self.pack_path, 'r') as f: with open(self.pack_path, 'rb') as f:
self.insert_purchase_item(load(f)) self.insert_purchase_item(load(f))
with open(self.single_path, 'r') as f: with open(self.single_path, 'rb') as f:
self.insert_purchase_item(load(f)) self.insert_purchase_item(load(f))
self.c.execute(
'''select exists(select * from item where item_id='epilogue')''')
if self.c.fetchone() == (0,):
self.c.execute(
'''insert into item values('epilogue','pack',1)''')
def course_init(self) -> None: def course_init(self) -> None:
'''初始化课题信息''' '''初始化课题信息'''
courses = [] courses = []
with open(self.course_path, 'r', encoding='utf-8') as f: with open(self.course_path, 'rb') as f:
courses = load(f) courses = load(f)
for i in courses: for i in courses:
x = Course(self.c).from_dict(i) x = Course(self.c).from_dict(i)
@@ -157,35 +152,73 @@ class DatabaseInit:
self.admin_init() self.admin_init()
class FileChecker: class LogDatabaseInit:
def __init__(self, db_path: str = Config.SQLITE_LOG_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
self.db_path = db_path
self.init_folder_path = init_folder_path
self.c = None
def __init__(self, app=None): @property
self.app = app def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_init(self) -> None:
'''初始化数据库结构'''
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})
def init(self) -> None:
with Connect(self.db_path) as c:
self.c = c
self.table_init()
class FileChecker:
'''文件检查及初始化类'''
def __init__(self, logger=None):
self.logger = logger
def check_file(self, file_path: str) -> bool: def check_file(self, file_path: str) -> bool:
f = os.path.isfile(file_path) f = os.path.isfile(file_path)
if not f: if not f:
self.app.logger.warning('File `%s` is missing.' % file_path) self.logger.warning('File `%s` is missing.' % file_path)
return f return f
def check_folder(self, folder_path: str) -> bool: def check_folder(self, folder_path: str) -> bool:
f = os.path.isdir(folder_path) f = os.path.isdir(folder_path)
if not f: if not f:
self.app.logger.warning('Folder `%s` is missing.' % folder_path) self.logger.warning('Folder `%s` is missing.' % folder_path)
return f return f
def check_update_database(self) -> bool: def check_update_database(self) -> bool:
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
# 新建日志数据库
try:
self.logger.info(
f'Try to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
LogDatabaseInit().init()
self.logger.info(
f'Success to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH): if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库 # 新建数据库
try: try:
self.app.logger.info( self.logger.info(
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) 'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
DatabaseInit().init() DatabaseInit().init()
self.app.logger.info( self.logger.info(
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) 'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
except: except Exception as e:
self.app.logger.warning( self.logger.error(format_exc())
'Fail to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) self.logger.warning(
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
return False return False
else: else:
# 检查更新 # 检查更新
@@ -197,10 +230,10 @@ class FileChecker:
x = None x = None
# 数据库自动更新,不强求 # 数据库自动更新,不强求
if not x or x[0] != ARCAEA_SERVER_VERSION: if not x or x[0] != ARCAEA_SERVER_VERSION:
self.app.logger.warning( self.logger.warning(
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH) 'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
try: try:
self.app.logger.info( self.logger.info(
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) 'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH): if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
@@ -224,11 +257,12 @@ class FileChecker:
DatabaseInit().init() DatabaseInit().init()
self.update_database(temp_path) self.update_database(temp_path)
self.app.logger.info( self.logger.info(
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) 'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
except ValueError: except Exception as e:
self.app.logger.warning( self.logger.error(format_exc())
self.logger.warning(
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) 'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
return True return True
@@ -240,9 +274,22 @@ class FileChecker:
DatabaseMigrator(old_path, new_path).update_database() DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path) os.remove(old_path)
def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
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())
self.logger.warning('Initialization error!')
f = False
return f
def check_before_run(self) -> bool: def check_before_run(self) -> bool:
'''运行前检查,返回布尔值''' '''运行前检查,返回布尔值'''
# TODO: try
MemoryDatabase() # 初始化内存数据库 MemoryDatabase() # 初始化内存数据库
SonglistParser() # 解析songlist return self.check_song_file() & self.check_update_database()
return self.check_folder(Config.SONG_FILE_FOLDER_PATH) & self.check_update_database()

View File

@@ -5,10 +5,14 @@ from .error import InputError, ItemNotEnough, ItemUnavailable, NoData
class Item: class Item:
item_type = None item_type = None
def __init__(self) -> None: def __init__(self, c=None) -> None:
self.item_id = None self.item_id = None
self.__amount = None self.__amount = None
self.is_available = None self.is_available = None
self.c = c
def __eq__(self, other: 'Item') -> bool:
return self.item_id == other.item_id and self.item_type == other.item_type
@property @property
def amount(self): def amount(self):
@@ -18,12 +22,13 @@ class Item:
def amount(self, value: int): def amount(self, value: int):
self.__amount = int(value) self.__amount = int(value)
def to_dict(self, has_is_available: bool = False) -> dict: def to_dict(self, has_is_available: bool = False, has_amount: bool = True) -> dict:
r = { r = {
'id': self.item_id, 'id': self.item_id,
'amount': self.amount,
'type': self.item_type 'type': self.item_type
} }
if has_amount:
r['amount'] = self.amount
if has_is_available: if has_is_available:
r['is_available'] = self.is_available r['is_available'] = self.is_available
return r return r
@@ -32,6 +37,32 @@ class Item:
# parameter: user - User类或子类的实例 # parameter: user - User类或子类的实例
pass pass
def select_exists(self):
self.c.execute('''select exists(select * from item where item_id=? and type=?)''',
(self.item_id, self.item_type))
return bool(self.c.fetchone()[0])
def insert(self, ignore: bool = False):
sql = '''insert into item values(?,?,?)''' if not ignore else '''insert or ignore into item values(?,?,?)'''
self.c.execute(sql, (self.item_id, self.item_type, self.is_available))
def delete(self):
self.c.execute('''delete from item where item_id=? and type=?''',
(self.item_id, self.item_type))
def update(self):
self.c.execute('''update item set is_available=? where item_id=? and type=?''',
(self.is_available, self.item_id, self.item_type))
def select(self):
self.c.execute('''select is_available from item where item_id=? and type=?''',
(self.item_id, self.item_type))
x = self.c.fetchone()
if not x:
raise NoData(
f'No such item `{self.item_type}`: `{self.item_id}`', api_error_code=-121)
self.is_available = x[0]
class UserItem(Item): class UserItem(Item):
@@ -40,7 +71,7 @@ class UserItem(Item):
self.c = c self.c = c
self.user = None self.user = None
def select(self, user=None): def select_user_item(self, user=None):
''' '''
查询用户item\ 查询用户item\
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
@@ -302,7 +333,8 @@ class ItemFactory:
elif item_type == 'course_banner': elif item_type == 'course_banner':
return CourseBanner(self.c) return CourseBanner(self.c)
else: else:
raise InputError('The item type `%s` is wrong.' % item_type) raise InputError(
f'The item type `{item_type}` is invalid.', api_error_code=-120)
@classmethod @classmethod
def from_dict(cls, d: dict, c=None): def from_dict(cls, d: dict, c=None):

View File

@@ -116,8 +116,8 @@ class RemoteMultiPlayer:
self.data_recv = received.split('|') self.data_recv = received.split('|')
if self.data_recv[0] != '0': if self.data_recv[0] != '0':
raise ArcError('Link Play error.', code = int(self.data_recv[0])
int(self.data_recv[0]), status=400) raise ArcError(f'Link Play error code: {code}', code, status=400)
def create_room(self, user: 'Player' = None) -> None: def create_room(self, user: 'Player' = None) -> None:
'''创建房间''' '''创建房间'''

View File

@@ -0,0 +1,250 @@
from .download import DownloadList
from .error import NoData
from .save import SaveData
from .score import Score
from .sql import Connect, Sql
from .user import User
class BaseOperation:
_name: str = None
def __init__(self, *args, **kwargs):
pass
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
class RefreshAllScoreRating(BaseOperation):
'''
刷新所有成绩的评分
'''
_name = 'refresh_all_score_rating'
def run(self):
# 追求效率不用Song类尽量不用对象
# 但其实还是很慢
with Connect() as c:
c.execute(
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
x = c.fetchall()
songs = [i[0] for i in x]
c.execute(
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
for i in x:
for j in range(0, 4):
defnum = -10 # 没在库里的全部当做定数-10
if i[j+1] is not None and i[j+1] > 0:
defnum = float(i[j+1]) / 10
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
'a': i[0], 'b': j})
y = c.fetchall()
values = []
where_values = []
for k in y:
ptt = Score.calculate_rating(defnum, k[1])
if ptt < 0:
ptt = 0
values.append((ptt,))
where_values.append((k[0], i[0], j))
if values:
Sql(c).update_many('best_score', ['rating'], values, [
'user_id', 'song_id', 'difficulty'], where_values)
class RefreshSongFileCache(BaseOperation):
'''
刷新歌曲文件缓存包括文件hash缓存重建、文件目录重遍历、songlist重解析
注意在设置里预先计算关闭的情况下文件hash不会计算
'''
_name = 'refresh_song_file_cache'
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 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:
`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)
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(
'''select exists(select * from user where user_id = ?)''', (self.user.user_id,))
if not c.fetchone()[0]:
raise NoData(
f'No such user: `{self.user.user_id}`', api_error_code=-110)
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)

View File

@@ -1,8 +1,7 @@
from time import time from time import time
from core.item import ItemFactory from .error import ArcError, DataExist, NoData
from .item import ItemFactory
from .error import ArcError, NoData
class Present: class Present:
@@ -18,15 +17,39 @@ class Present:
def is_expired(self) -> bool: def is_expired(self) -> bool:
return self.expire_ts < int(time() * 1000) return self.expire_ts < int(time() * 1000)
def to_dict(self) -> dict: def to_dict(self, has_items: bool = True) -> dict:
return { r = {
'present_id': self.present_id, 'present_id': self.present_id,
'expire_ts': self.expire_ts, 'expire_ts': self.expire_ts,
'description': self.description, 'description': self.description
'items': [x.to_dict() for x in self.items]
} }
if has_items:
r['items'] = [x.to_dict() for x in self.items]
return r
def select(self, present_id: str = None) -> None: def from_dict(self, d: dict) -> 'Present':
self.present_id = d['present_id']
self.expire_ts = int(d['expire_ts'])
self.description = d.get('description', '')
self.items = [ItemFactory.from_dict(
i, c=self.c) for i in d.get('items', [])]
return self
def from_list(self, l: list) -> 'Present':
self.present_id = l[0]
self.expire_ts = int(l[1]) if l[1] else 0
self.description = l[2] if l[2] else ''
return self
def select_exists(self) -> bool:
'''
查询present是否存在
'''
self.c.execute(
'''select exists(select * from present where present_id=?)''', (self.present_id,))
return bool(self.c.fetchone()[0])
def select(self, present_id: str = None) -> 'Present':
''' '''
用present_id查询信息 用present_id查询信息
''' '''
@@ -39,8 +62,8 @@ class Present:
if x is None: if x is None:
raise NoData('The present `%s` does not exist.' % self.present_id) raise NoData('The present `%s` does not exist.' % self.present_id)
self.expire_ts = x[1] if x[1] else 0 self.from_list(x)
self.description = x[2] if x[2] else '' return self
def select_items(self) -> None: def select_items(self) -> None:
''' '''
@@ -48,15 +71,79 @@ class Present:
''' '''
self.c.execute( self.c.execute(
'''select * from present_item where present_id=:a''', {'a': self.present_id}) '''select * from present_item where present_id=:a''', {'a': self.present_id})
x = self.c.fetchall()
if not x:
raise NoData('The present `%s` does not have any items.' %
self.present_id)
self.items = [ItemFactory.from_dict({ self.items = [ItemFactory.from_dict({
'item_id': i[1], 'item_id': i[1],
'type': i[2], 'type': i[2],
'amount': i[3] if i[3] else 1 'amount': i[3] if i[3] else 1
}, self.c) for i in x] }, self.c) for i in self.c.fetchall()]
def insert_items(self) -> None:
for i in self.items:
self.c.execute('''insert or ignore into item values(?,?,?)''',
(i.item_id, i.item_type, i.is_available))
self.c.execute('''insert or ignore into present_item values(?,?,?,?)''',
(self.present_id, i.item_id, i.item_type, i.amount))
def insert(self) -> None:
self.c.execute('''insert into present values(?,?,?)''',
(self.present_id, self.expire_ts, self.description))
def insert_all(self) -> None:
self.insert()
self.insert_items()
def delete(self) -> None:
self.c.execute(
'''delete from present where present_id=?''', (self.present_id,))
def delete_present_item(self) -> None:
self.c.execute(
'''delete from present_item where present_id=?''', (self.present_id,))
def delete_all(self) -> None:
self.delete_present_item()
self.delete()
def update(self) -> None:
self.c.execute('''update present set expire_ts=?, description=? where present_id=?''',
(self.expire_ts, self.description, self.present_id))
def remove_items(self, items: list) -> None:
'''删除present_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in present `{self.present_id}`', api_error_code=-124)
self.c.executemany('''delete from present_item where present_id=? and item_id=? and type=?''', [
(self.present_id, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def add_items(self, items: list) -> None:
'''添加物品到present_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in present `{self.present_id}`', api_error_code=-123)
self.c.executemany('''insert into present_item values(?,?,?,?)''', [
(self.present_id, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def update_items(self, items: list) -> None:
'''更新present_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in present `{self.present_id}`', api_error_code=-124)
self.c.executemany('''update present_item set amount=? where present_id=? and item_id=? and type=?''', [
(i.amount, self.present_id, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class UserPresent(Present): class UserPresent(Present):

View File

@@ -1,6 +1,6 @@
from time import time from time import time
from .error import NoData, TicketNotEnough from .error import DataExist, InputError, NoData, TicketNotEnough
from .item import ItemFactory from .item import ItemFactory
@@ -22,6 +22,8 @@ class Purchase:
self.items: list = [] self.items: list = []
# TODO: "discount_reason": "extend"
@property @property
def price_displayed(self) -> int: def price_displayed(self) -> int:
''' '''
@@ -32,31 +34,35 @@ class Purchase:
if self.discount_reason == 'anni5tix': if self.discount_reason == 'anni5tix':
x = ItemFactory(self.c).get_item('anni5tix') x = ItemFactory(self.c).get_item('anni5tix')
x.item_id = 'anni5tix' x.item_id = 'anni5tix'
x.select(self.user) x.select_user_item(self.user)
if x.amount >= 1: if x.amount >= 1:
return 0 return 0
return self.price return self.price
return self.orig_price return self.orig_price
def to_dict(self) -> dict: def to_dict(self, has_items: bool = True, show_real_price: bool = True) -> dict:
price = self.price_displayed if show_real_price:
price = self.price_displayed
r = { r = {
'name': self.purchase_name, 'name': self.purchase_name,
'price': price, 'price': price if show_real_price else self.price,
'orig_price': self.orig_price, 'orig_price': self.orig_price,
'items': [x.to_dict(has_is_available=True) for x in self.items]
} }
if has_items:
r['items'] = [x.to_dict(has_is_available=True) for x in self.items]
if self.discount_from > 0 and self.discount_to > 0: if self.discount_from > 0 and self.discount_to > 0:
r['discount_from'] = self.discount_from r['discount_from'] = self.discount_from
r['discount_to'] = self.discount_to r['discount_to'] = self.discount_to
if self.discount_reason == 'anni5tix' and price == 0: if not show_real_price or (self.discount_reason == 'anni5tix' and price == 0):
r['discount_reason'] = self.discount_reason r['discount_reason'] = self.discount_reason
return r return r
def from_dict(self, d: dict) -> 'Purchase': def from_dict(self, d: dict) -> 'Purchase':
self.purchase_name = d['name'] self.purchase_name = d.get('name') or d.get('purchase_name')
self.price = d['price'] if not self.purchase_name:
self.orig_price = d['orig_price'] raise InputError('purchase_name is required')
self.orig_price = int(d['orig_price'])
self.price = d.get('price', self.orig_price)
self.discount_from = d.get('discount_from', -1) self.discount_from = d.get('discount_from', -1)
self.discount_to = d.get('discount_to', -1) self.discount_to = d.get('discount_to', -1)
self.discount_reason = d.get('discount_reason', '') self.discount_reason = d.get('discount_reason', '')
@@ -65,24 +71,50 @@ class Purchase:
return self return self
def from_list(self, l: list) -> 'Purchase':
self.purchase_name = l[0]
self.price = l[1]
self.orig_price = l[2]
self.discount_from = l[3] if l[3] else -1
self.discount_to = l[4] if l[4] else -1
self.discount_reason = l[5] if l[5] else ''
return self
def insert_all(self) -> None: def insert_all(self) -> None:
'''向数据库插入包括item表和purchase_item表''' '''向数据库插入包括item表和purchase_item表'''
self.insert()
self.insert_items()
def insert(self) -> None:
'''向数据库插入不包括item表和purchase_item表'''
self.c.execute('''insert into purchase values(?,?,?,?,?,?)''', self.c.execute('''insert into purchase values(?,?,?,?,?,?)''',
(self.purchase_name, self.price, self.orig_price, self.discount_from, self.discount_to, self.discount_reason)) (self.purchase_name, self.price, self.orig_price, self.discount_from, self.discount_to, self.discount_reason))
self.insert_items()
# def insert_only_purchase_item(self) -> None:
# '''向数据库插入purchase_item表'''
# for i in self.items:
# self.c.execute('''insert into purchase_item values(?,?,?,?)''',
# (self.purchase_name, i.item_id, i.item_type, i.amount))
def insert_items(self) -> None: def insert_items(self) -> None:
'''向数据库插入物品,注意已存在的物品不会变更''' '''向数据库插入物品,注意已存在的物品不会变更'''
for i in self.items: for i in self.items:
self.c.execute( self.c.execute('''insert or ignore into item values(?,?,?)''',
'''select exists(select * from item where item_id=?)''', (i.item_id,)) (i.item_id, i.item_type, i.is_available))
if self.c.fetchone() == (0,):
self.c.execute('''insert into item values(?,?,?)''',
(i.item_id, i.item_type, i.is_available))
self.c.execute('''insert into purchase_item values(?,?,?,?)''', self.c.execute('''insert or ignore into purchase_item values(?,?,?,?)''',
(self.purchase_name, i.item_id, i.item_type, i.amount)) (self.purchase_name, i.item_id, i.item_type, i.amount))
def select_exists(self, purchase_name: str = None) -> bool:
'''
用purchase_name查询存在性
'''
if purchase_name:
self.purchase_name = purchase_name
self.c.execute(
'''select exists(select * from purchase where purchase_name=?)''', (self.purchase_name,))
return self.c.fetchone() == (1,)
def select(self, purchase_name: str = None) -> 'Purchase': def select(self, purchase_name: str = None) -> 'Purchase':
''' '''
用purchase_name查询信息 用purchase_name查询信息
@@ -91,11 +123,11 @@ class Purchase:
self.purchase_name = purchase_name self.purchase_name = purchase_name
self.c.execute( self.c.execute(
'''select * from purchase where purchase_name=:name''', {'name': purchase_name}) '''select * from purchase where purchase_name=:name''', {'name': self.purchase_name})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('The purchase `%s` does not exist.' % raise NoData(
purchase_name, 501) f'Purchase `{self.purchase_name}` does not exist.', 501)
self.price = x[1] self.price = x[1]
self.orig_price = x[2] self.orig_price = x[2]
@@ -110,9 +142,9 @@ class Purchase:
self.c.execute( self.c.execute(
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name}) '''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
x = self.c.fetchall() x = self.c.fetchall()
if not x: # if not x:
raise NoData('The items of the purchase `%s` does not exist.' % # raise NoData(
self.purchase_name, 501) # f'The items of the purchase `{self.purchase_name}` does not exist.', 501)
self.items = [] self.items = []
t = None t = None
@@ -160,6 +192,61 @@ class Purchase:
for i in self.items: for i in self.items:
i.user_claim_item(self.user) i.user_claim_item(self.user)
def delete_purchase_item(self) -> None:
'''删除purchase_item表'''
self.c.execute(
'''delete from purchase_item where purchase_name=?''', (self.purchase_name, ))
def delete(self) -> None:
'''删除purchase表'''
self.c.execute(
'''delete from purchase where purchase_name=?''', (self.purchase_name, ))
def delete_all(self) -> None:
'''删除purchase表和purchase_item表'''
self.delete_purchase_item()
self.delete()
def update(self) -> None:
'''更新purchase表'''
self.c.execute('''update purchase set price=:a, orig_price=:b, discount_from=:c, discount_to=:d, discount_reason=:e where purchase_name=:f''', {
'a': self.price, 'b': self.orig_price, 'c': self.discount_from, 'd': self.discount_to, 'e': self.discount_reason, 'f': self.purchase_name})
def add_items(self, items: list) -> None:
'''添加purchase_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in purchase `{self.purchase_name}`', api_error_code=-123)
self.c.executemany('''insert into purchase_item values (?, ?, ?, ?)''', [
(self.purchase_name, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def remove_items(self, items: list) -> None:
'''删除purchase_item表'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in purchase `{self.purchase_name}`', api_error_code=-124)
self.c.executemany('''delete from purchase_item where purchase_name=? and item_id=? and type=?''', [
(self.purchase_name, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def update_items(self, items: list) -> None:
'''更新purchase_item表只能更新amount'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in purchase `{self.purchase_name}`', api_error_code=-124)
self.c.executemany('''update purchase_item set amount=? where purchase_name=? and item_id=? and type=?''', [
(i.amount, self.purchase_name, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class PurchaseList: class PurchaseList:
''' '''

View File

@@ -1,4 +1,4 @@
from .error import NoData, RedeemUnavailable from .error import DataExist, NoData, RedeemUnavailable
from .item import ItemFactory from .item import ItemFactory
@@ -11,29 +11,117 @@ class Redeem:
self.items: list = [] self.items: list = []
self.fragment: int = None self.fragment: int = None
def select(self, code: str = None) -> None: def to_dict(self, has_items: bool = True) -> dict:
r = {
'code': self.code,
'type': self.redeem_type
}
if has_items:
r['items'] = [x.to_dict() for x in self.items]
return r
def from_dict(self, d: dict) -> 'Redeem':
self.code = str(d['code'])
self.redeem_type = int(d.get('type') or d.get('redeem_type', 0))
self.items = [ItemFactory.from_dict(
i, c=self.c) for i in d.get('items', [])]
return self
def from_list(self, l: list) -> 'Redeem':
self.code = l[0]
self.redeem_type = l[1]
return self
def select_exists(self) -> bool:
self.c.execute(
'''select exists(select * from redeem where code=?)''', (self.code,))
return bool(self.c.fetchone()[0])
def select(self, code: str = None) -> 'Redeem':
if code: if code:
self.code = code self.code = code
self.c.execute('''select * from redeem where code=:a''', self.c.execute('''select * from redeem where code=:a''',
{'a': self.code}) {'a': self.code})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The redeem `%s` does not exist.' % self.code, 504) raise NoData(f'The redeem `{self.code}` does not exist.', 504)
self.redeem_type = x[1] self.redeem_type = x[1]
return self
def select_items(self) -> None: def select_items(self) -> None:
self.c.execute('''select * from redeem_item where code=:a''', self.c.execute('''select * from redeem_item where code=:a''',
{'a': self.code}) {'a': self.code})
x = self.c.fetchall()
if not x:
raise NoData(
'The redeem `%s` does not have any items.' % self.code)
self.items = [ItemFactory.from_dict({ self.items = [ItemFactory.from_dict({
'item_id': i[1], 'item_id': i[1],
'type': i[2], 'type': i[2],
'amount': i[3] if i[3] else 1 'amount': i[3] if i[3] else 1
}, self.c) for i in x] }, self.c) for i in self.c.fetchall()]
def insert(self) -> None:
self.c.execute('''insert into redeem values(?,?)''',
(self.code, self.redeem_type))
def insert_items(self) -> None:
for i in self.items:
i.insert(ignore=True)
self.c.execute('''insert into redeem_item values(?,?,?,?)''', (
self.code, i.item_id, i.item_type, i.amount))
def insert_all(self) -> None:
self.insert()
self.insert_items()
def delete(self) -> None:
self.c.execute('''delete from redeem where code=?''', (self.code,))
def delete_redeem_item(self) -> None:
self.c.execute(
'''delete from redeem_item where code=?''', (self.code,))
def delete_all(self) -> None:
self.delete_redeem_item()
self.delete()
def update(self) -> None:
self.c.execute('''update redeem set type=? where code=?''',
(self.redeem_type, self.code))
def remove_items(self, items: list) -> None:
'''删除redeem_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in redeem `{self.code}`', api_error_code=-124)
self.c.executemany('''delete from redeem_item where code=? and item_id=? and type=?''', [
(self.code, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def add_items(self, items: list) -> None:
'''添加物品到redeem_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in redeem `{self.code}`', api_error_code=-123)
self.c.executemany('''insert into redeem_item values(?,?,?,?)''', [
(self.code, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def update_items(self, items: list) -> None:
'''更新redeem_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in redeem `{self.code}`', api_error_code=-124)
self.c.executemany('''update redeem_item set amount=? where code=? and item_id=? and type=?''', [
(i.amount, self.code, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class UserRedeem(Redeem): class UserRedeem(Redeem):

View File

@@ -3,7 +3,7 @@ from time import time
from .config_manager import Config from .config_manager import Config
from .constant import Constant from .constant import Constant
from .error import InputError from .error import InputError, NoData
from .util import md5 from .util import md5
@@ -54,6 +54,20 @@ class SaveData:
} }
} }
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: def select_all(self, user) -> None:
''' '''
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例

View File

@@ -7,7 +7,7 @@ from .course import CoursePlay
from .error import NoData, StaminaNotEnough from .error import NoData, StaminaNotEnough
from .item import ItemCore from .item import ItemCore
from .song import Chart from .song import Chart
from .sql import Query, Sql from .sql import Connect, Query, Sql
from .util import md5 from .util import md5
from .world import WorldPlay from .world import WorldPlay
@@ -102,8 +102,8 @@ class Score:
return True return True
@staticmethod @staticmethod
def calculate_rating(defnum: int, score: int) -> float: def calculate_rating(defnum: float, score: int) -> float:
'''计算rating谱面定数小于等于0视为Unrank这里的defnum = Chart const''' '''计算rating谱面定数小于等于0视为Unrank返回值会为-1这里的defnum = Chart const'''
if not defnum or defnum <= 0: if not defnum or defnum <= 0:
# 谱面没定数或者定数小于等于0被视作Unrank # 谱面没定数或者定数小于等于0被视作Unrank
return -1 return -1
@@ -209,6 +209,7 @@ class UserPlay(UserScore):
self.stamina_multiply: int = None self.stamina_multiply: int = None
self.fragment_multiply: int = None self.fragment_multiply: int = None
self.prog_boost_multiply: int = None self.prog_boost_multiply: int = None
self.beyond_boost_gauge_usage: int = None
self.ptt: Potential = None # 临时用来计算用户ptt的 self.ptt: Potential = None # 临时用来计算用户ptt的
self.world_play: 'WorldPlay' = None self.world_play: 'WorldPlay' = None
@@ -228,7 +229,7 @@ class UserPlay(UserScore):
r['user_rating'] = self.user.rating_ptt r['user_rating'] = self.user.rating_ptt
r['finale_challenge_higher'] = self.rating > self.ptt.value r['finale_challenge_higher'] = self.rating > self.ptt.value
r['global_rank'] = self.user.global_rank r['global_rank'] = self.user.global_rank
r['finale_play_value'] = self.rating * 5 # emmmm r['finale_play_value'] = 9.065 * self.rating ** 0.5 # by Lost-MSth
return r return r
@property @property
@@ -285,24 +286,29 @@ class UserPlay(UserScore):
self.stamina_multiply = int(x[8]) self.stamina_multiply = int(x[8])
self.fragment_multiply = int(x[9]) self.fragment_multiply = int(x[9])
self.prog_boost_multiply = int(x[10]) self.prog_boost_multiply = int(x[10])
self.beyond_boost_gauge_usage = int(x[11])
self.is_world_mode = True self.is_world_mode = True
self.course_play_state = -1 self.course_play_state = -1
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0) -> None: def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0) -> None:
self.song_token = b64encode(urandom(64)).decode() self.song_token = b64encode(urandom(64)).decode()
self.stamina_multiply = int(stamina_multiply) self.stamina_multiply = int(stamina_multiply)
self.fragment_multiply = int(fragment_multiply) self.fragment_multiply = int(fragment_multiply)
self.prog_boost_multiply = int(prog_boost_multiply) self.prog_boost_multiply = int(prog_boost_multiply)
if self.prog_boost_multiply != 0: self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
self.c.execute('''select prog_boost from user where user_id=:a''', { if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
'a': self.user.user_id}) 'a': self.user.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if x and x[0] == 300: if x:
self.prog_boost_multiply = 300 self.prog_boost_multiply = 300 if x[0] == 300 else 0
if x[1] < self.beyond_boost_gauge_usage or (self.beyond_boost_gauge_usage != 100 and self.beyond_boost_gauge_usage != 200):
# 注意偷懒了没判断是否是beyond图
self.beyond_boost_gauge_usage = 0
self.clear_play_state() self.clear_play_state()
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f)''', { self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g)''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 't': self.song_token}) 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 't': self.song_token})
self.user.select_user_about_current_map() self.user.select_user_about_current_map()
self.user.current_map.select_map_info() self.user.current_map.select_map_info()
@@ -334,8 +340,8 @@ class UserPlay(UserScore):
self.course_play.score = 0 self.course_play.score = 0
self.course_play.clear_type = 3 # 设置为PM即最大值 self.course_play.clear_type = 3 # 设置为PM即最大值
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0)''', (self.song_token, self.user.user_id, self.song.song_id, self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0)''', (self.song_token, self.user.user_id, self.song.song_id,
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type)) self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
self.user.select_user_about_stamina() self.user.select_user_about_stamina()
if use_course_skip_purchase: if use_course_skip_purchase:
x = ItemCore(self.c) x = ItemCore(self.c)
@@ -408,6 +414,12 @@ class UserPlay(UserScore):
self.ptt.insert_recent_30() self.ptt.insert_recent_30()
def record_score(self) -> None:
'''向log数据库记录分数请注意列名不同'''
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2:
c2.execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def upload_score(self) -> None: def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算''' '''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
self.get_play_state() self.get_play_state()
@@ -420,6 +432,9 @@ class UserPlay(UserScore):
self.time_played = int(time()) self.time_played = int(time())
# 记录分数
self.record_score()
# recent更新 # recent更新
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', { self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000}) 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})

View File

@@ -1,4 +1,5 @@
from .error import NoData from .error import NoData
from .config_manager import Config
class Chart: class Chart:
@@ -33,14 +34,16 @@ class Chart:
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id}) '''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
self.defnum = -10 if Config.ALLOW_SCORE_WITH_NO_SONG:
# raise NoData('The song `%s` does not exist.' % self.song_id) self.defnum = -10
else:
raise NoData(f'The song `{self.song_id}` does not exist.', 120)
else: else:
self.defnum = x[self.difficulty] self.defnum = x[self.difficulty]
class Song: 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.c = c
self.song_id: str = song_id self.song_id: str = song_id
self.name: str = None self.name: str = None
@@ -65,10 +68,39 @@ class Song:
self.charts[3].defnum = x[5] self.charts[3].defnum = x[5]
return self 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: def insert(self) -> None:
self.c.execute( 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)) '''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': def select(self, song_id: str = None) -> 'Song':
if song_id is not None: if song_id is not None:
self.song_id = song_id self.song_id = song_id
@@ -77,6 +109,6 @@ class Song:
'a': self.song_id}) 'a': self.song_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: 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) return self.from_list(x)

View File

@@ -2,16 +2,15 @@ import sqlite3
import traceback import traceback
from atexit import register from atexit import register
from flask import current_app
from .constant import Constant from .constant import Constant
from .error import ArcError, InputError from .error import ArcError, InputError
class Connect: class Connect:
# 数据库连接类,上下文管理 # 数据库连接类,上下文管理
logger = None
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False): def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
""" """
数据库连接默认连接arcaea_database.db\ 数据库连接默认连接arcaea_database.db\
接受:文件路径\ 接受:文件路径\
@@ -19,6 +18,8 @@ class Connect:
""" """
self.file_path = file_path self.file_path = file_path
self.in_memory: bool = in_memory self.in_memory: bool = in_memory
if logger is not None:
self.logger = logger
def __enter__(self) -> sqlite3.Cursor: def __enter__(self) -> sqlite3.Cursor:
if self.in_memory: if self.in_memory:
@@ -37,7 +38,7 @@ class Connect:
else: else:
self.conn.rollback() self.conn.rollback()
current_app.logger.error( self.logger.error(
traceback.format_exception(exc_type, exc_val, exc_tb)) traceback.format_exception(exc_type, exc_val, exc_tb))
self.conn.commit() self.conn.commit()
@@ -49,9 +50,9 @@ class Connect:
class Query: class Query:
'''查询参数类''' '''查询参数类'''
def __init__(self, query_able: list = None, quzzy_query_able: list = None, sort_able: list = None) -> None: def __init__(self, query_able: list = None, fuzzy_query_able: list = None, sort_able: list = None) -> None:
self.query_able: list = query_able # None表示不限制 self.query_able: list = query_able # None表示不限制
self.quzzy_query_able: list = quzzy_query_able # None表示不限制 self.fuzzy_query_able: list = fuzzy_query_able # None表示不限制
self.sort_able: list = sort_able self.sort_able: list = sort_able
self.__limit: int = -1 self.__limit: int = -1
@@ -115,7 +116,7 @@ class Query:
def fuzzy_query_append(self, fuzzy_query: dict) -> None: def fuzzy_query_append(self, fuzzy_query: dict) -> None:
if not isinstance(fuzzy_query, dict): if not isinstance(fuzzy_query, dict):
raise InputError(api_error_code=-101) raise InputError(api_error_code=-101)
if self.quzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.quzzy_query_able)): if self.fuzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.fuzzy_query_able)):
raise InputError(api_error_code=-102) raise InputError(api_error_code=-102)
if not self.__fuzzy_query: if not self.__fuzzy_query:
self.__fuzzy_query = fuzzy_query self.__fuzzy_query = fuzzy_query
@@ -216,7 +217,7 @@ class Sql:
return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')' return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')'
@staticmethod @staticmethod
def get_update_sql(table_name: str, d: dict = {}, query: 'Query' = None) -> str: def get_update_sql(table_name: str, d: dict = None, query: 'Query' = None):
if not d: if not d:
return None return None
sql_list = [] sql_list = []
@@ -245,6 +246,13 @@ class Sql:
return sql, sql_list return sql, sql_list
@staticmethod
def get_update_many_sql(table_name: str, key: list = None, where_key: list = None) -> str:
'''拼接update语句这里不用Query类也不用字典请注意只返回sql语句'''
if not key or not where_key:
return None
return f"update {table_name} set {','.join([f'{k}=?' for k in key])} where {' and '.join([f'{k}=?' for k in where_key])}"
@staticmethod @staticmethod
def get_delete_sql(table_name: str, query: 'Query' = None): def get_delete_sql(table_name: str, query: 'Query' = None):
'''拼接删除语句query中只有query和fuzzy_query会被处理''' '''拼接删除语句query中只有query和fuzzy_query会被处理'''
@@ -304,6 +312,13 @@ class Sql:
sql, sql_list = self.get_update_sql(table_name, d, query) sql, sql_list = self.get_update_sql(table_name, d, query)
self.c.execute(sql, sql_list) self.c.execute(sql, sql_list)
def update_many(self, table_name: str, key: list, value_list: list, where_key: list, where_value_list: list) -> None:
'''单表内行update多句sql语句这里不用Query类也不用字典要求值list长度一致有点像insert_many'''
if not key or not value_list or not where_key or not where_value_list or not len(key) == len(value_list[0]) or not len(where_key) == len(where_value_list[0]) or not len(value_list) == len(where_value_list):
raise ValueError
self.c.executemany(self.get_update_many_sql(
table_name, key, where_key), [x + y for x, y in zip(value_list, where_value_list)])
def delete(self, table_name: str, query: 'Query' = None) -> None: def delete(self, table_name: str, query: 'Query' = None) -> None:
'''删除query中只有query和fuzzy_query会被处理''' '''删除query中只有query和fuzzy_query会被处理'''
sql, sql_list = self.get_delete_sql(table_name, query) sql, sql_list = self.get_delete_sql(table_name, query)
@@ -348,8 +363,10 @@ class DatabaseMigrator:
if db1_pk != db2_pk: if db1_pk != db2_pk:
return False return False
sql2.insert_many(table_name, [], sql1.select( public_column = list(filter(lambda x: x in db2_name, db1_name))
table_name, list(filter(lambda x: x in db2_name, db1_name))), insert_type='replace')
sql2.insert_many(table_name, public_column, sql1.select(
table_name, public_column), insert_type='replace')
return True return True
@@ -366,13 +383,6 @@ class DatabaseMigrator:
c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [ c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [
(j[0], i[0], i[1], exp, i[2], 0) for j in y]) (j[0], i[0], i[1], exp, i[2], 0) for j in y])
@staticmethod
def update_user_epilogue(c) -> None:
'''给用户添加epilogue包'''
c.execute('''select user_id from user''')
Sql(c).insert_many('user_item', [], [(i[0], 'epilogue', 'pack', 1)
for i in c.fetchall()], insert_type='ignore')
def update_database(self) -> None: def update_database(self) -> None:
''' '''
将c1数据库不存在数据加入或覆盖到c2数据库上 将c1数据库不存在数据加入或覆盖到c2数据库上
@@ -387,7 +397,6 @@ class DatabaseMigrator:
self.update_one_table(c1, c2, 'character') self.update_one_table(c1, c2, 'character')
self.update_user_char_full(c2) # 更新user_char_full self.update_user_char_full(c2) # 更新user_char_full
self.update_user_epilogue(c2) # 更新user的epilogue
class MemoryDatabase: class MemoryDatabase:

View File

@@ -243,8 +243,9 @@ class UserLogin(User):
'name': self.name}) 'name': self.name})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: 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) self.now = int(time.time() * 1000)
if x[2] is not None and x[2] != '': if x[2] is not None and x[2] != '':
# 自动封号检查 # 自动封号检查
@@ -255,14 +256,14 @@ class UserLogin(User):
if x[1] == '': 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: if x[1] != self.hash_pwd:
raise NoAccess('Wrong password.', 104) raise NoAccess('Wrong password.', 104)
self.user_id = str(x[0])
self.token = base64.b64encode(hashlib.sha256( 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( self.c.execute(
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id}) '''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
@@ -304,7 +305,8 @@ class UserInfo(User):
self.recent_score = Score() self.recent_score = Score()
self.favorite_character = None self.favorite_character = None
self.max_stamina_notification_enabled = False self.max_stamina_notification_enabled = False
self.prog_boost = 0 self.prog_boost: int = 0
self.beyond_boost_gauge: float = 0
self.next_fragstam_ts: int = None self.next_fragstam_ts: int = None
self.world_mode_locked_end_ts: int = None self.world_mode_locked_end_ts: int = None
@@ -495,6 +497,7 @@ class UserInfo(User):
"is_skill_sealed": self.is_skill_sealed, "is_skill_sealed": self.is_skill_sealed,
"current_map": self.current_map.map_id, "current_map": self.current_map.map_id,
"prog_boost": self.prog_boost, "prog_boost": self.prog_boost,
"beyond_boost_gauge": self.beyond_boost_gauge,
"next_fragstam_ts": self.next_fragstam_ts, "next_fragstam_ts": self.next_fragstam_ts,
"max_stamina_ts": self.stamina.max_stamina_ts, "max_stamina_ts": self.stamina.max_stamina_ts,
"stamina": self.stamina.stamina, "stamina": self.stamina.stamina,
@@ -552,6 +555,7 @@ class UserInfo(User):
self.stamina = UserStamina(self.c, self) self.stamina = UserStamina(self.c, self)
self.stamina.set_value(x[32], x[33]) self.stamina.set_value(x[32], x[33])
self.world_mode_locked_end_ts = x[34] if x[34] else -1 self.world_mode_locked_end_ts = x[34] if x[34] else -1
self.beyond_boost_gauge = x[35] if x[35] else 0
return self return self
@@ -609,7 +613,7 @@ class UserInfo(User):
查询user表有关世界模式打歌的信息 查询user表有关世界模式打歌的信息
''' '''
self.c.execute( self.c.execute(
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts from user where user_id=?''', (self.user_id,)) '''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge from user where user_id=?''', (self.user_id,))
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -622,6 +626,7 @@ class UserInfo(User):
self.character.is_uncapped_override = x[5] == 1 self.character.is_uncapped_override = x[5] == 1
self.current_map = UserMap(self.c, x[6], self) self.current_map = UserMap(self.c, x[6], self)
self.world_mode_locked_end_ts = x[7] if x[7] else -1 self.world_mode_locked_end_ts = x[7] if x[7] else -1
self.beyond_boost_gauge = x[8] if x[8] else 0
@property @property
def global_rank(self) -> int: def global_rank(self) -> int:

View File

@@ -13,13 +13,11 @@ def md5(code: str) -> str:
def get_file_md5(file_path: str) -> str: def get_file_md5(file_path: str) -> str:
'''计算文件MD5''' '''计算文件MD5,假设是文件'''
if not os.path.isfile(file_path):
return None
myhash = hashlib.md5() myhash = hashlib.md5()
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
while True: while True:
b = f.read(8096) b = f.read(8192)
if not b: if not b:
break break
myhash.update(b) myhash.update(b)

View File

@@ -50,6 +50,7 @@ class Step:
self.restrict_id: str = None self.restrict_id: str = None
self.restrict_ids: list = [] self.restrict_ids: list = []
self.restrict_type: str = None self.restrict_type: str = None
self.restrict_difficulty: int = None
self.step_type: list = None self.step_type: list = None
self.speed_limit_value: int = None self.speed_limit_value: int = None
self.plus_stamina_value: int = None self.plus_stamina_value: int = None
@@ -61,12 +62,14 @@ class Step:
} }
if self.items: if self.items:
r['items'] = [i.to_dict() for i in 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: if self.restrict_type:
r['restrict_type'] = 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: if self.step_type:
r['step_type'] = self.step_type r['step_type'] = self.step_type
if self.speed_limit_value: if self.speed_limit_value:
@@ -82,6 +85,7 @@ class Step:
self.restrict_id = d.get('restrict_id') self.restrict_id = d.get('restrict_id')
self.restrict_ids = d.get('restrict_ids') self.restrict_ids = d.get('restrict_ids')
self.restrict_type = d.get('restrict_type') self.restrict_type = d.get('restrict_type')
self.restrict_difficulty = d.get('restrict_difficulty')
self.step_type = d.get('step_type') self.step_type = d.get('step_type')
self.speed_limit_value = d.get('speed_limit_value') self.speed_limit_value = d.get('speed_limit_value')
self.plus_stamina_value = d.get('plus_stamina_value') self.plus_stamina_value = d.get('plus_stamina_value')
@@ -102,7 +106,7 @@ class Map:
self.available_from: int = None self.available_from: int = None
self.available_to: int = None self.available_to: int = None
self.is_repeatable: bool = 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_type: str = None
self.require_value: int = None self.require_value: int = None
self.coordinate: str = None self.coordinate: str = None
@@ -111,6 +115,10 @@ class Map:
self.steps: list = [] self.steps: list = []
self.__rewards: list = None self.__rewards: list = None
self.require_localunlock_songid: str = None
self.require_localunlock_challengeid: str = None
self.chain_info: dict = None
@property @property
def rewards(self) -> list: def rewards(self) -> list:
if self.__rewards is None: if self.__rewards is None:
@@ -133,7 +141,7 @@ class Map:
def to_dict(self) -> dict: def to_dict(self) -> dict:
if self.chapter is None: if self.chapter is None:
self.select_map_info() self.select_map_info()
return { r = {
'map_id': self.map_id, 'map_id': self.map_id,
'is_legacy': self.is_legacy, 'is_legacy': self.is_legacy,
'is_beyond': self.is_beyond, 'is_beyond': self.is_beyond,
@@ -151,8 +159,13 @@ class Map:
'custom_bg': self.custom_bg, 'custom_bg': self.custom_bg,
'stamina_cost': self.stamina_cost, 'stamina_cost': self.stamina_cost,
'step_count': self.step_count, '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], 'steps': [s.to_dict() for s in self.steps],
} }
if self.chain_info is not None:
r['chain_info'] = self.chain_info
return r
def from_dict(self, raw_dict: dict) -> 'Map': def from_dict(self, raw_dict: dict) -> 'Map':
self.is_legacy = raw_dict.get('is_legacy') self.is_legacy = raw_dict.get('is_legacy')
@@ -170,6 +183,11 @@ class Map:
self.coordinate = raw_dict.get('coordinate') self.coordinate = raw_dict.get('coordinate')
self.custom_bg = raw_dict.get('custom_bg', '') self.custom_bg = raw_dict.get('custom_bg', '')
self.stamina_cost = raw_dict.get('stamina_cost') 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.chain_info = raw_dict.get('chain_info')
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')] self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
return self return self
@@ -290,7 +308,7 @@ class UserMap(Map):
if self.require_type in ['pack', 'single']: if self.require_type in ['pack', 'single']:
item = ItemFactory(self.c).get_item(self.require_type) item = ItemFactory(self.c).get_item(self.require_type)
item.item_id = self.require_id item.item_id = self.require_id
item.select(self.user) item.select_user_item(self.user)
if not item.amount: if not item.amount:
self.is_locked = True self.is_locked = True
@@ -436,8 +454,9 @@ class WorldPlay:
self.step_value: float = None self.step_value: float = None
self.prog_tempest: float = None self.prog_tempest: float = None
self.overdrive_extra: float = None
self.character_bonus_progress: float = None self.character_bonus_progress: float = None
self.prog_skill_increase: float = None
self.over_skill_increase: float = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
arcmap: 'UserMap' = self.user.current_map arcmap: 'UserMap' = self.user.current_map
@@ -465,14 +484,17 @@ class WorldPlay:
}, },
"current_stamina": self.user.stamina.stamina, "current_stamina": self.user.stamina.stamina,
"max_stamina_ts": self.user.stamina.max_stamina_ts, "max_stamina_ts": self.user.stamina.max_stamina_ts,
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
'beyond_boost_gauge': self.user.beyond_boost_gauge
} }
if self.overdrive_extra is not None: if self.over_skill_increase is not None:
r['char_stats']['overdrive'] += self.overdrive_extra r['char_stats']['over_skill_increase'] = self.over_skill_increase
if self.prog_skill_increase is not None:
r['char_stats']['prog_skill_increase'] = self.prog_skill_increase
if self.prog_tempest is not None: if self.prog_tempest is not None:
r['char_stats']['prog'] += self.prog_tempest r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样
r['char_stats']['prog_tempest'] = self.prog_tempest r['char_stats']['prog_tempest'] = self.prog_tempest
if self.character_bonus_progress is not None: if self.character_bonus_progress is not None:
@@ -492,16 +514,45 @@ class WorldPlay:
r['fragment_multiply'] = self.user_play.fragment_multiply r['fragment_multiply'] = self.user_play.fragment_multiply
if self.user_play.prog_boost_multiply != 0: if self.user_play.prog_boost_multiply != 0:
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
if self.user_play.beyond_boost_gauge_usage != 0:
r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage
return r return r
@property
def beyond_boost_gauge_addition(self) -> float:
# guessed by Lost-MSth
return 2.45 * self.user_play.rating ** 0.5 + 27
@property @property
def step_times(self) -> float: def step_times(self) -> float:
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * prog_boost_multiply / 100 * beyond_boost_times
@property @property
def exp_times(self) -> float: def exp_times(self) -> float:
return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * prog_boost_multiply / 100 * beyond_boost_times
def get_step(self) -> None: def get_step(self) -> None:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
@@ -510,6 +561,8 @@ class WorldPlay:
self.character_used.level) self.character_used.level)
if self.prog_tempest: if self.prog_tempest:
prog += self.prog_tempest prog += self.prog_tempest
if self.prog_skill_increase:
prog += self.prog_skill_increase
self.step_value = self.base_step_value * prog / 50 * self.step_times self.step_value = self.base_step_value * prog / 50 * self.step_times
else: else:
@@ -528,8 +581,8 @@ class WorldPlay:
overdrive = self.character_used.overdrive.get_value( overdrive = self.character_used.overdrive.get_value(
self.character_used.level) self.character_used.level)
if self.overdrive_extra: if self.over_skill_increase:
overdrive += self.overdrive_extra overdrive += self.over_skill_increase
self.step_value = self.base_step_value * overdrive / \ self.step_value = self.base_step_value * overdrive / \
50 * self.step_times * affinity_multiplier 50 * self.step_times * affinity_multiplier
@@ -542,6 +595,20 @@ class WorldPlay:
self.user_play.clear_play_state() self.user_play.clear_play_state()
self.user.select_user_about_world_play() self.user.select_user_about_world_play()
if self.user_play.beyond_gauge == 0:
# 更新byd大招蓄力条
self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition
if self.user.beyond_boost_gauge > 200:
self.user.beyond_boost_gauge = 200
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge:
self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage
if abs(self.user.beyond_boost_gauge) <= 1e-5:
self.user.beyond_boost_gauge = 0
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
self.character_used = Character() self.character_used = Character()
self.user.character.select_character_info() self.user.character.select_character_info()
@@ -587,9 +654,13 @@ class WorldPlay:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed: if self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
self._special_tempest() self._special_tempest()
elif self.character_used.skill_id_displayed == 'ilith_awakened_skill':
self._ilith_awakened_skill()
else: else:
if self.character_used.skill_id_displayed == 'skill_vita': if self.character_used.skill_id_displayed == 'skill_vita':
self._skill_vita() self._skill_vita()
if self.character_used.skill_id_displayed == 'skill_mika':
self._skill_mika()
def after_climb(self) -> None: def after_climb(self) -> None:
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap, factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
@@ -616,9 +687,9 @@ class WorldPlay:
vita技能overdrive随回忆率提升提升量最多为10\ vita技能overdrive随回忆率提升提升量最多为10\
此处采用线性函数 此处采用线性函数
''' '''
self.overdrive_extra = 0 self.over_skill_increase = 0
if 0 < self.user_play.health <= 100: if 0 < self.user_play.health <= 100:
self.overdrive_extra = self.user_play.health / 10 self.over_skill_increase = self.user_play.health / 10
def _eto_uncap(self) -> None: def _eto_uncap(self) -> None:
'''eto觉醒技能获得残片奖励时世界模式进度加7''' '''eto觉醒技能获得残片奖励时世界模式进度加7'''
@@ -677,3 +748,20 @@ class WorldPlay:
self.character_bonus_progress = -self.step_value / 2 / self.step_times self.character_bonus_progress = -self.step_value / 2 / self.step_times
self.step_value = self.step_value / 2 self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
def _ilith_awakened_skill(self) -> None:
'''
ilith 觉醒技能,曲目通关时步数+6wiki 说是 prog 值+6
'''
if self.user_play.health > 0:
self.prog_skill_increase = 6
def _skill_mika(self) -> None:
'''
mika 技能,通关特定曲目能力值翻倍
'''
if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0:
self.over_skill_increase = self.character_used.overdrive.get_value(
self.character_used.level)
self.prog_skill_increase = self.character_used.prog.get_value(
self.character_used.level)

View File

@@ -1,45 +1,45 @@
class InitData: 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', 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'] '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)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki']
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', 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'] '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_shama', 'skill_milk', 'skill_shikoku', 'skill_mika']
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', '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, 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, 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, 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, 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] 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, 45, 57, 55.5, 47, 33, 26]
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, 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] 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, 45, 70, 37.5, 29, 44, 26]
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, 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] 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, 45, 57, 31, 29, 65, 26]
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, 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] 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, 75, 80, 89.5, 50, 50, 51]
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73,
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92] 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51]
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92] 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47] 65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 89.5, 50, 50, 51]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92] 80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92] 56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51]
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2] 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0]
char_core = { char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
@@ -57,14 +57,15 @@ class InitData:
43: [{'core_id': 'core_chunithm', 'amount': 15}], 43: [{'core_id': 'core_chunithm', 'amount': 15}],
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}], 11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}] 19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', 'amount': 30}], # TODO: check
} }
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase'] 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral']
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", 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'] "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock']
world_unlocks = ["scenery_chap1", "scenery_chap2", world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
@@ -75,12 +76,12 @@ class InitData:
role_caption = ['系统', '管理员', '用户', '查询接口'] role_caption = ['系统', '管理员', '用户', '查询接口']
power = ['system', 'select', 'select_me', 'change', 'change_me', 'grant', power = ['system', 'select', 'select_me', 'change', 'change_me', 'grant',
'grant_inf', 'select_song_rank', 'select_song_info'] 'grant_inf', 'select_song_rank', 'select_song_info', 'select_song_rank_top']
power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限', power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限',
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限'] '自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限', '歌曲排行榜有限查询权限']
role_power = {'system': power, role_power = {'system': power,
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank', 'select_song_info'], 'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank_top', 'select_song_info'],
'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'], 'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'],
'selector': ['select'] 'selector': ['select']
} }

View File

@@ -0,0 +1,25 @@
create table if not exists cache(key text primary key,
value text,
expire_time int
);
create table if not exists user_score(user_id int,
song_id text,
difficulty int,
time_played int,
score int,
shiny_perfect_count int,
perfect_count int,
near_count int,
miss_count int,
health int,
modifier int,
clear_type int,
rating real,
primary key(user_id, song_id, difficulty, time_played)
);
create index if not exists user_score_1 on user_score (song_id, difficulty);
create index if not exists user_score_2 on user_score (time_played);
PRAGMA journal_mode = WAL;
PRAGMA default_cache_size = 4000;

View File

@@ -327,11 +327,10 @@
"is_available": true "is_available": true
} }
], ],
"price": 0, "price": 700,
"orig_price": 700, "orig_price": 700,
"discount_from": 1646784000000, "discount_from": 1646784000000,
"discount_to": 1647388799000, "discount_to": 1647388799000
"discount_reason": "extend"
}, },
{ {
"name": "alice", "name": "alice",
@@ -595,6 +594,11 @@
"id": "finale", "id": "finale",
"is_available": true "is_available": true
}, },
{
"type": "pack",
"id": "epilogue",
"is_available": true
},
{ {
"type": "core", "type": "core",
"amount": 5, "amount": 5,
@@ -622,5 +626,43 @@
], ],
"orig_price": 500, "orig_price": 500,
"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
},
{
"name": "maimai_append_1",
"items": [
{
"type": "pack",
"id": "maimai_append_1",
"is_available": true
},
{
"type": "core",
"amount": 4,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 400,
"price": 400
} }
] ]

View File

@@ -28,8 +28,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "yourvoiceso", "name": "yourvoiceso",
@@ -48,8 +48,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "crosssoul", "name": "crosssoul",
@@ -68,8 +68,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "impurebird", "name": "impurebird",
@@ -88,8 +88,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "auxesia", "name": "auxesia",
@@ -128,8 +128,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "yozakurafubuki", "name": "yozakurafubuki",
@@ -148,8 +148,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "surrender", "name": "surrender",
@@ -168,8 +168,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "metallicpunisher", "name": "metallicpunisher",
@@ -224,8 +224,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "callmyname", "name": "callmyname",
@@ -244,8 +244,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "fallensquare", "name": "fallensquare",
@@ -264,8 +264,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "dropdead", "name": "dropdead",
@@ -304,8 +304,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "astraltale", "name": "astraltale",
@@ -324,8 +324,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "phantasia", "name": "phantasia",
@@ -380,8 +380,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "dottodot", "name": "dottodot",
@@ -400,8 +400,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "dreadnought", "name": "dreadnought",
@@ -420,8 +420,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "mirzam", "name": "mirzam",
@@ -440,8 +440,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "heavenlycaress", "name": "heavenlycaress",
@@ -460,8 +460,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "filament", "name": "filament",
@@ -480,8 +480,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "avantraze", "name": "avantraze",
@@ -518,8 +518,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "saikyostronger", "name": "saikyostronger",
@@ -556,8 +556,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "einherjar", "name": "einherjar",
@@ -576,8 +576,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "laqryma", "name": "laqryma",
@@ -596,8 +596,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "amygdata", "name": "amygdata",
@@ -616,8 +616,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "altale", "name": "altale",
@@ -636,8 +636,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "feelssoright", "name": "feelssoright",
@@ -656,8 +656,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "scarletcage", "name": "scarletcage",
@@ -712,8 +712,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "badtek", "name": "badtek",
@@ -732,8 +732,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "maliciousmischance", "name": "maliciousmischance",
@@ -788,8 +788,8 @@
], ],
"price": 100, "price": 100,
"orig_price": 100, "orig_price": 100,
"discount_from": 1615248000000, "discount_from": 1646784000000,
"discount_to": 1615852799000 "discount_to": 1647388799000
}, },
{ {
"name": "xeraphinite", "name": "xeraphinite",
@@ -807,7 +807,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "xanatos", "name": "xanatos",
@@ -825,7 +827,9 @@
} }
], ],
"price": 100, "price": 100,
"orig_price": 100 "orig_price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "attraqtia", "name": "attraqtia",
@@ -843,7 +847,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "gimmedablood", "name": "gimmedablood",
@@ -879,7 +885,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "theultimacy", "name": "theultimacy",
@@ -897,7 +905,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "rekkaresonanc", "name": "rekkaresonanc",
@@ -945,7 +955,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "eveninginscarlet", "name": "eveninginscarlet",
@@ -981,7 +993,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "goldenslaughterer", "name": "goldenslaughterer",
@@ -999,7 +1013,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "redolentshape", "name": "redolentshape",
@@ -1017,7 +1033,9 @@
} }
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100,
"discount_from": 1646784000000,
"discount_to": 1647388799000
}, },
{ {
"name": "summerfireworks", "name": "summerfireworks",
@@ -1056,29 +1074,11 @@
"price": 100 "price": 100
}, },
{ {
"name": "internetoverdose", "name": "kissinglucifer",
"items": [ "items": [
{ {
"type": "single", "type": "single",
"id": "internetoverdose", "id": "kissinglucifer",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "macromod",
"items": [
{
"type": "single",
"id": "macromod",
"is_available": true "is_available": true
}, },
{ {
@@ -1110,11 +1110,11 @@
"price": 100 "price": 100
}, },
{ {
"name": "kissinglucifer", "name": "macromod",
"items": [ "items": [
{ {
"type": "single", "type": "single",
"id": "kissinglucifer", "id": "macromod",
"is_available": true "is_available": true
}, },
{ {
@@ -1128,11 +1128,11 @@
"price": 100 "price": 100
}, },
{ {
"name": "headbonkache", "name": "internetoverdose",
"items": [ "items": [
{ {
"type": "single", "type": "single",
"id": "headbonkache", "id": "internetoverdose",
"is_available": true "is_available": true
}, },
{ {
@@ -1163,6 +1163,24 @@
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100
}, },
{
"name": "headbonkache",
"items": [
{
"type": "single",
"id": "headbonkache",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{ {
"name": "picopicotranslation", "name": "picopicotranslation",
"items": [ "items": [
@@ -1217,6 +1235,24 @@
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100
}, },
{
"name": "cocorocosmetic",
"items": [
{
"type": "single",
"id": "cocorocosmetic",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{ {
"name": "freemyself", "name": "freemyself",
"items": [ "items": [
@@ -1236,11 +1272,83 @@
"price": 100 "price": 100
}, },
{ {
"name": "cocorocosmetic", "name": "capella",
"items": [ "items": [
{ {
"type": "single", "type": "single",
"id": "cocorocosmetic", "id": "capella",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"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
},
{
"name": "hiirogekka",
"items": [
{
"type": "single",
"id": "hiirogekka",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "manicjeer",
"items": [
{
"type": "single",
"id": "manicjeer",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "crimsonthrone",
"items": [
{
"type": "single",
"id": "crimsonthrone",
"is_available": true "is_available": true
}, },
{ {

View File

@@ -33,7 +33,8 @@ ban_flag text,
next_fragstam_ts int, next_fragstam_ts int,
max_stamina_ts int, max_stamina_ts int,
stamina int, stamina int,
world_mode_locked_end_ts int world_mode_locked_end_ts int,
beyond_boost_gauge real default 0
); );
create table if not exists login(access_token text, create table if not exists login(access_token text,
user_id int, user_id int,
@@ -182,7 +183,8 @@ course_score int,
course_clear_type int, course_clear_type int,
stamina_multiply int, stamina_multiply int,
fragment_multiply int, fragment_multiply int,
prog_boost_multiply int prog_boost_multiply int,
beyond_boost_gauge_usage int
); );
create table if not exists item(item_id text, create table if not exists item(item_id text,
type text, type text,

View File

@@ -15,6 +15,8 @@ class Config:
-------------------------------------------------- --------------------------------------------------
''' '''
DEBUG = False
TIME_LIMIT = 3600000 TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000 COMMAND_INTERVAL = 1000000

View File

@@ -1,93 +1,18 @@
import base64 import binascii
import logging import logging
import random
import socketserver import socketserver
import threading import threading
import time
from os import urandom
# import binascii
from .aes import decrypt, encrypt from .aes import decrypt, encrypt
from .config import Config from .config import Config
from .udp_class import Player, Room, bi from .store import Store, TCPRouter, clear_player, clear_room
from .udp_class import bi
from .udp_parser import CommandParser from .udp_parser import CommandParser
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
link_play_data = {}
room_id_dict = {} # 'room_id': Room
room_code_dict = {} # 'room_code': Room
player_dict = {} # 'player_id' : Player
clean_timer = 0
lock = threading.RLock()
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s', logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
level=logging.INFO) level=logging.INFO)
def random_room_code():
# 随机生成房间号
re = ''
for _ in range(4):
re += chr(random.randint(65, 90))
for _ in range(2):
re += str(random.randint(0, 9))
return re
def unique_random(dataset, length=8, random_func=None):
'''无重复随机且默认非0'''
if random_func is None:
x = bi(urandom(length))
while x in dataset or x == 0:
x = bi(urandom(length))
else:
x = random_func()
while x in dataset:
x = random_func()
return x
def clear_player(token):
# 清除玩家信息和token
del player_dict[link_play_data[token]['player_id']]
del link_play_data[token]
def clear_room(room):
# 清除房间信息
room_id = room.room_id
room_code = room.room_code
del room_id_dict[room_id]
del room_code_dict[room_code]
del room
def memory_clean(now):
# 内存清理
with lock:
clean_room_list = []
clean_player_list = []
for token in link_play_data:
room = link_play_data[token]['room']
if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id)
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token)
for room_id in room_id_dict:
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id)
for room_id in clean_room_list:
if room_id in room_id_dict:
clear_room(room_id_dict[room_id])
for token in clean_player_list:
clear_player(token)
class UDP_handler(socketserver.BaseRequestHandler): class UDP_handler(socketserver.BaseRequestHandler):
def handle(self): def handle(self):
client_msg, server = self.request client_msg, server = self.request
@@ -96,32 +21,34 @@ class UDP_handler(socketserver.BaseRequestHandler):
iv = client_msg[8:20] iv = client_msg[8:20]
tag = client_msg[20:32] tag = client_msg[20:32]
ciphertext = client_msg[32:] ciphertext = client_msg[32:]
if int.from_bytes(token, byteorder='little') in link_play_data: if bi(token) not in Store.link_play_data:
user = link_play_data[bi(token)]
else:
return None return None
user = Store.link_play_data[bi(token)]
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag) plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
return None return None
# print(binascii.b2a_hex(plaintext))
# if Config.DEBUG:
# logging.info(
# f'UDP-From-{self.client_address[0]}-{binascii.b2a_hex(plaintext)}')
commands = CommandParser( commands = CommandParser(
user['room'], user['player_index']).get_commands(plaintext) user['room'], user['player_index']).get_commands(plaintext)
if user['room'].players[user['player_index']].player_id == 0: if user['room'].players[user['player_index']].player_id == 0:
clear_player(bi(token)) clear_player(bi(token))
temp = [] if user['room'].player_num == 0:
for i in commands: clear_room(user['room'])
if i[:3] == b'\x06\x16\x12': commands = [i for i in commands if i[2] == 0x12]
temp.append(i)
commands = temp
# 处理不能正确被踢的问题 # 处理不能正确被踢的问题
for i in commands: for i in commands:
iv, ciphertext, tag = encrypt(user['key'], i, b'') iv, ciphertext, tag = encrypt(user['key'], i, b'')
# print(binascii.b2a_hex(i)) # if Config.DEBUG:
# logging.info(
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
server.sendto(token + iv + tag[:12] + server.sendto(token + iv + tag[:12] +
ciphertext, self.client_address) ciphertext, self.client_address)
@@ -132,126 +59,18 @@ class TCP_handler(socketserver.StreamRequestHandler):
self.data = self.rfile.readline().strip() self.data = self.rfile.readline().strip()
message = self.data.decode('utf-8') message = self.data.decode('utf-8')
# print(message) if Config.DEBUG:
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
data = message.split('|') data = message.split('|')
if data[0] != Config.AUTHENTICATION: if data[0] != Config.AUTHENTICATION:
self.wfile.write(b'No authentication') self.wfile.write(b'No authentication')
logging.warning('TCP-%s-No authentication' % logging.warning(f'TCP-{self.client_address[0]}-No authentication')
self.client_address[0])
return None return None
global clean_timer r = TCPRouter(data[1:]).handle()
now = round(time.time() * 1000) if Config.DEBUG:
if now - clean_timer >= Config.TIME_LIMIT: logging.info(f'TCP-To-{self.client_address[0]}-{r}')
logging.info('Start cleaning memory...') self.wfile.write(r.encode('utf-8'))
clean_timer = now
memory_clean(now)
self.wfile.write(data_swap(data[1:]).encode('utf-8'))
def data_swap(data: list) -> str:
# data: list[str] = [command, ...]
if data[0] == '1':
# 开房
# data = ['1', name, song_unlock, ]
# song_unlock: base64 str
name = data[1]
song_unlock = base64.b64decode(data[2])
key = urandom(16)
with lock:
room_id = unique_random(room_id_dict)
room = Room()
room.room_id = room_id
room_id_dict[room_id] = room
player_id = unique_random(player_dict, 3)
player = Player()
player.player_id = player_id
player.set_player_name(name)
player_dict[player_id] = player
player.song_unlock = song_unlock
room.song_unlock = song_unlock
room.host_id = player_id
room.players[0] = player
room.player_num = 1
room_code = unique_random(
room_code_dict, random_func=random_room_code)
room.room_code = room_code
room_code_dict[room_code] = room
token = room_id
player.token = token
link_play_data[token] = {'key': key,
'room': room,
'player_index': 0,
'player_id': player_id}
logging.info('TCP-Create room `%s` by player `%s`' % (room_code, name))
return '|'.join([str(x) for x in (0, room_code, room_id, token, base64.b64encode(key).decode('utf-8'), player_id)])
elif data[0] == '2':
# 入房
# data = ['2', name, song_unlock, room_code]
# song_unlock: base64 str
room_code = data[3].upper()
with lock:
if room_code not in room_code_dict:
# 房间号错误
return '1202'
room = room_code_dict[room_code]
if room.player_num == 4:
# 满人
return '1201'
elif room.state != 2:
# 无法加入
return '1205'
name = data[1]
song_unlock = base64.b64decode(data[2])
key = urandom(16)
with lock:
token = unique_random(link_play_data)
player_id = unique_random(player_dict, 3)
player = Player()
player.player_id = player_id
player.set_player_name(name)
player.token = token
player_dict[player_id] = player
player.song_unlock = song_unlock
room.update_song_unlock()
for i in range(4):
if room.players[i].player_id == 0:
room.players[i] = player
player_index = i
break
link_play_data[token] = {'key': key,
'room': room,
'player_index': player_index,
'player_id': player_id}
logging.info('TCP-Player `%s` joins room `%s`' % (name, room_code))
return '|'.join([str(x) for x in (0, room_code, room.room_id, token, base64.b64encode(key).decode('utf-8'), player_id, base64.b64encode(room.song_unlock).decode('utf-8'))])
elif data[0] == '3':
# 房间信息更新
# data = ['3', token]
token = int(data[1])
with lock:
if token in link_play_data:
r = link_play_data[token]
logging.info('TCP-Room `%s` info update' % room_code)
return '|'.join([str(x) for x in (0, r['room'].room_code, r['room'].room_id, base64.b64encode(r['key']).decode('utf-8'), r['room'].players[r['player_index']].player_id, base64.b64encode(r['room'].song_unlock).decode('utf-8'))])
else:
return '108'
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT): def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):

View File

@@ -0,0 +1,231 @@
import logging
from base64 import b64decode, b64encode
from os import urandom
from random import randint
from threading import RLock
from time import time
from .config import Config
from .udp_class import Player, Room, bi
class Store:
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
link_play_data = {}
room_id_dict = {} # 'room_id': Room
room_code_dict = {} # 'room_code': Room
player_dict = {} # 'player_id' : Player
lock = RLock()
def random_room_code():
re = ''
for _ in range(4):
re += chr(randint(65, 90))
for _ in range(2):
re += str(randint(0, 9))
return re
def unique_random(dataset, length=8, random_func=None):
'''无重复随机且默认非0没处理可能的死循环'''
if random_func is None:
x = bi(urandom(length))
while x in dataset or x == 0:
x = bi(urandom(length))
else:
x = random_func()
while x in dataset:
x = random_func()
return x
def clear_player(token):
# 清除玩家信息和token
del Store.player_dict[Store.link_play_data[token]['player_id']]
del Store.link_play_data[token]
def clear_room(room):
# 清除房间信息
room_id = room.room_id
room_code = room.room_code
del Store.room_id_dict[room_id]
del Store.room_code_dict[room_code]
del room
def memory_clean(now):
# 内存清理,应对玩家不正常退出
with Store.lock:
clean_room_list = []
clean_player_list = []
for token in Store.link_play_data:
room = Store.link_play_data[token]['room']
if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id)
if now - room.players[Store.link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token)
for room_id in Store.room_id_dict:
if now - Store.room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id)
for room_id in clean_room_list:
if room_id in Store.room_id_dict:
clear_room(Store.room_id_dict[room_id])
for token in clean_player_list:
clear_player(token)
class TCPRouter:
clean_timer = 0
router = {
'0': 'debug',
'1': 'create_room',
'2': 'join_room',
'3': 'update_room',
}
def __init__(self, data: list):
self.data = data # data: list[str] = [command, ...]
def debug(self):
if Config.DEBUG:
return eval(self.data[1])
return 'ok'
@staticmethod
def clean_check():
now = round(time() * 1000)
if now - TCPRouter.clean_timer >= Config.TIME_LIMIT:
logging.info('Start cleaning memory...')
TCPRouter.clean_timer = now
memory_clean(now)
def handle(self) -> str:
self.clean_check()
if self.data[0] not in self.router:
return None
r = getattr(self, self.router[self.data[0]])()
if isinstance(r, tuple):
return '|'.join(map(str, r))
return str(r)
@staticmethod
def generate_player(name: str) -> Player:
player_id = unique_random(Store.player_dict, 3)
player = Player()
player.player_id = player_id
player.set_player_name(name)
Store.player_dict[player_id] = player
return player
@staticmethod
def generate_room() -> Room:
room_id = unique_random(Store.room_id_dict)
room = Room()
room.room_id = room_id
room.timestamp = round(time() * 1000)
Store.room_id_dict[room_id] = room
room_code = unique_random(
Store.room_code_dict, random_func=random_room_code)
room.room_code = room_code
Store.room_code_dict[room_code] = room
return room
def create_room(self) -> tuple:
# 开房
# data = ['1', name, song_unlock, ]
# song_unlock: base64 str
name = self.data[1]
song_unlock = b64decode(self.data[2])
key = urandom(16)
with Store.lock:
room = self.generate_room()
player = self.generate_player(name)
player.song_unlock = song_unlock
room.song_unlock = song_unlock
room.host_id = player.player_id
room.players[0] = player
token = room.room_id
player.token = token
Store.link_play_data[token] = {
'key': key,
'room': room,
'player_index': 0,
'player_id': player.player_id
}
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`')
return (0, room.room_code, room.room_id, token, b64encode(key).decode('utf-8'), player.player_id)
def join_room(self) -> tuple:
# 入房
# data = ['2', name, song_unlock, room_code]
# song_unlock: base64 str
room_code = self.data[3].upper()
key = urandom(16)
name = self.data[1]
song_unlock = b64decode(self.data[2])
with Store.lock:
if room_code not in Store.room_code_dict:
# 房间号错误 / 房间不存在
return '1202'
room: Room = Store.room_code_dict[room_code]
player_num = room.player_num
if player_num == 4:
# 满人
return '1201'
elif player_num == 0:
# 房间不存在
return '1202'
elif room.state != 2:
# 无法加入
return '1205'
token = unique_random(Store.link_play_data)
player = self.generate_player(name)
player.token = token
player.song_unlock = song_unlock
room.update_song_unlock()
for i in range(4):
if room.players[i].player_id == 0:
room.players[i] = player
player_index = i
break
Store.link_play_data[token] = {
'key': key,
'room': room,
'player_index': player_index,
'player_id': player.player_id
}
logging.info(f'TCP-Player `{name}` joins room `{room_code}`')
return (0, room_code, room.room_id, token, b64encode(key).decode('utf-8'), player.player_id, b64encode(room.song_unlock).decode('utf-8'))
def update_room(self) -> tuple:
# 房间信息更新
# data = ['3', token]
token = int(self.data[1])
with Store.lock:
if token not in Store.link_play_data:
return '108'
r = Store.link_play_data[token]
room = r['room']
logging.info(f'TCP-Room `{room.room_code}` info update')
return (0, room.room_code, room.room_id, b64encode(r['key']).decode('utf-8'), room.players[r['player_index']].player_id, b64encode(room.song_unlock).decode('utf-8'))

View File

@@ -1,3 +1,5 @@
from time import time
from .config import Config from .config import Config
@@ -61,11 +63,10 @@ class Room:
self.song_idx = 0xffff self.song_idx = 0xffff
self.last_song_idx = 0xffff self.last_song_idx = 0xffff
self.song_unlock = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
self.host_id = 0 self.host_id = 0
self.players = [Player(), Player(), Player(), Player()] self.players = [Player(), Player(), Player(), Player()]
self.player_num = 0
self.interval = 1000 self.interval = 1000
self.times = 100 self.times = 100
@@ -73,7 +74,33 @@ class Room:
self.round_switch = 0 self.round_switch = 0
self.command_queue = [] self.command_queue = []
self.command_queue_length = 0
@property
def command_queue_length(self) -> int:
return len(self.command_queue)
@property
def player_num(self) -> int:
self.check_player_online()
return sum(i.player_id != 0 for i in self.players)
def check_player_online(self, now: int = None):
# 检测玩家是否被自动踢出房间 / 离线判断
now = round(time() * 1000000) if now is None else now
flag = False
player_index_list = []
for i, x in enumerate(self.players):
if x.player_id == 0 or x.last_timestamp == 0:
continue
if now - x.last_timestamp >= Config.PLAYER_TIMEOUT:
self.delete_player(i)
flag = True
player_index_list.append(i)
elif x.online == 1 and now - x.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
x.online = 0
player_index_list.append(i)
return flag, player_index_list
def get_players_info(self): def get_players_info(self):
# 获取所有玩家信息 # 获取所有玩家信息
@@ -113,7 +140,6 @@ class Room:
def delete_player(self, player_index: int): def delete_player(self, player_index: int):
# 删除某个玩家 # 删除某个玩家
self.player_num -= 1
if self.players[player_index].player_id == self.host_id: if self.players[player_index].player_id == self.host_id:
self.make_round() self.make_round()

View File

@@ -6,31 +6,23 @@ from .udp_sender import CommandSender
class CommandParser: class CommandParser:
route = [None, 'command_01', 'command_02', 'command_03', 'command_04', 'command_05',
'command_06', 'command_07', 'command_08', 'command_09', 'command_0a', 'command_0b']
def __init__(self, room: Room, player_index: int = 0) -> None: def __init__(self, room: Room, player_index: int = 0) -> None:
self.room = room self.room = room
self.player_index = player_index self.player_index = player_index
self.s = CommandSender(self.room)
def get_commands(self, command): def get_commands(self, command):
self.command = command self.command = command
l = {b'\x06\x16\x01': self.command_01, r = getattr(self, self.route[self.command[2]])()
b'\x06\x16\x02': self.command_02,
b'\x06\x16\x03': self.command_03,
b'\x06\x16\x04': self.command_04,
b'\x06\x16\x05': self.command_05,
b'\x06\x16\x06': self.command_06,
b'\x06\x16\x07': self.command_07,
b'\x06\x16\x08': self.command_08,
b'\x06\x16\x09': self.command_09,
b'\x06\x16\x0a': self.command_0a,
b'\x06\x16\x0b': self.command_0b
}
r = l[command[:3]]()
re = [] re = []
flag_13 = False flag_13 = False
for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length): for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length):
if self.room.command_queue[i][:3] == b'\x06\x16\x13': if self.room.command_queue[i][2] == 0x13:
if flag_13: if flag_13:
break break
flag_13 = True flag_13 = True
@@ -52,16 +44,13 @@ class CommandParser:
if i.player_id == player_id and i.online == 1: if i.player_id == player_id and i.online == 1:
self.room.host_id = player_id self.room.host_id = player_id
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24] self.room.command_queue.append(self.s.command_10())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_10())
return None return None
def command_02(self): def command_02(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
song_idx = bi(self.command[24:26]) song_idx = bi(self.command[24:26])
flag = 2 flag = 2
@@ -69,17 +58,14 @@ class CommandParser:
flag = 0 flag = 0
self.room.state = 3 self.room.state = 3
self.room.song_idx = song_idx self.room.song_idx = song_idx
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_11())
self.room.command_queue.append(x.command_11()) self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_13())
return [x.command_0d(flag)] return [self.s.command_0d(flag)]
def command_03(self): def command_03(self):
# 尝试进入结算 # 尝试进入结算
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player = self.room.players[self.player_index] player = self.room.players[self.player_index]
player.score = bi(self.command[24:28]) player.score = bi(self.command[24:28])
player.cleartype = self.command[28] player.cleartype = self.command[28]
@@ -89,20 +75,17 @@ class CommandParser:
player.last_timestamp -= Config.COMMAND_INTERVAL player.last_timestamp -= Config.COMMAND_INTERVAL
self.room.last_song_idx = self.room.song_idx self.room.last_song_idx = self.room.song_idx
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_12(self.player_index))
self.room.command_queue.append(x.command_12(self.player_index))
if self.room.is_finish(): if self.room.is_finish():
self.room.make_finish() self.room.make_finish()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return None return None
def command_04(self): def command_04(self):
# 踢人 # 踢人
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player_id = bi(self.command[24:32]) player_id = bi(self.command[24:32])
flag = 2 flag = 2
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id: if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
@@ -110,51 +93,42 @@ class CommandParser:
if self.room.players[i].player_id == player_id: if self.room.players[i].player_id == player_id:
flag = 1 flag = 1
self.room.delete_player(i) self.room.delete_player(i)
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_12(i))
self.room.command_queue.append(x.command_12(i))
self.room.update_song_unlock() self.room.update_song_unlock()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_14())
break break
return [x.command_0d(flag)] return [self.s.command_0d(flag)]
def command_05(self): def command_05(self):
pass pass
def command_06(self): def command_06(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return None return None
def command_07(self): def command_07(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
self.room.players[self.player_index].song_unlock = self.command[24:536] self.room.players[self.player_index].song_unlock = self.command[24:536]
self.room.update_song_unlock() self.room.update_song_unlock()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_14())
return None return None
def command_08(self): def command_08(self):
self.room.round_switch = bi(self.command[24:25]) self.room.round_switch = bi(self.command[24:25])
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24] self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_13())
return None return None
def command_09(self): def command_09(self):
re = [] re = []
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player = self.room.players[self.player_index] player = self.room.players[self.player_index]
if bi(self.command[12:16]) == 0: if bi(self.command[12:16]) == 0:
@@ -162,29 +136,17 @@ class CommandParser:
self.room.state = 1 self.room.state = 1
self.room.update_song_unlock() self.room.update_song_unlock()
player.start_command_num = self.room.command_queue_length player.start_command_num = self.room.command_queue_length
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_15())
self.room.command_queue.append(x.command_15())
else: else:
if x.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL: if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
re.append(x.command_0c()) re.append(self.s.command_0c())
player.last_timestamp = x.timestamp player.last_timestamp = self.s.timestamp
flag_13 = False
# 离线判断 # 离线判断
for i in range(4): flag_13, player_index_list = self.room.check_player_online(
if i != self.player_index: self.s.timestamp)
t = self.room.players[i] for i in player_index_list:
if t.player_id != 0: self.room.command_queue.append(self.s.command_12(i))
if t.last_timestamp != 0:
if t.online == 1 and x.timestamp - t.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
t.online = 0
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(i))
elif t.online == 0 and x.timestamp - t.last_timestamp >= Config.PLAYER_TIMEOUT:
self.room.delete_player(i)
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(i))
flag_13 = True
flag_11 = False flag_11 = False
flag_12 = False flag_12 = False
@@ -276,7 +238,7 @@ class CommandParser:
if player.timer != 0 or self.room.state != 8: if player.timer != 0 or self.room.state != 8:
for i in self.room.players: for i in self.room.players:
i.extra_command_queue.append( i.extra_command_queue.append(
x.command_0e(self.player_index)) self.s.command_0e(self.player_index))
if self.room.is_ready(8, 1): if self.room.is_ready(8, 1):
flag_13 = True flag_13 = True
@@ -293,14 +255,12 @@ class CommandParser:
flag_13 = True flag_13 = True
if flag_11: if flag_11:
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_11())
self.room.command_queue.append(x.command_11())
if flag_12: if flag_12:
self.room.command_queue_length += 1 self.room.command_queue.append(
self.room.command_queue.append(x.command_12(self.player_index)) self.s.command_12(self.player_index))
if flag_13: if flag_13:
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return re return re
@@ -308,28 +268,22 @@ class CommandParser:
# 退出房间 # 退出房间
self.room.delete_player(self.player_index) self.room.delete_player(self.player_index)
x = CommandSender(self.room) self.room.command_queue.append(self.s.command_12(self.player_index))
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(self.player_index))
if self.room.state == 3 or self.room.state == 2: if self.room.state == 3 or self.room.state == 2:
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
# self.room.command_queue_length += 1 # self.room.command_queue.append(self.s.command_11())
# self.room.command_queue.append(x.command_11()) self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_14())
return None return None
def command_0b(self): def command_0b(self):
# 推荐歌曲 # 推荐歌曲
song_idx = bi(self.command[16:18]) song_idx = bi(self.command[16:18])
x = CommandSender(self.room)
for i in range(4): for i in range(4):
if self.player_index != i and self.room.players[i].online == 1: if self.player_index != i and self.room.players[i].online == 1:
self.room.players[i].extra_command_queue.append( self.room.players[i].extra_command_queue.append(
x.command_0f(self.player_index, song_idx)) self.s.command_0f(self.player_index, song_idx))
return None return None

View File

@@ -1,46 +1,64 @@
import time from time import time
from .udp_class import Room, b from .udp_class import Room, b
class CommandSender: class CommandSender:
def __init__(self, room: Room = Room()) -> None:
PROTOCOL_NAME = b'\x06\x16'
PROTOCOL_VERSION = b'\x09'
def __init__(self, room: Room = None) -> None:
self.room = room self.room = room
self.timestamp = round(time.time() * 1000000) self.timestamp = round(time() * 1000000)
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00' self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
@staticmethod
def command_encode(t: tuple):
r = b''.join(t)
x = 16 - len(r) % 16
return r + b(x) * x
def command_prefix(self, command: bytes):
length = self.room.command_queue_length
if command >= b'\x10':
length += 1
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
def command_0c(self): def command_0c(self):
return b'\x06\x16\x0c\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b'\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b' return self.command_encode((*self.command_prefix(b'\x0c'), self.random_code, b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8)))
def command_0d(self, code: int): def command_0d(self, code: int):
return b'\x06\x16\x0d\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(code) + b'\x07\x07\x07\x07\x07\x07\x07' return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
def command_0e(self, player_index: int): def command_0e(self, player_index: int):
# 分数广播 # 分数广播
player = self.room.players[player_index] player = self.room.players[player_index]
return b'\x06\x16\x0e\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + b(player.player_id, 8) + b(player.character_id) + b(player.is_uncapped) + b(player.difficulty) + b(player.score, 4) + b(player.timer, 4) + b(player.cleartype) + b(player.player_state) + b(player.download_percent) + b'\x01' + b(player.last_score, 4) + b(player.last_timer, 4) + b(player.online) return self.command_encode((*self.command_prefix(b'\x0e'), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b'\x01', b(player.last_score, 4), b(player.last_timer, 4), b(player.online)))
def command_0f(self, player_index: int, song_idx: int): def command_0f(self, player_index: int, song_idx: int):
# 歌曲推荐 # 歌曲推荐
player = self.room.players[player_index] player = self.room.players[player_index]
return b'\x06\x16\x0f\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + b(player.player_id, 8) + b(song_idx, 2) + b'\x06\x06\x06\x06\x06\x06' return self.command_encode((*self.command_prefix(b'\x0f'), b(player.player_id, 8), b(song_idx, 2)))
def command_10(self): def command_10(self):
# 房主宣告 # 房主宣告
return b'\x06\x16\x10\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.host_id, 8) return self.command_encode((*self.command_prefix(b'\x10'), self.random_code, b(self.room.host_id, 8)))
def command_11(self): def command_11(self):
return b'\x06\x16\x11\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + self.room.get_players_info() + b'\x08\x08\x08\x08\x08\x08\x08\x08' return self.command_encode((*self.command_prefix(b'\x11'), self.random_code, self.room.get_players_info()))
def command_12(self, player_index: int): def command_12(self, player_index: int):
player = self.room.players[player_index] player = self.room.players[player_index]
return b'\x06\x16\x12\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(player_index) + b(player.player_id, 8) + b(player.character_id) + b(player.is_uncapped) + b(player.difficulty) + b(player.score, 4) + b(player.timer, 4) + b(player.cleartype) + b(player.player_state) + b(player.download_percent) + b(player.online) return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b(player.online)))
def command_13(self): def command_13(self):
return b'\x06\x16\x13\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.host_id, 8) + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b(self.room.song_idx, 2) + b(self.room.interval, 2) + b(self.room.times, 7) + self.room.get_player_last_score() + b(self.room.last_song_idx, 2) + b(self.room.round_switch, 1) + b'\x01' return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
def command_14(self): def command_14(self):
return b'\x06\x16\x14\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + self.room.song_unlock + b'\x08\x08\x08\x08\x08\x08\x08\x08' return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
def command_15(self): def command_15(self):
return b'\x06\x16\x15\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.room.get_players_info() + self.room.song_unlock + b(self.room.host_id, 8) + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b(self.room.song_idx, 2) + b(self.room.interval, 2) + b(self.room.times, 7) + self.room.get_player_last_score() + b(self.room.last_song_idx, 2) + b(self.room.round_switch, 1) + b'\x09\x09\x09\x09\x09\x09\x09\x09\x09' return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))

View File

@@ -27,7 +27,7 @@ import server
import web.index import web.index
import web.login import web.login
from core.constant import Constant from core.constant import Constant
from core.download import (UserDownload, initialize_songfile) from core.download import UserDownload
from core.error import ArcError, NoAccess, RateLimit from core.error import ArcError, NoAccess, RateLimit
from core.init import FileChecker from core.init import FileChecker
from core.sql import Connect from core.sql import Connect
@@ -105,6 +105,12 @@ if Config.DEPLOY_MODE == 'waitress':
f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}') f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}')
return response return response
# @app.before_request
# def before_request():
# print(request.path)
# print(request.headers)
# print(request.data)
def tcp_server_run(): def tcp_server_run():
if Config.DEPLOY_MODE == 'gevent': if Config.DEPLOY_MODE == 'gevent':
@@ -128,6 +134,18 @@ def tcp_server_run():
app.run(Config.HOST, Config.PORT) app.run(Config.HOST, Config.PORT)
def generate_log_file_dict(level: str, filename: str) -> dict:
return {
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": level,
"formatter": "default",
"filename": filename
}
def main(): def main():
log_dict = { log_dict = {
'version': 1, 'version': 1,
@@ -141,15 +159,7 @@ def main():
'stream': 'ext://flask.logging.wsgi_errors_stream', 'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default' 'formatter': 'default'
}, },
"error_file": { "error_file": generate_log_file_dict('ERROR', './log/error.log')
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": "ERROR",
"formatter": "default",
"filename": "./log/error.log"
}
}, },
'formatters': { 'formatters': {
'default': { 'default': {
@@ -159,50 +169,30 @@ def main():
} }
if Config.ALLOW_INFO_LOG: if Config.ALLOW_INFO_LOG:
log_dict['root']['handlers'].append('info_file') log_dict['root']['handlers'].append('info_file')
log_dict['handlers']['info_file'] = { log_dict['handlers']['info_file'] = generate_log_file_dict(
"class": "logging.handlers.RotatingFileHandler", 'INFO', './log/info.log')
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": "INFO",
"formatter": "default",
"filename": "./log/info.log"
}
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
log_dict['root']['handlers'].append('warning_file') log_dict['root']['handlers'].append('warning_file')
log_dict['handlers']['warning_file'] = { log_dict['handlers']['warning_file'] = generate_log_file_dict(
"class": "logging.handlers.RotatingFileHandler", 'WARNING', './log/warning.log')
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": "WARNING",
"formatter": "default",
"filename": "./log/warning.log"
}
dictConfig(log_dict) dictConfig(log_dict)
if not FileChecker(app).check_before_run(): Connect.logger = app.logger
app.logger.error('Something wrong. The server will not run.') if not FileChecker(app.logger).check_before_run():
app.logger.error('Some errors occurred. The server will not run.')
input('Press ENTER key to exit.') input('Press ENTER key to exit.')
sys.exit() sys.exit()
app.logger.info("Start to initialize song data...")
try:
initialize_songfile()
app.logger.info('Complete!')
except:
app.logger.warning('Initialization error!')
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS: if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
from linkplay_server import link_play from linkplay_server import link_play
process = [Process(target=link_play, args=( process = [Process(target=link_play, args=(
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))] Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
[p.start() for p in process] [p.start() for p in process]
app.logger.info("Link Play UDP server is running on " + app.logger.info(
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_UDP_PORT) + " ...") f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...")
app.logger.info("Link Play TCP server is running on " + app.logger.info(
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_TCP_PORT) + " ...") f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...")
tcp_server_run() tcp_server_run()
[p.join() for p in process] [p.join() for p in process]
else: else:

View File

@@ -1,3 +1,4 @@
cd /d %~dp0 cd /d %~dp0
:: Develop server :: Develop server
python -B main.py python -B main.py
pause

View File

@@ -1,56 +0,0 @@
from core.sql import Connect
def calculate_rating(defnum, score):
# 计算rating
if score >= 10000000:
ptt = defnum + 2
elif score < 9800000:
ptt = defnum + (score-9500000) / 300000
if ptt < 0 and defnum != -10:
ptt = 0
else:
ptt = defnum + 1 + (score-9800000) / 200000
return ptt
def refresh_all_score_rating():
# 刷新所有best成绩的rating
error = 'Unknown error.'
with Connect() as c:
c.execute(
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
x = c.fetchall()
if x:
song_list = [i[0] for i in x]
with Connect() as c:
c.execute('''update best_score set rating=0 where song_id not in ({0})'''.format(
','.join(['?']*len(song_list))), tuple(song_list))
for i in x:
for j in range(0, 4):
defnum = -10 # 没在库里的全部当做定数-10
if i is not None:
defnum = float(i[j+1]) / 10
if defnum <= 0:
defnum = -10 # 缺少难度的当做定数-10
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
'a': i[0], 'b': j})
y = c.fetchall()
if y:
for k in y:
ptt = calculate_rating(defnum, k[1])
if ptt < 0:
ptt = 0
c.execute('''update best_score set rating=:a where user_id=:b and song_id=:c and difficulty=:d''', {
'a': ptt, 'b': k[0], 'c': i[0], 'd': j})
error = None
else:
error = 'No song data.'
return error

View File

@@ -1,13 +1,12 @@
import base64 import base64
from functools import wraps from functools import wraps
from core.config_manager import Config
from core.error import ArcError, NoAccess from core.error import ArcError, NoAccess
from core.sql import Connect from core.sql import Connect
from core.user import UserAuth, UserLogin from core.user import UserAuth, UserLogin
from flask import Blueprint, jsonify, request from flask import Blueprint, g, jsonify, request, current_app
from .func import arc_try, error_return from .func import arc_try, error_return, header_check
bp = Blueprint('auth', __name__, url_prefix='/auth') bp = Blueprint('auth', __name__, url_prefix='/auth')
@@ -16,10 +15,9 @@ bp = Blueprint('auth', __name__, url_prefix='/auth')
@arc_try @arc_try
def login(): def login():
headers = request.headers headers = request.headers
if 'AppVersion' in headers: # 版本检查 e = header_check(request)
if Config.ALLOW_APPVERSION: if e is not None:
if headers['AppVersion'] not in Config.ALLOW_APPVERSION: raise e
raise NoAccess('Wrong app version.', 1203)
request.form['grant_type'] request.form['grant_type']
with Connect() as c: with Connect() as c:
@@ -45,16 +43,21 @@ def auth_required(request):
headers = request.headers headers = request.headers
if 'AppVersion' in headers: # 版本检查 e = header_check(request)
if Config.ALLOW_APPVERSION: if e is not None:
if headers['AppVersion'] not in Config.ALLOW_APPVERSION: current_app.logger.warning(
return error_return(NoAccess('Wrong app version.', 1203)) f' - {e.error_code}|{e.api_error_code}: {e}')
return error_return(e)
with Connect() as c: with Connect() as c:
try: try:
user = UserAuth(c) 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() user_id = user.token_get_id()
g.user = user
except ArcError as e: except ArcError as e:
return error_return(e) return error_return(e)
return view(user_id, *args, **kwargs) return view(user_id, *args, **kwargs)

View File

@@ -19,7 +19,7 @@ def course_me(user_id):
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
core = ItemCore(c) core = ItemCore(c)
core.item_id = 'core_course_skip_purchase' core.item_id = 'core_course_skip_purchase'
core.select(user) core.select_user_item(user)
x = UserCourseList(c, user) x = UserCourseList(c, user)
x.select_all() x.select_all()
return success_return({ return success_return({

View File

@@ -1,9 +1,17 @@
from functools import wraps from functools import wraps
from traceback import format_exc from traceback import format_exc
from flask import current_app, g, jsonify
from core.config_manager import Config from core.config_manager import Config
from core.error import ArcError from core.error import ArcError, NoAccess
from flask import current_app, jsonify
has_arc_hash = False
try:
from core.arc_crypto import ArcHashChecker # type: ignore
has_arc_hash = True
except ModuleNotFoundError:
pass
default_error = ArcError('Unknown Error', status=500) default_error = ArcError('Unknown Error', status=500)
@@ -83,6 +91,22 @@ def arc_try(view):
except ArcError as e: except ArcError as e:
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
current_app.logger.warning(format_exc()) current_app.logger.warning(format_exc())
user = g.get("user", None)
current_app.logger.warning(
f'{user.user_id if user is not None else ""} - {e.error_code}|{e.api_error_code}: {e}')
return error_return(e) return error_return(e)
return wrapped_view return wrapped_view
def header_check(request) -> ArcError:
'''检查请求头是否合法'''
headers = request.headers
if Config.ALLOW_APPVERSION: # 版本检查
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return NoAccess('Invalid app version', 1203)
if has_arc_hash and not ArcHashChecker(request).check():
return NoAccess('Invalid request')
return None

View File

@@ -1,91 +0,0 @@
import os
from shutil import copy, copy2
from core.sql import Connect
from database.database_initialize import ARCAEA_SERVER_VERSION, main
from web.system import update_database
def try_rename(path, new_path):
# 尝试重命名文件,并尝试避免命名冲突,返回最终路径
final_path = new_path
if os.path.exists(new_path):
i = 1
while os.path.exists(new_path + str(i)):
i += 1
os.rename(path, new_path + str(i))
final_path = new_path + str(i)
else:
os.rename(path, new_path)
return final_path
def check_before_run(app):
# 运行前检查关键文件,返回布尔值,其实是因为有人经常忘了
f = True
if not os.path.exists('database'):
app.logger.warning('Folder `database` is missing.')
f = False
if not os.path.exists('database/songs'):
app.logger.warning('Folder `database/songs` is missing.')
f = False
if not os.path.exists('database/arcaea_database.db'):
app.logger.warning('File `database/arcaea_database.db` is missing.')
f = False
try:
app.logger.info(
'Try to new the file `database/arcaea_database.db`.')
main('./database/')
app.logger.info(
'Success to new the file `database/arcaea_database.db`.')
f = True
except:
app.logger.warning(
'Fail to new the file `database/arcaea_database.db`.')
else:
with Connect() as c:
try:
c.execute('''select value from config where id="version"''')
x = c.fetchone()
except:
x = None
# 数据库自动更新,不强求
if not x or x[0] != ARCAEA_SERVER_VERSION:
app.logger.warning(
'Maybe the file `database/arcaea_database.db` is an old version.')
try:
app.logger.info(
'Try to update the file `database/arcaea_database.db`.')
path = try_rename('database/arcaea_database.db',
'database/arcaea_database.db.bak')
try:
copy2(path, 'database/arcaea_database.db')
except:
copy(path, 'database/arcaea_database.db')
if os.path.isfile("database/old_arcaea_database.db"):
os.remove('database/old_arcaea_database.db')
try_rename('database/arcaea_database.db',
'database/old_arcaea_database.db')
main('./database/')
update_database()
app.logger.info(
'Success to update the file `database/arcaea_database.db`.')
except:
app.logger.warning(
'Fail to update the file `database/arcaea_database.db`.')
return f

View File

@@ -1,11 +1,10 @@
from core.error import ArcError
from core.present import UserPresent, UserPresentList from core.present import UserPresent, UserPresentList
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, error_return, success_return from .func import arc_try, success_return
bp = Blueprint('present', __name__, url_prefix='/present') bp = Blueprint('present', __name__, url_prefix='/present')

View File

@@ -24,18 +24,16 @@ def score_token():
@arc_try @arc_try
def score_token_world(user_id): def score_token_world(user_id):
stamina_multiply = int( stamina_multiply = int(request.args.get('stamina_multiply', 1))
request.args['stamina_multiply']) if 'stamina_multiply' in request.args else 1 fragment_multiply = int(request.args.get('fragment_multiply', 100))
fragment_multiply = int( prog_boost_multiply = int(request.args.get('prog_boost_multiply', 0))
request.args['fragment_multiply']) if 'fragment_multiply' in request.args else 100 beyond_boost_gauge_use = int(request.args.get('beyond_boost_gauge_use', 0))
prog_boost_multiply = int(
request.args['prog_boost_multiply']) if 'prog_boost_multiply' in request.args else 0
with Connect() as c: with Connect() as c:
x = UserPlay(c, UserOnline(c, user_id)) x = UserPlay(c, UserOnline(c, user_id))
x.song.set_chart(request.args['song_id'], int( x.song.set_chart(request.args['song_id'], int(
request.args['difficulty'])) request.args['difficulty']))
x.set_play_state_for_world(stamina_multiply, x.set_play_state_for_world(
fragment_multiply, prog_boost_multiply) stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use)
return success_return({ return success_return({
"stamina": x.user.stamina.stamina, "stamina": x.user.stamina.stamina,
"max_stamina_ts": x.user.stamina.max_stamina_ts, "max_stamina_ts": x.user.stamina.max_stamina_ts,

View File

@@ -8,7 +8,7 @@ from core.user import User, UserLogin, UserOnline, UserRegister
from flask import Blueprint, request from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, header_check, success_return
bp = Blueprint('user', __name__, url_prefix='/user') bp = Blueprint('user', __name__, url_prefix='/user')
@@ -16,10 +16,10 @@ bp = Blueprint('user', __name__, url_prefix='/user')
@bp.route('', methods=['POST']) # 注册接口 @bp.route('', methods=['POST']) # 注册接口
@arc_try @arc_try
def register(): def register():
if 'AppVersion' in request.headers: # 版本检查 headers = request.headers
if Config.ALLOW_APPVERSION: error = header_check(request)
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION: if error is not None:
raise NoAccess('Wrong app version.', 1203) raise error
with Connect() as c: with Connect() as c:
new_user = UserRegister(c) new_user = UserRegister(c)

View File

@@ -1,11 +1,11 @@
import os import os
import time import time
import server.arcscore
from core.download import DownloadList, initialize_songfile
from core.init import FileChecker from core.init import FileChecker
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
from core.rank import RankList from core.rank import RankList
from core.sql import Connect from core.sql import Connect
from core.user import User
from flask import Blueprint, flash, redirect, render_template, request, url_for from flask import Blueprint, flash, redirect, render_template, request, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -290,8 +290,7 @@ def update_database():
def update_song_hash(): def update_song_hash():
# 更新数据库内谱面文件hash值 # 更新数据库内谱面文件hash值
try: try:
DownloadList.clear_all_cache() RefreshSongFileCache().run()
initialize_songfile()
flash('数据刷新成功 Success refresh data.') flash('数据刷新成功 Success refresh data.')
except: except:
flash('Something error!') flash('Something error!')
@@ -302,11 +301,8 @@ def update_song_hash():
@login_required @login_required
def update_song_rating(): def update_song_rating():
# 更新所有分数的rating # 更新所有分数的rating
error = server.arcscore.refresh_all_score_rating() RefreshAllScoreRating().run()
if error: flash('数据刷新成功 Success refresh data.')
flash(error)
else:
flash('数据刷新成功 Success refresh data.')
return render_template('web/updatedatabase.html') return render_template('web/updatedatabase.html')
@@ -426,7 +422,7 @@ def all_character():
def change_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', 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) return render_template('web/changechar.html', skill_ids=skill_ids)
@@ -610,7 +606,7 @@ def edit_user_purchase():
if 'name' not in request.form and 'user_code' not in request.form: if 'name' not in request.form and 'user_code' not in request.form:
flag = False flag = False
if method == '0': if method == '0':
web.system.unlock_all_user_item(c) UnlockUserItem().run()
else: else:
c.execute( c.execute(
'''delete from user_item where type in ('pack', 'single')''') '''delete from user_item where type in ('pack', 'single')''')
@@ -636,7 +632,9 @@ def edit_user_purchase():
user_id = user_id[0] user_id = user_id[0]
if method == '0': if method == '0':
web.system.unlock_user_item(c, user_id) x = UnlockUserItem()
x.set_params(user_id=user_id)
x.run()
else: else:
c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', { c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', {
'user_id': user_id}) 'user_id': user_id})
@@ -942,7 +940,7 @@ def update_user_save():
# 全修改 # 全修改
if 'name' not in request.form and 'user_code' not in request.form: if 'name' not in request.form and 'user_code' not in request.form:
flag = False flag = False
web.system.update_all_save(c) SaveUpdateScore().run()
flash("全部用户存档同步成功 Successfully update all users' saves.") flash("全部用户存档同步成功 Successfully update all users' saves.")
else: else:
@@ -962,7 +960,9 @@ def update_user_save():
user_id = c.fetchone() user_id = c.fetchone()
if user_id: if user_id:
user_id = user_id[0] 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.") flash("用户存档同步成功 Successfully update the user's saves.")
else: else:

View File

@@ -40,41 +40,6 @@ def update_user_char(c):
(j[0], i[0], i[1], exp, i[2], 0)) (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(): def get_all_item():
# 所有物品数据查询 # 所有物品数据查询
with Connect() as c: with Connect() as c:
@@ -132,61 +97,6 @@ def get_all_purchase():
return re 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): def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount):
# 添加一个奖励 # 添加一个奖励