mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-09 17:27:27 +08:00
Compare commits
84 Commits
v2.11.2
...
cf20a2c7cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf20a2c7cb | ||
|
|
bfb6a2ddda | ||
|
|
8180eef856 | ||
|
|
2cc69a1100 | ||
|
|
73f28f38d3 | ||
|
|
707f881bdb | ||
|
|
71b789fbaa | ||
|
|
d11a8435b3 | ||
|
|
17998ee655 | ||
|
|
39e1e52d36 | ||
|
|
52bb834a8a | ||
|
|
65f6d212fe | ||
|
|
9c27b998ef | ||
|
|
a3891a901f | ||
|
|
32b708b7de | ||
|
|
eee34bae80 | ||
|
|
029d43e3f5 | ||
|
|
05bdd42679 | ||
|
|
5b5f544a19 | ||
|
|
d05618049c | ||
|
|
2e43dfbdff | ||
|
|
7f94416189 | ||
|
|
4911511ed7 | ||
|
|
46b3b053cf | ||
|
|
6c6a13086f | ||
|
|
97b86a4381 | ||
|
|
a3bc071416 | ||
|
|
c4da67cd94 | ||
|
|
3424ad67cb | ||
|
|
ecfb360228 | ||
|
|
5a37766cb9 | ||
|
|
a23e5372fb | ||
|
|
44d7d7e490 | ||
|
|
3f5281582c | ||
|
|
a6c26dedfe | ||
|
|
6f964d7a78 | ||
|
|
f3a1a5e0a9 | ||
|
|
bd40704fa7 | ||
|
|
9c9af892bd | ||
|
|
dc1ca344e5 | ||
|
|
c52fda6f47 | ||
|
|
4c40d8e21f | ||
|
|
27567b8a5c | ||
|
|
10a816b161 | ||
|
|
c3af92f29b | ||
|
|
014531f3f1 | ||
|
|
59422f96b5 | ||
|
|
9bb7171354 | ||
|
|
c38208832b | ||
|
|
64526e3c4b | ||
|
|
dd0bc64c93 | ||
|
|
2a08c9cd14 | ||
|
|
338f6579aa | ||
|
|
9636cfcae8 | ||
|
|
5c539bdf59 | ||
|
|
efedd96908 | ||
|
|
2d498ae02b | ||
|
|
ab201c9d48 | ||
|
|
02bf565ebd | ||
|
|
a05bb4854d | ||
|
|
328be5b923 | ||
|
|
2f826e9ffc | ||
|
|
eeced74f50 | ||
|
|
8f66b90912 | ||
|
|
d65cc3bcbe | ||
|
|
e206247c09 | ||
|
|
b3bf55407f | ||
|
|
50369d1f23 | ||
|
|
9d096f480b | ||
|
|
2edf559373 | ||
|
|
746712718b | ||
|
|
de701b4d1b | ||
|
|
b6663ac0dc | ||
|
|
de1d46f96e | ||
|
|
f3c17cdde7 | ||
|
|
150686d9f8 | ||
|
|
3e93082a3c | ||
|
|
04010d9c33 | ||
|
|
1f389e4073 | ||
|
|
5788972692 | ||
|
|
f373c7b052 | ||
|
|
04c6d1a0e0 | ||
|
|
41409dee27 | ||
|
|
4527339d78 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -21,3 +21,9 @@ latest version/config.py
|
|||||||
|
|
||||||
# song data
|
# song data
|
||||||
latest version/database/songs/
|
latest version/database/songs/
|
||||||
|
latest version/database/bundle/
|
||||||
|
|
||||||
|
!latest version/database/bundle/README.md
|
||||||
|
|
||||||
|
# backup
|
||||||
|
latest version/database/backup/
|
||||||
|
|||||||
77
README.md
77
README.md
@@ -1,10 +1,10 @@
|
|||||||
# 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 服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||||
|
|
||||||
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
||||||
|
|
||||||
@@ -17,25 +17,26 @@ This procedure is mainly used for study and research, and shall not be used for
|
|||||||
|
|
||||||
## 特性 Features
|
## 特性 Features
|
||||||
|
|
||||||
:x: : 不支持 Not supported
|
:x: : 不支持 Not supported
|
||||||
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
|
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
|
||||||
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
|
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
|
||||||
:construction: : 建设中 In construction
|
:construction: : 建设中 In construction
|
||||||
|
|
||||||
- 登录、注册 Login and registration
|
- 登录、注册 Login and registration
|
||||||
- 多设备自动封号 Auto-ban of multiple devices
|
- 多设备自动封号 Auto-ban of multiple devices
|
||||||
- :warning: 多设备登录 Multi device login
|
- :warning: 多设备登录 Multi device login
|
||||||
- 登录频次限制 Login rate limit
|
- 登录频次限制 Login rate limit
|
||||||
- :x: 销号 Destroy account
|
- 注册频次限制 Register rate limit
|
||||||
|
- :warning: 销号 Destroy account
|
||||||
- 成绩上传 Score upload
|
- 成绩上传 Score upload
|
||||||
- 成绩校验 Score check
|
- 成绩校验 Score check
|
||||||
- 成绩排名 Score rank
|
- 成绩排名 Score rank
|
||||||
- 潜力值机制 Potential
|
- 潜力值机制 Potential
|
||||||
- Best 30
|
- Best 30
|
||||||
- :warning: Recent Top 10
|
- :warning: Recent Top 10
|
||||||
- :warning: 世界排名 Global rank
|
- 世界排名 Global rank
|
||||||
- 段位系统 Course system
|
- 段位系统 Course system
|
||||||
- :warning: Link Play
|
- :warning: Link Play 2.0
|
||||||
- 好友系统 Friends
|
- 好友系统 Friends
|
||||||
- :x: 好友位提升 Max friend number increase
|
- :x: 好友位提升 Max friend number increase
|
||||||
- 云端存档 Cloud save
|
- 云端存档 Cloud save
|
||||||
@@ -48,14 +49,17 @@ This procedure is mainly used for study and research, and shall not be used for
|
|||||||
- :x: 加密下载 Encrypted downloading
|
- :x: 加密下载 Encrypted downloading
|
||||||
- 下载校验 Download check
|
- 下载校验 Download check
|
||||||
- 下载频次限制 Download rate limit
|
- 下载频次限制 Download rate limit
|
||||||
|
- 内容捆绑包热更新 Content bundle hot update
|
||||||
- 购买系统 Purchase system
|
- 购买系统 Purchase system
|
||||||
- 单曲和曲包 Single & Pack
|
- 单曲和曲包 Single & Pack
|
||||||
- :x: 捆绑包 Bundle
|
- :x: 捆绑包 Pack bundle
|
||||||
- 折扣 Discount
|
- 折扣 Discount
|
||||||
- 五周年兑换券 5-th anniversary ticket
|
- 五周年兑换券 5-th anniversary ticket
|
||||||
|
- 单曲兑换券 Pick ticket
|
||||||
- :x: Extend 包自动降价 Extend pack automatic price reduction
|
- :x: Extend 包自动降价 Extend pack automatic price reduction
|
||||||
- 奖励系统 Present system
|
- 奖励系统 Present system
|
||||||
- 兑换码系统 Redeem code system
|
- 兑换码系统 Redeem code system
|
||||||
|
- 新手任务 Missions
|
||||||
- 角色系统 Character system
|
- 角色系统 Character system
|
||||||
- 数据记录 Data recording
|
- 数据记录 Data recording
|
||||||
- 用户成绩 Users' scores
|
- 用户成绩 Users' scores
|
||||||
@@ -74,7 +78,6 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
[这里 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-Konmai Academy](https://616.sb)
|
|
||||||
|
|
||||||
## 更新日志 Update log
|
## 更新日志 Update log
|
||||||
|
|
||||||
@@ -83,44 +86,69 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
||||||
> Tips: When updating, please keep the original database in case of data loss.
|
> Tips: When updating, please keep the original database in case of data loss.
|
||||||
>
|
>
|
||||||
> 其它小改动请参考各个 commit 信息
|
> 其它小改动请参考各个 commit 信息。
|
||||||
> Please refer to the commit messages for other minor changes.
|
> Please refer to the commit messages for other minor changes.
|
||||||
|
|
||||||
### Version 2.11.2
|
### Version 2.12.1
|
||||||
|
|
||||||
- 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6
|
> v2.12.0.1 ~ v2.12.0.10 for Arcaea 5.10.6 ~ 6.7.1
|
||||||
- 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**.
|
>
|
||||||
- 新增用户潜力值每日记录功能 Add support for recording users' potential each day.
|
> Here are not all bug fixes and features.
|
||||||
- 修复搭档 **光 & 对立(Reunion)** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100)
|
|
||||||
- 添加 `finale/finale_end` 接口尝试修复最终挑战无法解锁结局的问题 Add the `finale/finale_end` endpoint to try to fix the problem that the endings cannot be unlocked correctly in the finale challenge. (#110)
|
- 适用于 Arcaea 6.7.1 版本
|
||||||
- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records.
|
For Arcaea 6.7.1
|
||||||
|
- 添加一些新搭档和搭档的觉醒形态,并支持他们的技能
|
||||||
|
Add some new partners, uncap some others, and add support for their skills.
|
||||||
|
- 支持“光 (Fatalis)”的由世界模式总台阶数决定的动态搭档数值
|
||||||
|
Add support for dynamic values of "Hikari (Fatalis)", which is depended by world mode total steps.
|
||||||
|
- 支持新版本客户端的账户注册和登录接口
|
||||||
|
Adapt to new account registration and login endpoints of the new version client.
|
||||||
|
- 修复 Link Play 房间中玩家历史分数异常的问题
|
||||||
|
Fix a bug that if players do not start the first play in one room, their scores will be wrong.
|
||||||
|
- 修复因为歌曲 idx 数值过大导致服务端错误,致使 Link Play 模块无法工作的问题
|
||||||
|
Fix a bug that if song's idx is too big, the server will catch the error, which makes Link Play cannot work.
|
||||||
|
- 修复谱面难度 ETR 相关问题
|
||||||
|
Fix some bugs about the ETR difficulty.
|
||||||
|
|
||||||
## 运行环境与依赖 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 >= 2.0
|
- Flask >= 2.0
|
||||||
- Cryptography >= 3.0.0
|
- Cryptography >= 35.0.0
|
||||||
- limits >= 2.7.0
|
- limits >= 2.7.0
|
||||||
- Charles, IDA, proxy app... (optional)
|
- Charles, IDA, proxy app... (optional)
|
||||||
|
|
||||||
|
## 子项目 Sub repositories
|
||||||
|
|
||||||
|
[Arcaea-Server-Wiki](https://arcaea.lost-msth.cn/Arcaea-Server/)
|
||||||
|
: 项目文档 Project documentation
|
||||||
|
|
||||||
|
[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler)
|
||||||
|
: 用于生成和解包内容捆绑包 Used to pack or unpack content bundles
|
||||||
|
|
||||||
|
[Arcaea-Server-Frontend](https://github.com/Lost-MSth/arcaea_server_frontend)
|
||||||
|
: In building
|
||||||
|
|
||||||
|
## 旧的说明 Old wiki
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## 环境搭建 Environment construction
|
### 环境搭建 Environment construction
|
||||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA)
|
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA)
|
||||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Environment-construction)
|
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Environment-construction)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## 使用说明 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)
|
||||||
|
|
||||||
@@ -129,6 +157,7 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
歌曲数据库来自 Using song database from
|
歌曲数据库来自 Using song database from
|
||||||
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||||
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
|
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
|
||||||
|
[ArcaeaSongDatabase Fork](https://github.com/CuSO4Deposit/ArcaeaSongDatabase)
|
||||||
|
|
||||||
> 从v2.9开始不再提供歌曲数据
|
> 从v2.9开始不再提供歌曲数据
|
||||||
> Since v2.9, song data will not be provided.
|
> Since v2.9, song data will not be provided.
|
||||||
@@ -138,7 +167,7 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
## 联系方式 Contact
|
## 联系方式 Contact
|
||||||
|
|
||||||
如有必要,可以联系本人 Contact me if necessary
|
如有必要,可以联系本人 Contact me if necessary
|
||||||
邮箱 Email:th84292@foxmail.com
|
邮箱 Email:arcaea@lost-msth.cn
|
||||||
|
|
||||||
## 支持一下 Support me
|
## 支持一下 Support me
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
from . import (users, songs, token, system, items,
|
from . import (users, songs, token, system, items,
|
||||||
purchases, presents, redeems, characters)
|
purchases, presents, redeems, characters, multiplay)
|
||||||
|
|
||||||
bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||||
bp.register_blueprint(users.bp)
|
l = [users, songs, token, system, items, purchases,
|
||||||
bp.register_blueprint(songs.bp)
|
presents, redeems, characters, multiplay]
|
||||||
bp.register_blueprint(token.bp)
|
for i in l:
|
||||||
bp.register_blueprint(system.bp)
|
bp.register_blueprint(i.bp)
|
||||||
bp.register_blueprint(items.bp)
|
|
||||||
bp.register_blueprint(purchases.bp)
|
|
||||||
bp.register_blueprint(presents.bp)
|
|
||||||
bp.register_blueprint(redeems.bp)
|
|
||||||
bp.register_blueprint(characters.bp)
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ CODE_MSG = {
|
|||||||
-210: 'Username exists',
|
-210: 'Username exists',
|
||||||
-211: 'Email address exists',
|
-211: 'Email address exists',
|
||||||
-212: 'User code exists',
|
-212: 'User code exists',
|
||||||
|
-213: 'Too many register attempts',
|
||||||
-999: 'Unknown error'
|
-999: 'Unknown error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
latest version/api/multiplay.py
Normal file
21
latest version/api/multiplay.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.linkplay import RemoteMultiPlayer
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('multiplay', __name__, url_prefix='/multiplay')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/rooms', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=['offset', 'limit'])
|
||||||
|
@api_try
|
||||||
|
def rooms_get(data, user):
|
||||||
|
'''获取房间列表'''
|
||||||
|
|
||||||
|
r = RemoteMultiPlayer().get_rooms(offset=data.get(
|
||||||
|
'offset', 0), limit=data.get('limit', 100))
|
||||||
|
return success_return(r)
|
||||||
@@ -63,7 +63,7 @@ def songs_get(data, user):
|
|||||||
'''查询全歌曲信息'''
|
'''查询全歌曲信息'''
|
||||||
A = ['song_id', 'name']
|
A = ['song_id', 'name']
|
||||||
B = ['song_id', 'name', 'rating_pst',
|
B = ['song_id', 'name', 'rating_pst',
|
||||||
'rating_prs', 'rating_ftr', 'rating_byn']
|
'rating_prs', 'rating_ftr', 'rating_byn', 'rating_etr']
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
query = Query(A, A, B).from_dict(data)
|
query = Query(A, A, B).from_dict(data)
|
||||||
x = Sql(c).select('chart', query=query)
|
x = Sql(c).select('chart', query=query)
|
||||||
@@ -97,8 +97,8 @@ def songs_post(data, user):
|
|||||||
@api_try
|
@api_try
|
||||||
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
|
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
|
||||||
'''查询歌曲某个难度的成绩排行榜,和游戏内接口相似,只允许limit'''
|
'''查询歌曲某个难度的成绩排行榜,和游戏内接口相似,只允许limit'''
|
||||||
if difficulty not in [0, 1, 2, 3]:
|
if difficulty not in [0, 1, 2, 3, 4]:
|
||||||
raise InputError('Difficulty must be 0, 1, 2 or 3')
|
raise InputError('Difficulty must be 0, 1, 2, 3 or 4')
|
||||||
limit = data.get('limit', 20)
|
limit = data.get('limit', 20)
|
||||||
if not isinstance(limit, int):
|
if not isinstance(limit, int):
|
||||||
raise InputError('Limit must be int')
|
raise InputError('Limit must be int')
|
||||||
|
|||||||
@@ -81,7 +81,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'], must_change=True)
|
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email', 'custom_banner'], must_change=True)
|
||||||
@api_try
|
@api_try
|
||||||
def users_user_put(data, user, user_id):
|
def users_user_put(data, user, user_id):
|
||||||
'''修改一个用户'''
|
'''修改一个用户'''
|
||||||
@@ -93,8 +93,12 @@ def users_user_put(data, user, user_id):
|
|||||||
u.set_name(data['name'])
|
u.set_name(data['name'])
|
||||||
r['name'] = u.name
|
r['name'] = u.name
|
||||||
if 'password' in data:
|
if 'password' in data:
|
||||||
u.set_password(data['password'])
|
if data['password'] == '':
|
||||||
r['password'] = u.hash_pwd
|
u.password = ''
|
||||||
|
r['password'] = ''
|
||||||
|
else:
|
||||||
|
u.set_password(data['password'])
|
||||||
|
r['password'] = u.hash_pwd
|
||||||
if 'email' in data:
|
if 'email' in data:
|
||||||
u.set_email(data['email'])
|
u.set_email(data['email'])
|
||||||
r['email'] = u.email
|
r['email'] = u.email
|
||||||
@@ -106,6 +110,11 @@ 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 'custom_banner' in data:
|
||||||
|
if not isinstance(data['custom_banner'], str):
|
||||||
|
raise InputError('Value `custom_banner` must be str')
|
||||||
|
u.custom_banner = data['custom_banner']
|
||||||
|
r['custom_banner'] = u.custom_banner
|
||||||
u.update_columns(d=r)
|
u.update_columns(d=r)
|
||||||
return success_return(r)
|
return success_return(r)
|
||||||
|
|
||||||
|
|||||||
254
latest version/core/bundle.py
Normal file
254
latest version/core/bundle.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from .config_manager import Config
|
||||||
|
from .constant import Constant
|
||||||
|
from .error import NoAccess, NoData, RateLimit
|
||||||
|
from .limiter import ArcLimiter
|
||||||
|
|
||||||
|
|
||||||
|
class ContentBundle:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.version: str = None
|
||||||
|
self.prev_version: str = None
|
||||||
|
self.app_version: str = None
|
||||||
|
self.uuid: str = None
|
||||||
|
|
||||||
|
self.json_size: int = None
|
||||||
|
self.bundle_size: int = None
|
||||||
|
self.json_path: str = None # relative path
|
||||||
|
self.bundle_path: str = None # relative path
|
||||||
|
|
||||||
|
self.json_url: str = None
|
||||||
|
self.bundle_url: str = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_version(version: str) -> tuple:
|
||||||
|
try:
|
||||||
|
r = tuple(map(int, version.split('.')))
|
||||||
|
except AttributeError:
|
||||||
|
r = (0, 0, 0)
|
||||||
|
return r
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_tuple(self) -> tuple:
|
||||||
|
return self.parse_version(self.version)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json_data: dict) -> 'ContentBundle':
|
||||||
|
x = cls()
|
||||||
|
x.version = json_data['versionNumber']
|
||||||
|
x.prev_version = json_data['previousVersionNumber']
|
||||||
|
x.app_version = json_data['applicationVersionNumber']
|
||||||
|
x.uuid = json_data['uuid']
|
||||||
|
if x.prev_version is None:
|
||||||
|
x.prev_version = '0.0.0'
|
||||||
|
return x
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
r = {
|
||||||
|
'contentBundleVersion': self.version,
|
||||||
|
'appVersion': self.app_version,
|
||||||
|
'jsonSize': self.json_size,
|
||||||
|
'bundleSize': self.bundle_size,
|
||||||
|
}
|
||||||
|
if self.json_url and self.bundle_url:
|
||||||
|
r['jsonUrl'] = self.json_url
|
||||||
|
r['bundleUrl'] = self.bundle_url
|
||||||
|
return r
|
||||||
|
|
||||||
|
def calculate_size(self) -> None:
|
||||||
|
self.json_size = os.path.getsize(os.path.join(
|
||||||
|
Constant.CONTENT_BUNDLE_FOLDER_PATH, self.json_path))
|
||||||
|
self.bundle_size = os.path.getsize(os.path.join(
|
||||||
|
Constant.CONTENT_BUNDLE_FOLDER_PATH, self.bundle_path))
|
||||||
|
|
||||||
|
|
||||||
|
class BundleParser:
|
||||||
|
|
||||||
|
# {app_version: [ List[ContentBundle] ]}
|
||||||
|
bundles: 'dict[str, list[ContentBundle]]' = {}
|
||||||
|
# {app_version: max bundle version}
|
||||||
|
max_bundle_version: 'dict[str, str]' = {}
|
||||||
|
|
||||||
|
# {bundle version: [next versions]} 宽搜索引
|
||||||
|
next_versions: 'dict[str, list[str]]' = {}
|
||||||
|
# {(bver, b prev version): ContentBundle} 正向索引
|
||||||
|
version_tuple_bundles: 'dict[tuple[str, str], ContentBundle]' = {}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not self.bundles:
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
def re_init(self) -> None:
|
||||||
|
self.bundles.clear()
|
||||||
|
self.max_bundle_version.clear()
|
||||||
|
self.next_versions.clear()
|
||||||
|
self.version_tuple_bundles.clear()
|
||||||
|
self.get_bundles.cache_clear()
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
def parse(self) -> None:
|
||||||
|
for root, dirs, files in os.walk(Constant.CONTENT_BUNDLE_FOLDER_PATH):
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith('.json'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
json_path = os.path.join(root, file)
|
||||||
|
bundle_path = os.path.join(root, f'{file[:-5]}.cb')
|
||||||
|
|
||||||
|
with open(json_path, 'rb') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
x = ContentBundle.from_json(data)
|
||||||
|
|
||||||
|
x.json_path = os.path.relpath(
|
||||||
|
json_path, Constant.CONTENT_BUNDLE_FOLDER_PATH)
|
||||||
|
x.bundle_path = os.path.relpath(
|
||||||
|
bundle_path, Constant.CONTENT_BUNDLE_FOLDER_PATH)
|
||||||
|
|
||||||
|
x.json_path = x.json_path.replace('\\', '/')
|
||||||
|
x.bundle_path = x.bundle_path.replace('\\', '/')
|
||||||
|
|
||||||
|
if not os.path.isfile(bundle_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f'Bundle file not found: {bundle_path}')
|
||||||
|
x.calculate_size()
|
||||||
|
|
||||||
|
self.bundles.setdefault(x.app_version, []).append(x)
|
||||||
|
|
||||||
|
self.version_tuple_bundles[(x.version, x.prev_version)] = x
|
||||||
|
self.next_versions.setdefault(
|
||||||
|
x.prev_version, []).append(x.version)
|
||||||
|
|
||||||
|
# sort by version
|
||||||
|
for k, v in self.bundles.items():
|
||||||
|
v.sort(key=lambda x: x.version_tuple)
|
||||||
|
self.max_bundle_version[k] = v[-1].version
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@lru_cache(maxsize=128)
|
||||||
|
def get_bundles(app_ver: str, b_ver: str) -> 'list[ContentBundle]':
|
||||||
|
if Config.BUNDLE_STRICT_MODE:
|
||||||
|
return BundleParser.bundles.get(app_ver, [])
|
||||||
|
|
||||||
|
k = b_ver if b_ver else '0.0.0'
|
||||||
|
|
||||||
|
target_version = BundleParser.max_bundle_version.get(app_ver, '0.0.0')
|
||||||
|
if k == target_version:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# BFS
|
||||||
|
q = [[k]]
|
||||||
|
ans = None
|
||||||
|
while True:
|
||||||
|
qq = []
|
||||||
|
for x in q:
|
||||||
|
if x[-1] == target_version:
|
||||||
|
ans = x
|
||||||
|
break
|
||||||
|
for y in BundleParser.next_versions.get(x[-1], []):
|
||||||
|
if y in x:
|
||||||
|
continue
|
||||||
|
qq.append(x + [y])
|
||||||
|
|
||||||
|
if ans is not None or not qq:
|
||||||
|
break
|
||||||
|
q = qq
|
||||||
|
|
||||||
|
if not ans:
|
||||||
|
raise NoData(
|
||||||
|
f'No bundles found for app version: {app_ver}, bundle version: {b_ver}', status=404)
|
||||||
|
|
||||||
|
r = []
|
||||||
|
for i in range(1, len(ans)):
|
||||||
|
r.append(BundleParser.version_tuple_bundles[(ans[i], ans[i-1])])
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class BundleDownload:
|
||||||
|
|
||||||
|
limiter = ArcLimiter(
|
||||||
|
Constant.BUNDLE_DOWNLOAD_TIMES_LIMIT, 'bundle_download')
|
||||||
|
|
||||||
|
def __init__(self, c_m=None):
|
||||||
|
self.c_m = c_m
|
||||||
|
|
||||||
|
self.client_app_version = None
|
||||||
|
self.client_bundle_version = None
|
||||||
|
self.device_id = None
|
||||||
|
|
||||||
|
def set_client_info(self, app_version: str, bundle_version: str, device_id: str = None) -> None:
|
||||||
|
self.client_app_version = app_version
|
||||||
|
self.client_bundle_version = bundle_version
|
||||||
|
self.device_id = device_id
|
||||||
|
|
||||||
|
def get_bundle_list(self) -> list:
|
||||||
|
bundles: 'list[ContentBundle]' = BundleParser.get_bundles(
|
||||||
|
self.client_app_version, self.client_bundle_version)
|
||||||
|
|
||||||
|
if not bundles:
|
||||||
|
return []
|
||||||
|
|
||||||
|
now = time()
|
||||||
|
|
||||||
|
if Constant.BUNDLE_DOWNLOAD_LINK_PREFIX:
|
||||||
|
prefix = Constant.BUNDLE_DOWNLOAD_LINK_PREFIX
|
||||||
|
if prefix[-1] != '/':
|
||||||
|
prefix += '/'
|
||||||
|
|
||||||
|
def url_func(x): return f'{prefix}{x}'
|
||||||
|
else:
|
||||||
|
def url_func(x): return url_for(
|
||||||
|
'bundle_download', token=x, _external=True)
|
||||||
|
|
||||||
|
sql_list = []
|
||||||
|
r = []
|
||||||
|
for x in bundles:
|
||||||
|
if x.version_tuple <= ContentBundle.parse_version(self.client_bundle_version):
|
||||||
|
continue
|
||||||
|
t1 = os.urandom(64).hex()
|
||||||
|
t2 = os.urandom(64).hex()
|
||||||
|
|
||||||
|
x.json_url = url_func(t1)
|
||||||
|
x.bundle_url = url_func(t2)
|
||||||
|
|
||||||
|
sql_list.append((t1, x.json_path, now, self.device_id))
|
||||||
|
sql_list.append((t2, x.bundle_path, now, self.device_id))
|
||||||
|
r.append(x.to_dict())
|
||||||
|
|
||||||
|
if not sql_list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
self.clear_expired_token()
|
||||||
|
|
||||||
|
self.c_m.executemany(
|
||||||
|
'''insert into bundle_download_token values (?, ?, ?, ?)''', sql_list)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
def get_path_by_token(self, token: str, ip: str) -> str:
|
||||||
|
r = self.c_m.execute(
|
||||||
|
'''select file_path, time, device_id from bundle_download_token where token = ?''', (token,)).fetchone()
|
||||||
|
if not r:
|
||||||
|
raise NoAccess('Invalid token.', status=403)
|
||||||
|
file_path, create_time, device_id = r
|
||||||
|
|
||||||
|
if time() - create_time > Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT:
|
||||||
|
raise NoAccess('Expired token.', status=403)
|
||||||
|
|
||||||
|
if file_path.endswith('.cb') and not self.limiter.hit(ip):
|
||||||
|
raise RateLimit(
|
||||||
|
f'Too many content bundle downloads, IP: {ip}, DeviceID: {device_id}', status=429)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
def clear_expired_token(self) -> None:
|
||||||
|
self.c_m.execute(
|
||||||
|
'''delete from bundle_download_token where time < ?''', (int(time() - Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT),))
|
||||||
@@ -2,6 +2,7 @@ from .config_manager import Config
|
|||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .error import ArcError, InputError, ItemNotEnough, NoData
|
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||||
from .item import CollectionItemMixin, ItemCore
|
from .item import CollectionItemMixin, ItemCore
|
||||||
|
from .sql import UserKVTable
|
||||||
|
|
||||||
|
|
||||||
class Level:
|
class Level:
|
||||||
@@ -55,6 +56,7 @@ class Skill:
|
|||||||
class CharacterValue:
|
class CharacterValue:
|
||||||
def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
|
def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
|
||||||
self.set_parameter(start, mid, end)
|
self.set_parameter(start, mid, end)
|
||||||
|
self.addition: float = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
|
def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
|
||||||
@@ -87,9 +89,9 @@ 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_math(level.level, self.start, self.mid)
|
return self._calc_char_value_20_math(level.level, self.start, self.mid) + self.addition
|
||||||
if level.mid_level < level.level <= level.max_level:
|
if 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) + self.addition
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -120,6 +122,18 @@ class Character(CollectionItemMixin):
|
|||||||
self.uncap_cores: list = []
|
self.uncap_cores: list = []
|
||||||
self.voice: list = None
|
self.voice: list = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frag_value(self) -> float:
|
||||||
|
return self.frag.get_value(self.level)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prog_value(self) -> float:
|
||||||
|
return self.prog.get_value(self.level)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def overdrive_value(self) -> float:
|
||||||
|
return self.overdrive.get_value(self.level)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skill_id_displayed(self) -> str:
|
def skill_id_displayed(self) -> str:
|
||||||
return None
|
return None
|
||||||
@@ -217,6 +231,10 @@ class UserCharacter(Character):
|
|||||||
self.character_id = character_id
|
self.character_id = character_id
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
|
self.skill_flag: bool = None
|
||||||
|
|
||||||
|
self.fatalis_is_limited: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skill_id_displayed(self) -> str:
|
def skill_id_displayed(self) -> str:
|
||||||
'''对外显示的技能id'''
|
'''对外显示的技能id'''
|
||||||
@@ -226,6 +244,12 @@ class UserCharacter(Character):
|
|||||||
return self.skill.skill_id
|
return self.skill.skill_id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skill_state(self) -> str:
|
||||||
|
if self.skill_id_displayed == 'skill_maya':
|
||||||
|
return 'add_random' if self.skill_flag else 'remove_random'
|
||||||
|
return None
|
||||||
|
|
||||||
def select_character_uncap_condition(self, user=None):
|
def select_character_uncap_condition(self, user=None):
|
||||||
# parameter: user - User类或子类的实例
|
# parameter: user - User类或子类的实例
|
||||||
# 获取此角色的觉醒信息
|
# 获取此角色的觉醒信息
|
||||||
@@ -255,24 +279,42 @@ class UserCharacter(Character):
|
|||||||
if y is None:
|
if y is None:
|
||||||
raise NoData('The character of the user does not exist.')
|
raise NoData('The character of the user does not exist.')
|
||||||
|
|
||||||
self.name = y[7]
|
self.name = y[8]
|
||||||
self.char_type = y[22]
|
self.char_type = y[23]
|
||||||
self.is_uncapped = y[4] == 1
|
self.is_uncapped = y[4] == 1
|
||||||
self.is_uncapped_override = y[5] == 1
|
self.is_uncapped_override = y[5] == 1
|
||||||
self.level.level = y[2]
|
self.level.level = y[2]
|
||||||
self.level.exp = y[3]
|
self.level.exp = y[3]
|
||||||
self.level.max_level = y[8]
|
self.level.max_level = y[9]
|
||||||
self.frag.set_parameter(y[9], y[12], y[15])
|
self.frag.set_parameter(y[10], y[13], y[16])
|
||||||
self.prog.set_parameter(y[10], y[13], y[16])
|
self.prog.set_parameter(y[11], y[14], y[17])
|
||||||
self.overdrive.set_parameter(y[11], y[14], y[17])
|
self.overdrive.set_parameter(y[12], y[15], y[18])
|
||||||
self.skill.skill_id = y[18]
|
self.skill.skill_id = y[19]
|
||||||
self.skill.skill_id_uncap = y[21]
|
self.skill.skill_id_uncap = y[22]
|
||||||
self.skill.skill_unlock_level = y[19]
|
self.skill.skill_unlock_level = y[20]
|
||||||
self.skill.skill_requires_uncap = y[20] == 1
|
self.skill.skill_requires_uncap = y[21] == 1
|
||||||
|
|
||||||
|
self.skill_flag = y[6] == 1
|
||||||
|
|
||||||
if self.character_id in (21, 46):
|
if self.character_id in (21, 46):
|
||||||
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
|
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
|
||||||
|
|
||||||
|
if self.character_id == 55:
|
||||||
|
# fatalis 提升数值
|
||||||
|
# prog & overdrive += 世界模式中完成的所有非无限地图的台阶数之和 / 30
|
||||||
|
if Config.CHARACTER_FULL_UNLOCK:
|
||||||
|
addition = Constant.FATALIS_MAX_VALUE
|
||||||
|
self.fatalis_is_limited = True
|
||||||
|
else:
|
||||||
|
kvd = UserKVTable(self.c, self.user.user_id, 'world')
|
||||||
|
steps = kvd['total_step_count'] or 0
|
||||||
|
addition = steps / 30
|
||||||
|
if addition >= Constant.FATALIS_MAX_VALUE:
|
||||||
|
addition = Constant.FATALIS_MAX_VALUE
|
||||||
|
self.fatalis_is_limited = True
|
||||||
|
self.prog.addition = addition
|
||||||
|
self.overdrive.addition = addition
|
||||||
|
|
||||||
self.select_character_core()
|
self.select_character_core()
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -299,9 +341,14 @@ class UserCharacter(Character):
|
|||||||
if self.voice:
|
if self.voice:
|
||||||
r['voice'] = self.voice
|
r['voice'] = self.voice
|
||||||
if self.character_id == 55:
|
if self.character_id == 55:
|
||||||
r['fatalis_is_limited'] = False # emmmmmmm
|
r['fatalis_is_limited'] = self.fatalis_is_limited
|
||||||
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
|
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
|
||||||
r['base_character_id'] = 1
|
r['base_character_id'] = 1
|
||||||
|
|
||||||
|
x = self.skill_state
|
||||||
|
if x:
|
||||||
|
r['skill_state'] = x
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def change_uncap_override(self, user=None):
|
def change_uncap_override(self, user=None):
|
||||||
@@ -409,6 +456,12 @@ class UserCharacter(Character):
|
|||||||
core.user_claim_item(self.user)
|
core.user_claim_item(self.user)
|
||||||
self.upgrade(self.user, - core.amount * Constant.CORE_EXP)
|
self.upgrade(self.user, - core.amount * Constant.CORE_EXP)
|
||||||
|
|
||||||
|
def change_skill_state(self) -> None:
|
||||||
|
'''翻转技能状态,目前为 skill_miya 专用'''
|
||||||
|
self.skill_flag = not self.skill_flag
|
||||||
|
self.c.execute(f'''update {self.database_table_name} set skill_flag = ? where user_id = ? and character_id = ?''', (
|
||||||
|
1 if self.skill_flag else 0, self.user.user_id, self.character_id))
|
||||||
|
|
||||||
|
|
||||||
class UserCharacterList:
|
class UserCharacterList:
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ class Config:
|
|||||||
|
|
||||||
SONG_FILE_HASH_PRE_CALCULATE = True
|
SONG_FILE_HASH_PRE_CALCULATE = True
|
||||||
|
|
||||||
GAME_API_PREFIX = '/pollen/22'
|
GAME_API_PREFIX = ['/apricotduck/38', '/'] # str | list[str]
|
||||||
|
OLD_GAME_API_PREFIX = [] # str | list[str]
|
||||||
|
|
||||||
ALLOW_APPVERSION = [] # list[str]
|
ALLOW_APPVERSION = [] # list[str]
|
||||||
|
|
||||||
|
BUNDLE_STRICT_MODE = True
|
||||||
|
|
||||||
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
|
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
|
||||||
|
|
||||||
LINKPLAY_HOST = '0.0.0.0'
|
LINKPLAY_HOST = '0.0.0.0'
|
||||||
@@ -23,6 +26,7 @@ class Config:
|
|||||||
LINKPLAY_TCP_PORT = 10901
|
LINKPLAY_TCP_PORT = 10901
|
||||||
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
|
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
|
||||||
LINKPLAY_DISPLAY_HOST = ''
|
LINKPLAY_DISPLAY_HOST = ''
|
||||||
|
LINKPLAY_TCP_SECRET_KEY = '1145141919810'
|
||||||
|
|
||||||
SSL_CERT = ''
|
SSL_CERT = ''
|
||||||
SSL_KEY = ''
|
SSL_KEY = ''
|
||||||
@@ -40,14 +44,21 @@ class Config:
|
|||||||
|
|
||||||
API_TOKEN = ''
|
API_TOKEN = ''
|
||||||
|
|
||||||
DOWNLOAD_LINK_PREFIX = ''
|
DOWNLOAD_LINK_PREFIX = '' # http(s)://host(:port)/download/
|
||||||
|
BUNDLE_DOWNLOAD_LINK_PREFIX = '' # http(s)://host(:port)/bundle_download/
|
||||||
|
|
||||||
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
|
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
|
||||||
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
|
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
|
||||||
|
BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_bundle_download/'
|
||||||
|
|
||||||
DOWNLOAD_TIMES_LIMIT = 3000
|
DOWNLOAD_TIMES_LIMIT = 3000
|
||||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||||
|
|
||||||
|
DOWNLOAD_FORBID_WHEN_NO_ITEM = False
|
||||||
|
|
||||||
|
BUNDLE_DOWNLOAD_TIMES_LIMIT = '100/60 minutes'
|
||||||
|
BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = 3000
|
||||||
|
|
||||||
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||||
ALLOW_LOGIN_SAME_DEVICE = False
|
ALLOW_LOGIN_SAME_DEVICE = False
|
||||||
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
||||||
@@ -67,24 +78,36 @@ class Config:
|
|||||||
|
|
||||||
SAVE_FULL_UNLOCK = False
|
SAVE_FULL_UNLOCK = False
|
||||||
|
|
||||||
|
ALLOW_SELF_ACCOUNT_DELETE = False
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
# You can change this to make another PTT mechanism.
|
# You can change this to make another PTT mechanism.
|
||||||
BEST30_WEIGHT = 1 / 40
|
BEST30_WEIGHT = 1 / 40
|
||||||
RECENT10_WEIGHT = 1 / 40
|
RECENT10_WEIGHT = 1 / 40
|
||||||
|
|
||||||
|
INVASION_START_WEIGHT = 0.1
|
||||||
|
INVASION_HARD_WEIGHT = 0.1
|
||||||
|
|
||||||
MAX_FRIEND_COUNT = 50
|
MAX_FRIEND_COUNT = 50
|
||||||
|
|
||||||
WORLD_MAP_FOLDER_PATH = './database/map/'
|
WORLD_MAP_FOLDER_PATH = './database/map/'
|
||||||
SONG_FILE_FOLDER_PATH = './database/songs/'
|
SONG_FILE_FOLDER_PATH = './database/songs/'
|
||||||
SONGLIST_FILE_PATH = './database/songs/songlist'
|
SONGLIST_FILE_PATH = './database/songs/songlist'
|
||||||
|
CONTENT_BUNDLE_FOLDER_PATH = './database/bundle/'
|
||||||
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'
|
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
|
||||||
|
SQLITE_DATABASE_DELETED_PATH = './database/arcaea_database_deleted.db'
|
||||||
|
LOG_FOLDER_PATH = './log/'
|
||||||
|
|
||||||
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'
|
||||||
|
GAME_REGISTER_IP_RATE_LIMIT = '10/1 day'
|
||||||
|
GAME_REGISTER_DEVICE_RATE_LIMIT = '3/1 day'
|
||||||
|
|
||||||
|
NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
|
|
||||||
ARCAEA_SERVER_VERSION = 'v2.11.2'
|
ARCAEA_SERVER_VERSION = 'v2.12.1.8'
|
||||||
|
ARCAEA_DATABASE_VERSION = 'v2.12.1.8'
|
||||||
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +11,12 @@ class Constant:
|
|||||||
|
|
||||||
MAX_STAMINA = 12
|
MAX_STAMINA = 12
|
||||||
|
|
||||||
|
# INSIGHT_STATES = [x for x in range(7)]
|
||||||
|
INSIGHT_TOGGLE_STATES = [3, 4, 5, 6]
|
||||||
|
# DEFAULT_INSIGHT_STATE = Config.DEFAULT_INSIGHT_STATE
|
||||||
|
|
||||||
STAMINA_RECOVER_TICK = 1800000
|
STAMINA_RECOVER_TICK = 1800000
|
||||||
|
FRAGSTAM_RECOVER_TICK = 23 * 3600 * 1000
|
||||||
|
|
||||||
COURSE_STAMINA_COST = 4
|
COURSE_STAMINA_COST = 4
|
||||||
|
|
||||||
@@ -19,12 +25,18 @@ class Constant:
|
|||||||
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
|
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
|
||||||
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
|
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
|
||||||
|
|
||||||
|
WORLD_VALUE_NAME_ENUM = ['frag', 'prog', 'over']
|
||||||
|
|
||||||
|
FREE_PACK_NAME = 'base'
|
||||||
|
SINGLE_PACK_NAME = 'single'
|
||||||
|
|
||||||
ETO_UNCAP_BONUS_PROGRESS = 7
|
ETO_UNCAP_BONUS_PROGRESS = 7
|
||||||
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',
|
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
|
||||||
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
|
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
|
||||||
|
FATALIS_MAX_VALUE = 100
|
||||||
|
|
||||||
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
||||||
|
|
||||||
@@ -33,24 +45,39 @@ class Constant:
|
|||||||
|
|
||||||
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
||||||
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
||||||
|
INVASION_START_WEIGHT = Config.INVASION_START_WEIGHT
|
||||||
|
INVASION_HARD_WEIGHT = Config.INVASION_HARD_WEIGHT
|
||||||
|
|
||||||
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
|
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
|
||||||
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
|
||||||
|
CONTENT_BUNDLE_FOLDER_PATH = Config.CONTENT_BUNDLE_FOLDER_PATH
|
||||||
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
||||||
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
||||||
|
SQLITE_DATABASE_DELETED_PATH = Config.SQLITE_DATABASE_DELETED_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
|
||||||
DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX
|
DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX
|
||||||
|
BUNDLE_DOWNLOAD_TIMES_LIMIT = Config.BUNDLE_DOWNLOAD_TIMES_LIMIT
|
||||||
|
BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = Config.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT
|
||||||
|
BUNDLE_DOWNLOAD_LINK_PREFIX = Config.BUNDLE_DOWNLOAD_LINK_PREFIX
|
||||||
|
|
||||||
LINKPLAY_UNLOCK_LENGTH = 512 # Units: bytes
|
LINKPLAY_UNLOCK_LENGTH = 1024 # Units: bytes
|
||||||
LINKPLAY_TIMEOUT = 5 # Units: seconds
|
LINKPLAY_TIMEOUT = 5 # Units: seconds
|
||||||
|
|
||||||
LINKPLAY_HOST = '127.0.0.1' if Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS else Config.LINKPLAY_HOST
|
LINKPLAY_HOST = '127.0.0.1' if Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS else Config.LINKPLAY_HOST
|
||||||
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
|
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
|
||||||
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
|
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
|
||||||
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
|
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
|
||||||
|
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
|
||||||
|
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||||
|
|
||||||
|
LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4 # Units: seconds
|
||||||
|
LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000]
|
||||||
|
LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1]
|
||||||
|
LINKPLAY_MATCH_TIMEOUT = 15 # Units: seconds
|
||||||
|
LINKPLAY_MATCH_MEMORY_CLEAN_INTERVAL = 60 # Units: seconds
|
||||||
|
|
||||||
# Well, I can't say a word when I see this.
|
# Well, I can't say a word when I see this.
|
||||||
FINALE_SWITCH = [
|
FINALE_SWITCH = [
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Course:
|
|||||||
|
|
||||||
self.requirements: list = []
|
self.requirements: list = []
|
||||||
self.charts: list = [None, None, None, None]
|
self.charts: list = [None, None, None, None]
|
||||||
self.items: list = None
|
self.items: list = []
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
if self.course_name is None:
|
if self.course_name is None:
|
||||||
@@ -160,13 +160,17 @@ class UserCourse(Course):
|
|||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.is_completed: bool = False
|
# self.is_completed: bool = False
|
||||||
self.high_score: int = None
|
self.high_score: int = None
|
||||||
self.best_clear_type: int = None
|
self.best_clear_type: int = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_completed(self) -> bool:
|
||||||
|
return self.best_clear_type != 0
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
r = super().to_dict()
|
r = super().to_dict()
|
||||||
if self.is_completed is None:
|
if self.best_clear_type is None:
|
||||||
self.select_user_course()
|
self.select_user_course()
|
||||||
r.update({
|
r.update({
|
||||||
'is_completed': self.is_completed,
|
'is_completed': self.is_completed,
|
||||||
@@ -182,22 +186,16 @@ class UserCourse(Course):
|
|||||||
(self.user.user_id, self.course_id))
|
(self.user.user_id, self.course_id))
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
self.is_completed = False
|
|
||||||
self.high_score = 0
|
self.high_score = 0
|
||||||
self.best_clear_type = 0
|
self.best_clear_type = 0
|
||||||
else:
|
else:
|
||||||
self.is_completed = True
|
|
||||||
self.high_score = x[2]
|
self.high_score = x[2]
|
||||||
self.best_clear_type = x[3]
|
self.best_clear_type = x[3]
|
||||||
|
|
||||||
def insert_user_course(self) -> None:
|
def insert_or_update_user_course(self) -> None:
|
||||||
self.c.execute('''insert into user_course values (?,?,?,?)''',
|
self.c.execute('''insert or replace into user_course values (?,?,?,?)''',
|
||||||
(self.user.user_id, self.course_id, self.high_score, self.best_clear_type))
|
(self.user.user_id, self.course_id, self.high_score, self.best_clear_type))
|
||||||
|
|
||||||
def update_user_course(self) -> None:
|
|
||||||
self.c.execute('''update user_course set high_score = ?, best_clear_type = ? where user_id = ? and course_id = ?''',
|
|
||||||
(self.high_score, self.best_clear_type, self.user.user_id, self.course_id))
|
|
||||||
|
|
||||||
|
|
||||||
class UserCourseList:
|
class UserCourseList:
|
||||||
'''
|
'''
|
||||||
@@ -262,15 +260,27 @@ class CoursePlay(UserCourse):
|
|||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
'''课题模式更新'''
|
'''课题模式更新'''
|
||||||
|
self.select_user_course()
|
||||||
|
|
||||||
|
self.score += self.user_play.score
|
||||||
|
|
||||||
|
flag = False
|
||||||
|
if self.score > self.high_score:
|
||||||
|
self.high_score = self.score
|
||||||
|
flag = True
|
||||||
|
|
||||||
if self.user_play.health < 0:
|
if self.user_play.health < 0:
|
||||||
# 你挂了
|
# 你挂了
|
||||||
self.user_play.course_play_state = 5
|
self.user_play.course_play_state = 5
|
||||||
self.score = 0
|
self.score = 0
|
||||||
self.clear_type = 0
|
self.clear_type = 0
|
||||||
self.user_play.update_play_state_for_course()
|
self.user_play.update_play_state_for_course()
|
||||||
|
if flag:
|
||||||
|
self.insert_or_update_user_course()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.user_play.course_play_state += 1
|
self.user_play.course_play_state += 1
|
||||||
self.score += self.user_play.score
|
|
||||||
from .score import Score
|
from .score import Score
|
||||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type):
|
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type):
|
||||||
self.clear_type = self.user_play.clear_type
|
self.clear_type = self.user_play.clear_type
|
||||||
@@ -278,24 +288,14 @@ class CoursePlay(UserCourse):
|
|||||||
|
|
||||||
if self.user_play.course_play_state == 4:
|
if self.user_play.course_play_state == 4:
|
||||||
self.user.select_user_about_stamina()
|
self.user.select_user_about_stamina()
|
||||||
self.select_course_item()
|
|
||||||
for i in self.items:
|
|
||||||
i.user_claim_item(self.user)
|
|
||||||
|
|
||||||
self.select_user_course()
|
|
||||||
if not self.is_completed:
|
if not self.is_completed:
|
||||||
self.high_score = self.score
|
self.select_course_item()
|
||||||
self.best_clear_type = self.clear_type
|
for i in self.items:
|
||||||
self.is_completed = True
|
i.user_claim_item(self.user)
|
||||||
self.insert_user_course()
|
|
||||||
return None
|
|
||||||
|
|
||||||
flag = False
|
|
||||||
if self.score > self.high_score:
|
|
||||||
self.high_score = self.score
|
|
||||||
flag = True
|
|
||||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.best_clear_type):
|
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.best_clear_type):
|
||||||
self.best_clear_type = self.clear_type
|
self.best_clear_type = self.clear_type
|
||||||
flag = True
|
flag = True
|
||||||
if flag:
|
|
||||||
self.update_user_course()
|
if flag:
|
||||||
|
self.insert_or_update_user_course()
|
||||||
|
|||||||
@@ -24,18 +24,16 @@ def get_song_file_md5(song_id: str, file_name: str) -> str:
|
|||||||
class SonglistParser:
|
class SonglistParser:
|
||||||
'''songlist文件解析器'''
|
'''songlist文件解析器'''
|
||||||
|
|
||||||
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
|
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff', '4.aff',
|
||||||
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']
|
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4']
|
||||||
|
|
||||||
|
has_songlist = False
|
||||||
songs: dict = {} # {song_id: value, ...}
|
songs: dict = {} # {song_id: value, ...}
|
||||||
# value: bit 76543210
|
# value: bitmap
|
||||||
# 7: video_audio.ogg
|
|
||||||
# 6: video.mp4
|
pack_info: 'dict[str, set]' = {} # {pack_id: {song_id, ...}, ...}
|
||||||
# 5: 3.ogg
|
free_songs: set = set() # {song_id, ...}
|
||||||
# 4: base.ogg
|
world_songs: set = set() # {world_song_id, ...}
|
||||||
# 3: 3.aff
|
|
||||||
# 2: 2.aff
|
|
||||||
# 1: 1.aff
|
|
||||||
# 0: 0.aff
|
|
||||||
|
|
||||||
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
|
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -49,48 +47,100 @@ class SonglistParser:
|
|||||||
# songlist没有,则只限制文件名
|
# songlist没有,则只限制文件名
|
||||||
return file_name in SonglistParser.FILE_NAMES
|
return file_name in SonglistParser.FILE_NAMES
|
||||||
rule = SonglistParser.songs[song_id]
|
rule = SonglistParser.songs[song_id]
|
||||||
for i in range(8):
|
for i, v in enumerate(SonglistParser.FILE_NAMES):
|
||||||
if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0:
|
if file_name == v and rule & (1 << i) != 0:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_unlocks(user) -> set:
|
||||||
|
'''user: UserInfo类或子类的实例'''
|
||||||
|
x = SonglistParser
|
||||||
|
if user is None:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
r = set()
|
||||||
|
for i in user.packs:
|
||||||
|
if i in x.pack_info:
|
||||||
|
r.update(x.pack_info[i])
|
||||||
|
|
||||||
|
if Constant.SINGLE_PACK_NAME in x.pack_info:
|
||||||
|
r.update(x.pack_info[Constant.SINGLE_PACK_NAME]
|
||||||
|
& set(user.singles))
|
||||||
|
r.update(set(i if i[-1] != '3' else i[:-1]
|
||||||
|
for i in (x.world_songs & set(user.world_songs))))
|
||||||
|
r.update(x.free_songs)
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
def parse_one(self, song: dict) -> dict:
|
def parse_one(self, song: dict) -> dict:
|
||||||
'''解析单个歌曲'''
|
'''解析单个歌曲'''
|
||||||
|
# TODO: byd_local_unlock ???
|
||||||
if not 'id' in song:
|
if not 'id' in song:
|
||||||
return None
|
return {}
|
||||||
r = 0
|
r = 0
|
||||||
if 'remote_dl' in song and song['remote_dl']:
|
if 'remote_dl' in song and song['remote_dl']:
|
||||||
r |= 16
|
r |= 32
|
||||||
for i in song.get('difficulties', []):
|
for i in song.get('difficulties', []):
|
||||||
if i['ratingClass'] == 3 and i.get('audioOverride', False):
|
if i['ratingClass'] == 3 and i.get('audioOverride', False):
|
||||||
r |= 32
|
r |= 64
|
||||||
r |= 1 << i['ratingClass']
|
r |= 1 << i['ratingClass']
|
||||||
else:
|
else: # 針對remote_dl為False時BYD難度強制下載的處理
|
||||||
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
|
for i in song.get('difficulties', []):
|
||||||
r |= 8
|
if i['ratingClass'] == 3:
|
||||||
|
r |= 8
|
||||||
|
if i.get('audioOverride', False):
|
||||||
|
r |= 64
|
||||||
|
|
||||||
if 'additional_files' in song:
|
for extra_file in song.get('additional_files', []):
|
||||||
if 'video.mp4' in song['additional_files']:
|
x = extra_file['file_name']
|
||||||
r |= 64
|
if x == SonglistParser.FILE_NAMES[7]:
|
||||||
if 'video_audio.ogg' in song['additional_files']:
|
|
||||||
r |= 128
|
r |= 128
|
||||||
|
elif x == SonglistParser.FILE_NAMES[8]:
|
||||||
|
r |= 256
|
||||||
|
elif x == SonglistParser.FILE_NAMES[9]:
|
||||||
|
r |= 512
|
||||||
|
elif x == SonglistParser.FILE_NAMES[10]:
|
||||||
|
r |= 1024
|
||||||
|
|
||||||
return {song['id']: r}
|
return {song['id']: r}
|
||||||
|
|
||||||
|
def parse_one_unlock(self, song: dict) -> None:
|
||||||
|
'''解析单个歌曲解锁方式'''
|
||||||
|
if not 'id' in song or not 'set' in song or not 'purchase' in song:
|
||||||
|
return {}
|
||||||
|
x = SonglistParser
|
||||||
|
if Constant.FREE_PACK_NAME == song['set']:
|
||||||
|
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
|
||||||
|
x.world_songs.add(song['id'] + '3')
|
||||||
|
x.free_songs.add(song['id'])
|
||||||
|
return None
|
||||||
|
|
||||||
|
if song.get('world_unlock', False):
|
||||||
|
x.world_songs.add(song['id'])
|
||||||
|
|
||||||
|
if song['purchase'] == '':
|
||||||
|
return None
|
||||||
|
|
||||||
|
x.pack_info.setdefault(song['set'], set()).add(song['id'])
|
||||||
|
|
||||||
def parse(self) -> None:
|
def parse(self) -> None:
|
||||||
'''解析songlist文件'''
|
'''解析songlist文件'''
|
||||||
if not os.path.isfile(self.path):
|
if not os.path.isfile(self.path):
|
||||||
return
|
return
|
||||||
with open(self.path, 'r', encoding='utf-8') as f:
|
with open(self.path, 'rb') as f:
|
||||||
self.data = loads(f.read()).get('songs', [])
|
self.data = loads(f.read()).get('songs', [])
|
||||||
|
self.has_songlist = True
|
||||||
for x in self.data:
|
for x in self.data:
|
||||||
self.songs.update(self.parse_one(x))
|
self.songs.update(self.parse_one(x))
|
||||||
|
self.parse_one_unlock(x)
|
||||||
|
|
||||||
|
|
||||||
class UserDownload:
|
class UserDownload:
|
||||||
'''
|
'''
|
||||||
用户下载类
|
用户下载类
|
||||||
|
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `UserInfo`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
limiter = ArcLimiter(
|
limiter = ArcLimiter(
|
||||||
@@ -198,6 +248,10 @@ class DownloadList(UserDownload):
|
|||||||
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.songs = {}
|
SonglistParser.songs = {}
|
||||||
|
SonglistParser.pack_info = {}
|
||||||
|
SonglistParser.free_songs = set()
|
||||||
|
SonglistParser.world_songs = set()
|
||||||
|
SonglistParser.has_songlist = False
|
||||||
|
|
||||||
def clear_download_token(self) -> None:
|
def clear_download_token(self) -> None:
|
||||||
'''清除过期下载链接'''
|
'''清除过期下载链接'''
|
||||||
@@ -243,7 +297,7 @@ class DownloadList(UserDownload):
|
|||||||
re['audio']['3'] = {"checksum": x.hash, "url": x.url}
|
re['audio']['3'] = {"checksum": x.hash, "url": x.url}
|
||||||
else:
|
else:
|
||||||
re['audio']['3'] = {"checksum": x.hash}
|
re['audio']['3'] = {"checksum": x.hash}
|
||||||
elif i in ('video.mp4', 'video_audio.ogg'):
|
elif i in ('video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4'):
|
||||||
if 'additional_files' not in re:
|
if 'additional_files' not in re:
|
||||||
re['additional_files'] = []
|
re['additional_files'] = []
|
||||||
|
|
||||||
@@ -253,6 +307,7 @@ class DownloadList(UserDownload):
|
|||||||
else:
|
else:
|
||||||
re['additional_files'].append(
|
re['additional_files'].append(
|
||||||
{"checksum": x.hash, 'file_name': i})
|
{"checksum": x.hash, 'file_name': i})
|
||||||
|
# 有参数 requirement 作用未知
|
||||||
else:
|
else:
|
||||||
if 'chart' not in re:
|
if 'chart' not in re:
|
||||||
re['chart'] = {}
|
re['chart'] = {}
|
||||||
@@ -277,9 +332,19 @@ class DownloadList(UserDownload):
|
|||||||
|
|
||||||
if not self.song_ids:
|
if not self.song_ids:
|
||||||
self.song_ids = self.get_all_song_ids()
|
self.song_ids = self.get_all_song_ids()
|
||||||
|
if Config.DOWNLOAD_FORBID_WHEN_NO_ITEM and SonglistParser.has_songlist:
|
||||||
|
# 没有歌曲时不允许下载
|
||||||
|
self.song_ids = list(SonglistParser.get_user_unlocks(
|
||||||
|
self.user) & set(self.song_ids))
|
||||||
|
|
||||||
for i in self.song_ids:
|
for i in self.song_ids:
|
||||||
self.add_one_song(i)
|
self.add_one_song(i)
|
||||||
else:
|
else:
|
||||||
|
if Config.DOWNLOAD_FORBID_WHEN_NO_ITEM and SonglistParser.has_songlist:
|
||||||
|
# 没有歌曲时不允许下载
|
||||||
|
self.song_ids = list(SonglistParser.get_user_unlocks(
|
||||||
|
self.user) & set(self.song_ids))
|
||||||
|
|
||||||
for i in self.song_ids:
|
for i in self.song_ids:
|
||||||
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
|
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
|
||||||
self.add_one_song(i)
|
self.add_one_song(i)
|
||||||
|
|||||||
@@ -101,6 +101,13 @@ class NoAccess(ArcError):
|
|||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
|
class LowVersion(ArcError):
|
||||||
|
'''版本过低'''
|
||||||
|
|
||||||
|
def __init__(self, message=None, error_code=5, api_error_code=-999, extra_data=None, status=403) -> None:
|
||||||
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class Timeout(ArcError):
|
class Timeout(ArcError):
|
||||||
'''超时'''
|
'''超时'''
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ from shutil import copy, copy2
|
|||||||
from time import time
|
from time import time
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
|
from core.bundle import BundleParser
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
|
from core.constant import ARCAEA_DATABASE_VERSION, ARCAEA_LOG_DATBASE_VERSION
|
||||||
from core.course import Course
|
from core.course import Course
|
||||||
from core.download import DownloadList
|
from core.download import DownloadList
|
||||||
from core.purchase import Purchase
|
from core.purchase import Purchase
|
||||||
@@ -15,6 +16,7 @@ from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
|
|||||||
MemoryDatabase)
|
MemoryDatabase)
|
||||||
from core.user import UserRegister
|
from core.user import UserRegister
|
||||||
from core.util import try_rename
|
from core.util import try_rename
|
||||||
|
from core.world import MapParser
|
||||||
|
|
||||||
|
|
||||||
class DatabaseInit:
|
class DatabaseInit:
|
||||||
@@ -45,7 +47,7 @@ class DatabaseInit:
|
|||||||
with open(self.sql_path, 'r', encoding='utf-8') as f:
|
with open(self.sql_path, 'r', encoding='utf-8') as f:
|
||||||
self.c.executescript(f.read())
|
self.c.executescript(f.read())
|
||||||
self.c.execute('''insert into config values("version", :a);''', {
|
self.c.execute('''insert into config values("version", :a);''', {
|
||||||
'a': ARCAEA_SERVER_VERSION})
|
'a': ARCAEA_DATABASE_VERSION})
|
||||||
|
|
||||||
def character_init(self) -> None:
|
def character_init(self) -> None:
|
||||||
'''初始化搭档信息'''
|
'''初始化搭档信息'''
|
||||||
@@ -92,6 +94,8 @@ class DatabaseInit:
|
|||||||
('memory', 'memory', 1))
|
('memory', 'memory', 1))
|
||||||
self.c.execute('''insert into item values(?,?,?)''',
|
self.c.execute('''insert into item values(?,?,?)''',
|
||||||
('anni5tix', 'anni5tix', 1))
|
('anni5tix', 'anni5tix', 1))
|
||||||
|
self.c.execute('''insert into item values(?,?,?)''',
|
||||||
|
('pick_ticket', 'pick_ticket', 1))
|
||||||
|
|
||||||
with open(self.pack_path, 'rb') as f:
|
with open(self.pack_path, 'rb') as f:
|
||||||
self.insert_purchase_item(load(f))
|
self.insert_purchase_item(load(f))
|
||||||
@@ -99,6 +103,9 @@ class DatabaseInit:
|
|||||||
with open(self.single_path, 'rb') 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('''insert into item values(?,?,?)''', # 新手任务奖励曲
|
||||||
|
('innocence', 'world_song', 1))
|
||||||
|
|
||||||
def course_init(self) -> None:
|
def course_init(self) -> None:
|
||||||
'''初始化课题信息'''
|
'''初始化课题信息'''
|
||||||
courses = []
|
courses = []
|
||||||
@@ -135,8 +142,7 @@ class DatabaseInit:
|
|||||||
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
|
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
|
||||||
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
|
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
|
||||||
''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email})
|
''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email})
|
||||||
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
|
||||||
'user_id': x.user_id})
|
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''insert into user_role values(?, "admin")''', (x.user_id,))
|
'''insert into user_role values(?, "admin")''', (x.user_id,))
|
||||||
|
|
||||||
@@ -169,7 +175,17 @@ class LogDatabaseInit:
|
|||||||
with open(self.sql_path, 'r') as f:
|
with open(self.sql_path, 'r') as f:
|
||||||
self.c.executescript(f.read())
|
self.c.executescript(f.read())
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION})
|
'''insert into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
with Connect(self.db_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_init()
|
||||||
|
|
||||||
|
|
||||||
|
class DeletedDatabaseInit(DatabaseInit):
|
||||||
|
def __init__(self, db_path: str = Config.SQLITE_DATABASE_DELETED_PATH) -> None:
|
||||||
|
super().__init__(db_path)
|
||||||
|
|
||||||
def init(self) -> None:
|
def init(self) -> None:
|
||||||
with Connect(self.db_path) as c:
|
with Connect(self.db_path) as c:
|
||||||
@@ -195,7 +211,7 @@ class FileChecker:
|
|||||||
self.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_log(self) -> bool:
|
||||||
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
||||||
# 新建日志数据库
|
# 新建日志数据库
|
||||||
try:
|
try:
|
||||||
@@ -232,66 +248,69 @@ class FileChecker:
|
|||||||
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
return True
|
||||||
|
|
||||||
|
def _check_update_database_main(self, db_path=Config.SQLITE_DATABASE_PATH, init_class=DatabaseInit) -> bool:
|
||||||
|
if not self.check_file(db_path):
|
||||||
# 新建数据库
|
# 新建数据库
|
||||||
try:
|
try:
|
||||||
self.logger.info(
|
self.logger.info(f'Try to new the file `{db_path}`.')
|
||||||
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
init_class().init()
|
||||||
DatabaseInit().init()
|
self.logger.info(f'Success to new the file `{db_path}`.')
|
||||||
self.logger.info(
|
|
||||||
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
self.logger.warning(
|
self.logger.warning(f'Failed to new the file `{db_path}`.')
|
||||||
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# 检查更新
|
# 检查更新
|
||||||
with Connect() as c:
|
with Connect(db_path) as c:
|
||||||
try:
|
try:
|
||||||
c.execute('''select value from config where id="version"''')
|
c.execute('''select value from config where id="version"''')
|
||||||
x = c.fetchone()
|
x = c.fetchone()
|
||||||
except:
|
except:
|
||||||
x = None
|
x = None
|
||||||
# 数据库自动更新,不强求
|
# 数据库自动更新,不强求
|
||||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
if not x or x[0] != ARCAEA_DATABASE_VERSION:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
|
f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}')
|
||||||
try:
|
try:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Try to update the file `{db_path}` to version {ARCAEA_DATABASE_VERSION}.')
|
||||||
|
|
||||||
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
||||||
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
|
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
|
||||||
|
|
||||||
backup_path = try_rename(Config.SQLITE_DATABASE_PATH, os.path.join(
|
backup_path = try_rename(db_path, os.path.join(
|
||||||
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(Config.SQLITE_DATABASE_PATH)[-1] + '.bak'))
|
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(db_path)[-1] + '.bak'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
copy2(backup_path, Config.SQLITE_DATABASE_PATH)
|
copy2(backup_path, db_path)
|
||||||
except:
|
except:
|
||||||
copy(backup_path, Config.SQLITE_DATABASE_PATH)
|
copy(backup_path, db_path)
|
||||||
|
|
||||||
temp_path = os.path.join(
|
temp_path = os.path.join(
|
||||||
*os.path.split(Config.SQLITE_DATABASE_PATH)[:-1], 'old_arcaea_database.db')
|
*os.path.split(db_path)[:-1], 'old_arcaea_database.db')
|
||||||
if os.path.isfile(temp_path):
|
if os.path.isfile(temp_path):
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
|
|
||||||
try_rename(Config.SQLITE_DATABASE_PATH, temp_path)
|
try_rename(db_path, temp_path)
|
||||||
|
|
||||||
DatabaseInit().init()
|
init_class().init()
|
||||||
self.update_database(temp_path)
|
self.update_database(temp_path, db_path)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Success to update the file `{db_path}`.')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
f'Fail to update the file `{db_path}`.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def check_update_database(self) -> bool:
|
||||||
|
return self._check_update_database_main() and self._check_update_database_log() and self._check_update_database_main(Config.SQLITE_DATABASE_DELETED_PATH, DeletedDatabaseInit)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
|
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
|
||||||
'''更新数据库,并删除旧文件'''
|
'''更新数据库,并删除旧文件'''
|
||||||
@@ -308,19 +327,42 @@ class FileChecker:
|
|||||||
def check_song_file(self) -> bool:
|
def check_song_file(self) -> bool:
|
||||||
'''检查song有关文件并初始化缓存'''
|
'''检查song有关文件并初始化缓存'''
|
||||||
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
||||||
self.logger.info("Start to initialize song data...")
|
self.logger.info("Initialize song data...")
|
||||||
try:
|
try:
|
||||||
DownloadList.initialize_cache()
|
DownloadList.initialize_cache()
|
||||||
if not Config.SONG_FILE_HASH_PRE_CALCULATE:
|
if not Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||||
self.logger.info('Song file hash pre-calculate is disabled.')
|
self.logger.info('Song file hash pre-calculate is disabled.')
|
||||||
self.logger.info('Complete!')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(format_exc())
|
self.logger.error(format_exc())
|
||||||
self.logger.warning('Initialization error!')
|
self.logger.warning('Song data initialization error!')
|
||||||
|
f = False
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_content_bundle(self) -> bool:
|
||||||
|
'''检查 content bundle 有关文件并初始化缓存'''
|
||||||
|
f = self.check_folder(Config.CONTENT_BUNDLE_FOLDER_PATH)
|
||||||
|
self.logger.info("Initialize content bundle data...")
|
||||||
|
try:
|
||||||
|
BundleParser()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.warning('Content bundle data initialization error!')
|
||||||
|
f = False
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_world_map(self) -> bool:
|
||||||
|
'''检查 world map 有关文件并初始化缓存'''
|
||||||
|
f = self.check_folder(Config.WORLD_MAP_FOLDER_PATH)
|
||||||
|
self.logger.info("Initialize world map data...")
|
||||||
|
try:
|
||||||
|
MapParser()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.warning('World map data initialization error!')
|
||||||
f = False
|
f = False
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def check_before_run(self) -> bool:
|
def check_before_run(self) -> bool:
|
||||||
'''运行前检查,返回布尔值'''
|
'''运行前检查,返回布尔值'''
|
||||||
MemoryDatabase() # 初始化内存数据库
|
MemoryDatabase() # 初始化内存数据库
|
||||||
return self.check_song_file() & self.check_update_database()
|
return self.check_song_file() and self.check_content_bundle() and self.check_update_database() and self.check_world_map()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
from .error import DataExist, InputError, ItemNotEnough, ItemUnavailable, NoData
|
from .error import (DataExist, InputError, ItemNotEnough, ItemUnavailable,
|
||||||
|
NoData)
|
||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
@@ -89,7 +90,7 @@ class UserItem(Item):
|
|||||||
|
|
||||||
|
|
||||||
class NormalItem(UserItem):
|
class NormalItem(UserItem):
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ class NormalItem(UserItem):
|
|||||||
|
|
||||||
|
|
||||||
class PositiveItem(UserItem):
|
class PositiveItem(UserItem):
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class PositiveItem(UserItem):
|
|||||||
class ItemCore(PositiveItem):
|
class ItemCore(PositiveItem):
|
||||||
item_type = 'core'
|
item_type = 'core'
|
||||||
|
|
||||||
def __init__(self, c, core_type: str = '', amount: int = 0) -> None:
|
def __init__(self, c=None, core_type: str = '', amount: int = 0) -> None:
|
||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
self.item_id = core_type
|
self.item_id = core_type
|
||||||
@@ -195,7 +196,7 @@ class ItemCharacter(UserItem):
|
|||||||
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user.user_id, self.item_id))
|
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user.user_id, self.item_id))
|
||||||
if self.c.fetchone() == (0,):
|
if self.c.fetchone() == (0,):
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''insert into user_char values(?,?,1,0,0,0)''', (user.user_id, self.item_id))
|
'''insert into user_char values(?,?,1,0,0,0,0)''', (user.user_id, self.item_id))
|
||||||
|
|
||||||
|
|
||||||
class Memory(UserItem):
|
class Memory(UserItem):
|
||||||
@@ -220,10 +221,12 @@ class Memory(UserItem):
|
|||||||
class Fragment(UserItem):
|
class Fragment(UserItem):
|
||||||
item_type = 'fragment'
|
item_type = 'fragment'
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c=None, amount=0) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
|
self.item_id = self.item_type
|
||||||
|
self.amount = amount
|
||||||
|
|
||||||
def user_claim_item(self, user):
|
def user_claim_item(self, user):
|
||||||
pass
|
pass
|
||||||
@@ -238,12 +241,24 @@ class Anni5tix(PositiveItem):
|
|||||||
def __init__(self, c) -> None:
|
def __init__(self, c) -> None:
|
||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
|
self.item_id = self.item_type
|
||||||
|
self.amount = 1
|
||||||
|
|
||||||
|
|
||||||
|
class PickTicket(PositiveItem):
|
||||||
|
item_type = 'pick_ticket'
|
||||||
|
|
||||||
|
def __init__(self, c=None) -> None:
|
||||||
|
super().__init__(c)
|
||||||
|
self.is_available = True
|
||||||
|
self.item_id = self.item_type
|
||||||
|
self.amount = 1
|
||||||
|
|
||||||
|
|
||||||
class WorldSong(NormalItem):
|
class WorldSong(NormalItem):
|
||||||
item_type = 'world_song'
|
item_type = 'world_song'
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
|
|
||||||
@@ -293,8 +308,10 @@ class ProgBoost(UserItem):
|
|||||||
class Stamina6(UserItem):
|
class Stamina6(UserItem):
|
||||||
item_type = 'stamina6'
|
item_type = 'stamina6'
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
|
self.item_id = 'stamina6'
|
||||||
|
self.amount = 1
|
||||||
|
|
||||||
def user_claim_item(self, user):
|
def user_claim_item(self, user):
|
||||||
'''
|
'''
|
||||||
@@ -307,6 +324,23 @@ class Stamina6(UserItem):
|
|||||||
user.update_user_one_column('world_mode_locked_end_ts', -1)
|
user.update_user_one_column('world_mode_locked_end_ts', -1)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemStamina(UserItem):
|
||||||
|
item_type = 'stamina'
|
||||||
|
|
||||||
|
def __init__(self, c=None, amount=1) -> None:
|
||||||
|
super().__init__(c)
|
||||||
|
self.item_id = 'stamina'
|
||||||
|
self.amount = amount
|
||||||
|
|
||||||
|
def user_claim_item(self, user):
|
||||||
|
'''
|
||||||
|
新手任务奖励体力
|
||||||
|
'''
|
||||||
|
user.select_user_about_stamina()
|
||||||
|
user.stamina.stamina += self.amount
|
||||||
|
user.stamina.update()
|
||||||
|
|
||||||
|
|
||||||
class ItemFactory:
|
class ItemFactory:
|
||||||
def __init__(self, c=None) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
@@ -324,6 +358,8 @@ class ItemFactory:
|
|||||||
return Memory(self.c)
|
return Memory(self.c)
|
||||||
elif item_type == 'anni5tix':
|
elif item_type == 'anni5tix':
|
||||||
return Anni5tix(self.c)
|
return Anni5tix(self.c)
|
||||||
|
elif item_type == 'pick_ticket':
|
||||||
|
return PickTicket(self.c)
|
||||||
elif item_type == 'world_song':
|
elif item_type == 'world_song':
|
||||||
return WorldSong(self.c)
|
return WorldSong(self.c)
|
||||||
elif item_type == 'world_unlock':
|
elif item_type == 'world_unlock':
|
||||||
|
|||||||
@@ -1,41 +1,31 @@
|
|||||||
import socket
|
import socket
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
from json import dumps, loads
|
||||||
|
from threading import RLock
|
||||||
|
from time import time
|
||||||
|
|
||||||
from core.error import ArcError, Timeout
|
from core.error import ArcError, Timeout
|
||||||
|
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .user import UserInfo
|
from .user import UserInfo
|
||||||
|
from .util import aes_gcm_128_decrypt, aes_gcm_128_encrypt
|
||||||
|
|
||||||
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
|
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
|
||||||
|
|
||||||
|
|
||||||
def get_song_unlock(client_song_map: dict) -> bytes:
|
def get_song_unlock(client_song_map: 'dict[str, list]') -> bytes:
|
||||||
'''处理可用歌曲bit,返回bytes'''
|
'''处理可用歌曲bit,返回bytes'''
|
||||||
|
|
||||||
user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH
|
user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH
|
||||||
for i in range(0, Constant.LINKPLAY_UNLOCK_LENGTH*2, 2):
|
|
||||||
x = 0
|
|
||||||
y = 0
|
|
||||||
if str(i) in client_song_map:
|
|
||||||
if client_song_map[str(i)][0]:
|
|
||||||
x += 1
|
|
||||||
if client_song_map[str(i)][1]:
|
|
||||||
x += 2
|
|
||||||
if client_song_map[str(i)][2]:
|
|
||||||
x += 4
|
|
||||||
if client_song_map[str(i)][3]:
|
|
||||||
x += 8
|
|
||||||
if str(i+1) in client_song_map:
|
|
||||||
if client_song_map[str(i+1)][0]:
|
|
||||||
y += 1
|
|
||||||
if client_song_map[str(i+1)][1]:
|
|
||||||
y += 2
|
|
||||||
if client_song_map[str(i+1)][2]:
|
|
||||||
y += 4
|
|
||||||
if client_song_map[str(i+1)][3]:
|
|
||||||
y += 8
|
|
||||||
|
|
||||||
user_song_unlock[i // 2] = y*16 + x
|
for k, v in client_song_map.items():
|
||||||
|
for i in range(5):
|
||||||
|
if not v[i]:
|
||||||
|
continue
|
||||||
|
index = int(k) * 5 + i
|
||||||
|
if index >= Constant.LINKPLAY_UNLOCK_LENGTH * 8:
|
||||||
|
continue
|
||||||
|
user_song_unlock[index // 8] |= 1 << (index % 8)
|
||||||
|
|
||||||
return bytes(user_song_unlock)
|
return bytes(user_song_unlock)
|
||||||
|
|
||||||
@@ -50,6 +40,10 @@ class Player(UserInfo):
|
|||||||
self.__song_unlock: bytes = None
|
self.__song_unlock: bytes = None
|
||||||
self.client_song_map: dict = None
|
self.client_song_map: dict = None
|
||||||
|
|
||||||
|
self.last_match_timestamp: int = 0
|
||||||
|
self.match_times: int = None # 已匹配次数,减 1 后乘 5 就大致是匹配时间
|
||||||
|
self.match_room: Room = None # 匹配到的房间,这个仅用来在两个人同时匹配时使用,一人建房,通知另一个人加入
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'userId': self.user_id,
|
'userId': self.user_id,
|
||||||
@@ -69,6 +63,16 @@ class Player(UserInfo):
|
|||||||
self.client_song_map = client_song_map
|
self.client_song_map = client_song_map
|
||||||
self.__song_unlock = get_song_unlock(self.client_song_map)
|
self.__song_unlock = get_song_unlock(self.client_song_map)
|
||||||
|
|
||||||
|
def calc_available_chart_num(self, song_unlock: bytes) -> int:
|
||||||
|
'''计算交叠后可用谱面数量'''
|
||||||
|
new_unlock = [i & j for i, j in zip(self.song_unlock, song_unlock)]
|
||||||
|
s = 0
|
||||||
|
for i in range(len(new_unlock)):
|
||||||
|
for j in range(8):
|
||||||
|
if new_unlock[i] & (1 << j):
|
||||||
|
s += 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -77,62 +81,92 @@ class Room:
|
|||||||
|
|
||||||
self.song_unlock: bytes = None
|
self.song_unlock: bytes = None
|
||||||
|
|
||||||
|
self.share_token: str = 'abcde12345'
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'roomId': str(self.room_id),
|
'roomId': str(self.room_id),
|
||||||
'roomCode': self.room_code,
|
'roomCode': self.room_code,
|
||||||
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode()
|
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode(),
|
||||||
|
'shareToken': self.share_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RemoteMultiPlayer:
|
class RemoteMultiPlayer:
|
||||||
|
TCP_AES_KEY = Constant.LINKPLAY_TCP_SECRET_KEY.encode(
|
||||||
|
'utf-8').ljust(16, b'\x00')[:16]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.user: 'Player' = None
|
self.user: 'Player' = None
|
||||||
self.room: 'Room' = None
|
self.room: 'Room' = None
|
||||||
|
|
||||||
self.data_recv: tuple = None
|
self.data_recv: 'dict | list' = None
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return dict(self.room.to_dict(), **self.user.to_dict())
|
return dict(self.room.to_dict(), **self.user.to_dict())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tcp(data: str) -> str:
|
def tcp(data: bytes) -> bytes:
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
sock.connect((Constant.LINKPLAY_HOST,
|
sock.connect((Constant.LINKPLAY_HOST,
|
||||||
Constant.LINKPLAY_TCP_PORT))
|
Constant.LINKPLAY_TCP_PORT))
|
||||||
sock.sendall(bytes(data + "\n", "utf-8"))
|
|
||||||
|
sock.sendall(data)
|
||||||
try:
|
try:
|
||||||
received = str(sock.recv(1024), "utf-8").strip()
|
cipher_len = int.from_bytes(sock.recv(8), byteorder='little')
|
||||||
|
if cipher_len > Constant.LINKPLAY_TCP_MAX_LENGTH:
|
||||||
|
raise ArcError(
|
||||||
|
'Too long body from link play server', status=400)
|
||||||
|
iv = sock.recv(12)
|
||||||
|
tag = sock.recv(16)
|
||||||
|
ciphertext = sock.recv(cipher_len)
|
||||||
|
received = aes_gcm_128_decrypt(
|
||||||
|
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||||
except socket.timeout as e:
|
except socket.timeout as e:
|
||||||
raise Timeout(
|
raise Timeout(
|
||||||
'Timeout when waiting for data from link play server.', status=400) from e
|
'Timeout when waiting for data from link play server.', status=400) from e
|
||||||
# print(received)
|
# print(received)
|
||||||
return received
|
return received
|
||||||
|
|
||||||
def data_swap(self, data: tuple) -> tuple:
|
def data_swap(self, data: dict) -> dict:
|
||||||
|
|
||||||
received = self.tcp(Constant.LINKPLAY_AUTHENTICATION +
|
iv, ciphertext, tag = aes_gcm_128_encrypt(
|
||||||
'|' + '|'.join([str(x) for x in data]))
|
self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
|
||||||
|
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
|
||||||
|
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag + ciphertext
|
||||||
|
recv_data = self.tcp(send_data)
|
||||||
|
self.data_recv = loads(recv_data)
|
||||||
|
|
||||||
self.data_recv = received.split('|')
|
code = self.data_recv['code']
|
||||||
if self.data_recv[0] != '0':
|
if code != 0:
|
||||||
code = int(self.data_recv[0])
|
|
||||||
raise ArcError(f'Link Play error code: {code}', code, status=400)
|
raise ArcError(f'Link Play error code: {code}', code, status=400)
|
||||||
|
|
||||||
|
return self.data_recv
|
||||||
|
|
||||||
def create_room(self, user: 'Player' = None) -> None:
|
def create_room(self, user: 'Player' = None) -> None:
|
||||||
'''创建房间'''
|
'''创建房间'''
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.user = user
|
self.user = user
|
||||||
user.select_user_one_column('name')
|
user.select_user_about_link_play()
|
||||||
self.data_swap((1, self.user.name, b64encode(
|
self.data_swap({
|
||||||
self.user.song_unlock).decode('utf-8')))
|
'endpoint': 'create_room',
|
||||||
|
'data': {
|
||||||
|
'name': self.user.name,
|
||||||
|
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||||
|
'rating_ptt': self.user.rating_ptt,
|
||||||
|
'is_hide_rating': self.user.is_hide_rating,
|
||||||
|
'match_times': self.user.match_times
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
self.room = Room()
|
self.room = Room()
|
||||||
self.room.room_code = self.data_recv[1]
|
x = self.data_recv['data']
|
||||||
self.room.room_id = int(self.data_recv[2])
|
self.room.room_code = x['room_code']
|
||||||
|
self.room.room_id = int(x['room_id'])
|
||||||
self.room.song_unlock = self.user.song_unlock
|
self.room.song_unlock = self.user.song_unlock
|
||||||
self.user.token = int(self.data_recv[3])
|
self.user.token = int(x['token'])
|
||||||
self.user.key = b64decode(self.data_recv[4])
|
self.user.key = b64decode(x['key'])
|
||||||
self.user.player_id = int(self.data_recv[5])
|
self.user.player_id = int(x['player_id'])
|
||||||
|
|
||||||
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
|
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
|
||||||
'''加入房间'''
|
'''加入房间'''
|
||||||
@@ -141,24 +175,187 @@ class RemoteMultiPlayer:
|
|||||||
if room is not None:
|
if room is not None:
|
||||||
self.room = room
|
self.room = room
|
||||||
|
|
||||||
self.user.select_user_one_column('name')
|
self.user.select_user_about_link_play()
|
||||||
self.data_swap(
|
self.data_swap({
|
||||||
(2, self.user.name, b64encode(self.user.song_unlock).decode('utf-8'), room.room_code))
|
'endpoint': 'join_room',
|
||||||
self.room.room_code = self.data_recv[1]
|
'data': {
|
||||||
self.room.room_id = int(self.data_recv[2])
|
'name': self.user.name,
|
||||||
self.room.song_unlock = b64decode(self.data_recv[6])
|
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||||
self.user.token = int(self.data_recv[3])
|
'room_code': self.room.room_code,
|
||||||
self.user.key = b64decode(self.data_recv[4])
|
'rating_ptt': self.user.rating_ptt,
|
||||||
self.user.player_id = int(self.data_recv[5])
|
'is_hide_rating': self.user.is_hide_rating,
|
||||||
|
'match_times': self.user.match_times
|
||||||
|
}
|
||||||
|
})
|
||||||
|
x = self.data_recv['data']
|
||||||
|
self.room.room_code = x['room_code']
|
||||||
|
self.room.room_id = int(x['room_id'])
|
||||||
|
self.room.song_unlock = b64decode(x['song_unlock'])
|
||||||
|
self.user.token = int(x['token'])
|
||||||
|
self.user.key = b64decode(x['key'])
|
||||||
|
self.user.player_id = int(x['player_id'])
|
||||||
|
|
||||||
def update_room(self, user: 'Player' = None) -> None:
|
def update_room(self, user: 'Player' = None) -> None:
|
||||||
'''更新房间'''
|
'''更新房间'''
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.user = user
|
self.user = user
|
||||||
self.data_swap((3, self.user.token))
|
|
||||||
|
self.user.select_user_about_link_play()
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'update_room',
|
||||||
|
'data': {
|
||||||
|
'token': self.user.token,
|
||||||
|
'rating_ptt': self.user.rating_ptt,
|
||||||
|
'is_hide_rating': self.user.is_hide_rating
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
self.room = Room()
|
self.room = Room()
|
||||||
self.room.room_code = self.data_recv[1]
|
x = self.data_recv['data']
|
||||||
self.room.room_id = int(self.data_recv[2])
|
self.room.room_code = x['room_code']
|
||||||
self.room.song_unlock = b64decode(self.data_recv[5])
|
self.room.room_id = int(x['room_id'])
|
||||||
self.user.key = b64decode(self.data_recv[3])
|
self.room.song_unlock = b64decode(x['song_unlock'])
|
||||||
self.user.player_id = int(self.data_recv[4])
|
self.user.key = b64decode(x['key'])
|
||||||
|
self.user.player_id = int(x['player_id'])
|
||||||
|
|
||||||
|
def get_rooms(self, offset=0, limit=50) -> dict:
|
||||||
|
'''获取房间列表'''
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'get_rooms',
|
||||||
|
'data': {
|
||||||
|
'offset': offset,
|
||||||
|
'limit': limit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
def select_room(self, room_code: str = None, share_token: str = None) -> dict:
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'select_room',
|
||||||
|
'data': {
|
||||||
|
'room_code': room_code,
|
||||||
|
'share_token': share_token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
def get_match_rooms(self) -> dict:
|
||||||
|
'''获取一定数量的公共房间列表'''
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'get_match_rooms',
|
||||||
|
'data': {
|
||||||
|
'limit': 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
|
||||||
|
class MatchStore:
|
||||||
|
|
||||||
|
last_get_rooms_timestamp = 0
|
||||||
|
room_cache: 'list[Room]' = []
|
||||||
|
|
||||||
|
player_queue: 'dict[int, Player]' = {}
|
||||||
|
|
||||||
|
lock = RLock()
|
||||||
|
|
||||||
|
last_memory_clean_timestamp = 0
|
||||||
|
|
||||||
|
def __init__(self, c=None) -> None:
|
||||||
|
self.c = c
|
||||||
|
self.remote = RemoteMultiPlayer()
|
||||||
|
|
||||||
|
def refresh_rooms(self):
|
||||||
|
now = time()
|
||||||
|
if now - self.last_get_rooms_timestamp < Constant.LINKPLAY_MATCH_GET_ROOMS_INTERVAL:
|
||||||
|
return
|
||||||
|
MatchStore.room_cache = self.remote.get_match_rooms()['rooms']
|
||||||
|
MatchStore.last_get_rooms_timestamp = now
|
||||||
|
|
||||||
|
def init_player(self, user: 'Player'):
|
||||||
|
user.match_times = 0
|
||||||
|
MatchStore.player_queue[user.user_id] = user
|
||||||
|
user.last_match_timestamp = time()
|
||||||
|
user.c = self.c
|
||||||
|
user.select_user_about_link_play()
|
||||||
|
user.c = None
|
||||||
|
|
||||||
|
def clear_player(self, user_id: int):
|
||||||
|
MatchStore.player_queue.pop(user_id, None)
|
||||||
|
|
||||||
|
def clean_room_cache(self):
|
||||||
|
MatchStore.room_cache = []
|
||||||
|
MatchStore.last_get_rooms_timestamp = 0
|
||||||
|
|
||||||
|
def memory_clean(self):
|
||||||
|
now = time()
|
||||||
|
if now - self.last_memory_clean_timestamp < Constant.LINKPLAY_MEMORY_CLEAN_INTERVAL:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
for i in MatchStore.player_queue:
|
||||||
|
if now - i.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||||
|
self.clear_player(i)
|
||||||
|
|
||||||
|
def match(self, user_id: int):
|
||||||
|
user = MatchStore.player_queue.get(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise ArcError(
|
||||||
|
f'User `{user_id}` not found in match queue.', code=999)
|
||||||
|
|
||||||
|
if user.match_room is not None:
|
||||||
|
# 二人开新房,第二人加入
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.join_room(user.match_room, user)
|
||||||
|
self.clear_player(user_id)
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
self.refresh_rooms()
|
||||||
|
|
||||||
|
rule = min(user.match_times, len(Constant.LINKPLAY_MATCH_PTT_ABS) -
|
||||||
|
1, len(Constant.LINKPLAY_MATCH_UNLOCK_MIN) - 1)
|
||||||
|
ptt_abs = Constant.LINKPLAY_MATCH_PTT_ABS[rule]
|
||||||
|
unlock_min = Constant.LINKPLAY_MATCH_UNLOCK_MIN[rule]
|
||||||
|
|
||||||
|
# 加入已有房间
|
||||||
|
for i in MatchStore.room_cache:
|
||||||
|
f = True
|
||||||
|
num = 0
|
||||||
|
for j in i['players']:
|
||||||
|
if j['player_id'] != 0:
|
||||||
|
num += 1
|
||||||
|
if abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs:
|
||||||
|
f = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# 有玩家非正常退房时,next_state_timestamp 不为 0,有概率新玩家进不来,所以使用 num 统计玩家数量
|
||||||
|
|
||||||
|
if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0 or num == 1):
|
||||||
|
room = Room()
|
||||||
|
room.room_code = i['room_code']
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.join_room(room, user)
|
||||||
|
self.clean_room_cache()
|
||||||
|
self.clear_player(user_id)
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
now = time()
|
||||||
|
|
||||||
|
# 二人开新房,第一人开房
|
||||||
|
for p in MatchStore.player_queue.values():
|
||||||
|
if p.user_id == user_id or now - p.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||||
|
continue
|
||||||
|
new_rule = min(rule, p.match_times)
|
||||||
|
if abs(user.rating_ptt - p.rating_ptt) < Constant.LINKPLAY_MATCH_PTT_ABS[new_rule] and user.calc_available_chart_num(p.song_unlock) >= Constant.LINKPLAY_MATCH_UNLOCK_MIN[new_rule]:
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.create_room(user)
|
||||||
|
self.clear_player(user_id)
|
||||||
|
p.match_room = self.remote.room
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
user.match_times += 1
|
||||||
|
user.last_match_timestamp = now
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
240
latest version/core/mission.py
Normal file
240
latest version/core/mission.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
from .item import Fragment, ItemCore, ItemStamina, PickTicket, WorldSong
|
||||||
|
|
||||||
|
|
||||||
|
class Mission:
|
||||||
|
mission_id: str = None
|
||||||
|
items: list = []
|
||||||
|
|
||||||
|
def __init__(self, c=None):
|
||||||
|
self.c = c
|
||||||
|
self.user = None
|
||||||
|
self._status: int = None
|
||||||
|
|
||||||
|
if self.c is not None:
|
||||||
|
for i in self.items:
|
||||||
|
i.c = self.c
|
||||||
|
|
||||||
|
def to_dict(self, has_items=False) -> dict:
|
||||||
|
r = {
|
||||||
|
'mission_id': self.mission_id,
|
||||||
|
'status': self.status,
|
||||||
|
}
|
||||||
|
if has_items:
|
||||||
|
r['items'] = [x.to_dict() for x in self.items]
|
||||||
|
return r
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> str:
|
||||||
|
if self._status == 1:
|
||||||
|
return 'inprogress'
|
||||||
|
elif self._status == 2:
|
||||||
|
return 'cleared'
|
||||||
|
elif self._status == 3:
|
||||||
|
return 'prevclaimedfragmission'
|
||||||
|
elif self._status == 4:
|
||||||
|
return 'claimed'
|
||||||
|
|
||||||
|
return 'locked'
|
||||||
|
|
||||||
|
def user_claim_mission(self, user):
|
||||||
|
# param: user - User 类或子类的实例
|
||||||
|
if user is not None:
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 4)''',
|
||||||
|
(self.user.user_id, self.mission_id))
|
||||||
|
for i in self.items:
|
||||||
|
i.user_claim_item(self.user)
|
||||||
|
self._status = 4
|
||||||
|
|
||||||
|
def user_clear_mission(self, user):
|
||||||
|
# param: user - User 类或子类的实例
|
||||||
|
if user is not None:
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 2)''',
|
||||||
|
(self.user.user_id, self.mission_id))
|
||||||
|
self._status = 2
|
||||||
|
|
||||||
|
def select_user_mission(self, user):
|
||||||
|
# param: user - User 类或子类的实例
|
||||||
|
if user is not None:
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
self._status = 0
|
||||||
|
self.c.execute('''select status from user_mission where user_id=? and mission_id=?''',
|
||||||
|
(self.user.user_id, self.mission_id))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
|
||||||
|
if x and x[0]:
|
||||||
|
self._status = x[0]
|
||||||
|
|
||||||
|
|
||||||
|
class M11(Mission):
|
||||||
|
mission_id = 'mission_1_1_tutorial'
|
||||||
|
items = [Fragment(amount=10)]
|
||||||
|
|
||||||
|
|
||||||
|
class M12(Mission):
|
||||||
|
mission_id = 'mission_1_2_clearsong'
|
||||||
|
items = [Fragment(amount=10)]
|
||||||
|
|
||||||
|
|
||||||
|
class M13(Mission):
|
||||||
|
mission_id = 'mission_1_3_settings'
|
||||||
|
items = [Fragment(amount=10)]
|
||||||
|
|
||||||
|
|
||||||
|
class M14(Mission):
|
||||||
|
mission_id = 'mission_1_4_allsongsview'
|
||||||
|
items = [Fragment(amount=10)]
|
||||||
|
|
||||||
|
|
||||||
|
class M15(Mission):
|
||||||
|
mission_id = 'mission_1_5_fragunlock'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||||
|
|
||||||
|
|
||||||
|
class M1E(Mission):
|
||||||
|
mission_id = 'mission_1_end'
|
||||||
|
items = [Fragment(amount=100)]
|
||||||
|
|
||||||
|
|
||||||
|
class M21(Mission):
|
||||||
|
mission_id = 'mission_2_1_account'
|
||||||
|
items = [Fragment(amount=20)]
|
||||||
|
|
||||||
|
|
||||||
|
class M22(Mission):
|
||||||
|
mission_id = 'mission_2_2_profile'
|
||||||
|
items = [Fragment(amount=20)]
|
||||||
|
|
||||||
|
|
||||||
|
class M23(Mission):
|
||||||
|
mission_id = 'mission_2_3_partner'
|
||||||
|
items = [Fragment(amount=20)]
|
||||||
|
|
||||||
|
|
||||||
|
class M24(Mission):
|
||||||
|
mission_id = 'mission_2_4_usestamina'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||||
|
|
||||||
|
|
||||||
|
class M25(Mission):
|
||||||
|
mission_id = 'mission_2_5_prologuestart'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||||
|
|
||||||
|
|
||||||
|
class M2E(Mission):
|
||||||
|
mission_id = 'mission_2_end'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||||
|
|
||||||
|
|
||||||
|
class M31(Mission):
|
||||||
|
mission_id = 'mission_3_1_prsclear'
|
||||||
|
items = [Fragment(amount=50)]
|
||||||
|
|
||||||
|
|
||||||
|
class M32(Mission):
|
||||||
|
mission_id = 'mission_3_2_etherdrop'
|
||||||
|
items = [ItemStamina(amount=2)]
|
||||||
|
|
||||||
|
|
||||||
|
class M33(Mission):
|
||||||
|
mission_id = 'mission_3_3_step50'
|
||||||
|
items = [Fragment(amount=50)]
|
||||||
|
|
||||||
|
|
||||||
|
class M34(Mission):
|
||||||
|
mission_id = 'mission_3_4_frag60'
|
||||||
|
items = [ItemStamina(amount=2)]
|
||||||
|
|
||||||
|
|
||||||
|
class M3E(Mission):
|
||||||
|
mission_id = 'mission_3_end'
|
||||||
|
items = [ItemStamina(amount=6)]
|
||||||
|
|
||||||
|
|
||||||
|
class M41(Mission):
|
||||||
|
mission_id = 'mission_4_1_exgrade'
|
||||||
|
items = [Fragment(amount=100)]
|
||||||
|
|
||||||
|
|
||||||
|
class M42(Mission):
|
||||||
|
mission_id = 'mission_4_2_potential350'
|
||||||
|
items = [ItemStamina(amount=2)]
|
||||||
|
|
||||||
|
|
||||||
|
class M43(Mission):
|
||||||
|
mission_id = 'mission_4_3_twomaps'
|
||||||
|
items = [Fragment(amount=100)]
|
||||||
|
|
||||||
|
|
||||||
|
class M44(Mission):
|
||||||
|
mission_id = 'mission_4_4_worldsongunlock'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||||
|
|
||||||
|
|
||||||
|
class M45(Mission):
|
||||||
|
mission_id = 'mission_4_5_prologuefinish'
|
||||||
|
items = [ItemStamina(amount=2)]
|
||||||
|
|
||||||
|
|
||||||
|
_innocence = WorldSong()
|
||||||
|
_innocence.amount = 1
|
||||||
|
_innocence.item_id = 'innocence'
|
||||||
|
|
||||||
|
|
||||||
|
class M4E(Mission):
|
||||||
|
mission_id = 'mission_4_end'
|
||||||
|
items = [_innocence]
|
||||||
|
|
||||||
|
|
||||||
|
class M51(Mission):
|
||||||
|
mission_id = 'mission_5_1_songgrouping'
|
||||||
|
items = [Fragment(amount=50)]
|
||||||
|
|
||||||
|
|
||||||
|
class M52(Mission):
|
||||||
|
mission_id = 'mission_5_2_partnerlv12'
|
||||||
|
items = [Fragment(amount=250)]
|
||||||
|
|
||||||
|
|
||||||
|
class M53(Mission):
|
||||||
|
mission_id = 'mission_5_3_cores'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||||
|
|
||||||
|
|
||||||
|
class M54(Mission):
|
||||||
|
mission_id = 'mission_5_4_courseclear'
|
||||||
|
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||||
|
|
||||||
|
|
||||||
|
class M5E(Mission):
|
||||||
|
mission_id = 'mission_5_end'
|
||||||
|
items = [PickTicket()]
|
||||||
|
|
||||||
|
|
||||||
|
MISSION_DICT = {i.mission_id: i for i in Mission.__subclasses__()}
|
||||||
|
|
||||||
|
|
||||||
|
class UserMissionList:
|
||||||
|
def __init__(self, c=None, user=None):
|
||||||
|
self.c = c
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
self.missions: list = []
|
||||||
|
|
||||||
|
def select_all(self):
|
||||||
|
self.missions = []
|
||||||
|
self.c.execute('''select mission_id, status from user_mission where user_id=?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
for i in self.c.fetchall():
|
||||||
|
x = MISSION_DICT[i[0]]()
|
||||||
|
x._status = i[1]
|
||||||
|
self.missions.append(x)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_dict_list(self) -> list:
|
||||||
|
return [i.to_dict() for i in self.missions]
|
||||||
101
latest version/core/notification.py
Normal file
101
latest version/core/notification.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from .config_manager import Config
|
||||||
|
from .user import User
|
||||||
|
from .sql import Connect
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNotification:
|
||||||
|
|
||||||
|
notification_type = None
|
||||||
|
|
||||||
|
def __init__(self, c_m=None) -> None:
|
||||||
|
self.receiver = None
|
||||||
|
self.sender = None
|
||||||
|
self.timestamp = None
|
||||||
|
self.content = None
|
||||||
|
|
||||||
|
self.c_m = c_m
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
now = round(time() * 1000)
|
||||||
|
return now - self.timestamp > Config.NOTIFICATION_EXPIRE_TIME
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def insert(self):
|
||||||
|
self.receiver.select_user_one_column('mp_notification_enabled', True, bool)
|
||||||
|
if not self.receiver.mp_notification_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.c_m.execute(
|
||||||
|
'''select max(id) from notification where user_id = ?''', (self.receiver.user_id,))
|
||||||
|
x = self.c_m.fetchone()
|
||||||
|
if x is None or x[0] is None:
|
||||||
|
x = 0
|
||||||
|
else:
|
||||||
|
x = x[0] + 1
|
||||||
|
|
||||||
|
self.c_m.execute(
|
||||||
|
'''insert into notification values (?, ?, ?, ?, ?, ?, ?)''',
|
||||||
|
(self.receiver.user_id, x, self.notification_type, self.content,
|
||||||
|
self.sender.user_id, self.sender.name, self.timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomInviteNotification(BaseNotification):
|
||||||
|
|
||||||
|
notification_type = 'room_inv'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, l: list, user: User = None) -> 'RoomInviteNotification':
|
||||||
|
x = cls()
|
||||||
|
x.sender = User()
|
||||||
|
x.sender.user_id = l[2]
|
||||||
|
x.sender.name = l[3]
|
||||||
|
x.content = l[1]
|
||||||
|
x.timestamp = l[4]
|
||||||
|
x.receiver = user
|
||||||
|
return x
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_sender(cls, sender: User, receiver: User, share_token: str, c_m) -> 'RoomInviteNotification':
|
||||||
|
x = cls()
|
||||||
|
x.c_m = c_m
|
||||||
|
x.sender = sender
|
||||||
|
x.receiver = receiver
|
||||||
|
x.content = share_token
|
||||||
|
x.timestamp = round(time() * 1000)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'sender': self.sender.name,
|
||||||
|
'type': self.notification_type,
|
||||||
|
'shareToken': self.content,
|
||||||
|
'sendTs': self.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationFactory:
|
||||||
|
def __init__(self, c_m: Connect, user=None):
|
||||||
|
self.c_m = c_m
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def get_notification(self) -> 'list[BaseNotification]':
|
||||||
|
r = []
|
||||||
|
|
||||||
|
self.c_m.execute('''select type, content, sender_user_id, sender_name, timestamp from notification where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
for i in self.c_m.fetchall():
|
||||||
|
x = None
|
||||||
|
if i[0] == 'room_inv':
|
||||||
|
x = RoomInviteNotification.from_list(i, self.user)
|
||||||
|
|
||||||
|
if x is not None and not x.is_expired:
|
||||||
|
r.append(x)
|
||||||
|
self.c_m.execute(
|
||||||
|
'''delete from notification where user_id = ?''', (self.user.user_id,))
|
||||||
|
return r
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
from .bundle import BundleParser
|
||||||
|
from .constant import Constant
|
||||||
from .download import DownloadList
|
from .download import DownloadList
|
||||||
from .error import NoData
|
from .error import NoData
|
||||||
from .save import SaveData
|
from .save import SaveData
|
||||||
from .score import Score
|
from .score import Score
|
||||||
from .sql import Connect, Sql
|
from .sql import Connect, Sql
|
||||||
from .user import User
|
from .user import User
|
||||||
|
from .world import MapParser
|
||||||
|
|
||||||
|
|
||||||
class BaseOperation:
|
class BaseOperation:
|
||||||
@@ -25,6 +28,7 @@ class BaseOperation:
|
|||||||
class RefreshAllScoreRating(BaseOperation):
|
class RefreshAllScoreRating(BaseOperation):
|
||||||
'''
|
'''
|
||||||
刷新所有成绩的评分
|
刷新所有成绩的评分
|
||||||
|
包括 score_v2
|
||||||
'''
|
'''
|
||||||
_name = 'refresh_all_score_rating'
|
_name = 'refresh_all_score_rating'
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
# 但其实还是很慢
|
# 但其实还是很慢
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart''')
|
||||||
x = c.fetchall()
|
x = c.fetchall()
|
||||||
|
|
||||||
songs = [i[0] for i in x]
|
songs = [i[0] for i in x]
|
||||||
@@ -41,12 +45,12 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
||||||
|
|
||||||
for i in x:
|
for i in x:
|
||||||
for j in range(0, 4):
|
for j in range(0, 5):
|
||||||
defnum = -10 # 没在库里的全部当做定数-10
|
defnum = -10 # 没在库里的全部当做定数 -10
|
||||||
if i[j+1] is not None and i[j+1] > 0:
|
if i[j+1] is not None and i[j+1] > 0:
|
||||||
defnum = float(i[j+1]) / 10
|
defnum = float(i[j+1]) / 10
|
||||||
|
|
||||||
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
|
c.execute('''select user_id, score, shiny_perfect_count, perfect_count, near_count, miss_count from best_score where song_id=:a and difficulty=:b''', {
|
||||||
'a': i[0], 'b': j})
|
'a': i[0], 'b': j})
|
||||||
y = c.fetchall()
|
y = c.fetchall()
|
||||||
values = []
|
values = []
|
||||||
@@ -54,12 +58,46 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
for k in y:
|
for k in y:
|
||||||
ptt = Score.calculate_rating(defnum, k[1])
|
ptt = Score.calculate_rating(defnum, k[1])
|
||||||
ptt = max(ptt, 0)
|
ptt = max(ptt, 0)
|
||||||
values.append((ptt,))
|
score_v2 = Score.calculate_score_v2(
|
||||||
|
defnum, k[2], k[3], k[4], k[5])
|
||||||
|
values.append((ptt, score_v2,))
|
||||||
where_values.append((k[0], i[0], j))
|
where_values.append((k[0], i[0], j))
|
||||||
if values:
|
if values:
|
||||||
Sql(c).update_many('best_score', ['rating'], values, [
|
Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [
|
||||||
'user_id', 'song_id', 'difficulty'], where_values)
|
'user_id', 'song_id', 'difficulty'], where_values)
|
||||||
|
|
||||||
|
# 更新 recent30
|
||||||
|
song_defum: 'dict[str, list[int]]' = {}
|
||||||
|
for i in x:
|
||||||
|
song_defum[i[0]] = []
|
||||||
|
for j in range(0, 5):
|
||||||
|
defnum = -10
|
||||||
|
if i[j+1] is not None and i[j+1] > 0:
|
||||||
|
defnum = float(i[j+1]) / 10
|
||||||
|
song_defum[i[0]].append(defnum)
|
||||||
|
|
||||||
|
users = c.execute('''select user_id from user''').fetchall()
|
||||||
|
for i in users:
|
||||||
|
values = []
|
||||||
|
where_values = []
|
||||||
|
user_id = i[0]
|
||||||
|
c.execute(
|
||||||
|
'''select r_index, song_id, difficulty, score from recent30 where user_id = ?''', (user_id,))
|
||||||
|
for j in c.fetchall():
|
||||||
|
if j[1] in song_defum:
|
||||||
|
defnum = song_defum[j[1]][j[2]]
|
||||||
|
else:
|
||||||
|
defnum = -10
|
||||||
|
ptt = Score.calculate_rating(defnum, j[3])
|
||||||
|
ptt = max(ptt, 0)
|
||||||
|
|
||||||
|
values.append((ptt,))
|
||||||
|
where_values.append((user_id, j[0]))
|
||||||
|
|
||||||
|
if values:
|
||||||
|
Sql(c).update_many('recent30', ['rating'], values, [
|
||||||
|
'user_id', 'r_index'], where_values)
|
||||||
|
|
||||||
|
|
||||||
class RefreshSongFileCache(BaseOperation):
|
class RefreshSongFileCache(BaseOperation):
|
||||||
'''
|
'''
|
||||||
@@ -73,6 +111,26 @@ class RefreshSongFileCache(BaseOperation):
|
|||||||
DownloadList.initialize_cache()
|
DownloadList.initialize_cache()
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshBundleCache(BaseOperation):
|
||||||
|
'''
|
||||||
|
刷新 bundle 缓存
|
||||||
|
'''
|
||||||
|
_name = 'refresh_content_bundle_cache'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
BundleParser().re_init()
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshWorldMapCache(BaseOperation):
|
||||||
|
'''
|
||||||
|
刷新 map 缓存
|
||||||
|
'''
|
||||||
|
_name = 'refresh_world_map_cache'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
MapParser().re_init()
|
||||||
|
|
||||||
|
|
||||||
class SaveUpdateScore(BaseOperation):
|
class SaveUpdateScore(BaseOperation):
|
||||||
'''
|
'''
|
||||||
云存档更新成绩,是覆盖式更新
|
云存档更新成绩,是覆盖式更新
|
||||||
@@ -113,19 +171,24 @@ class SaveUpdateScore(BaseOperation):
|
|||||||
song_id = list(set(song_id_1 + song_id_2))
|
song_id = list(set(song_id_1 + song_id_2))
|
||||||
|
|
||||||
c.execute(
|
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)
|
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
|
||||||
x = c.fetchall()
|
x = c.fetchall()
|
||||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4], i[5]]
|
||||||
for i in x} # chart const * 10
|
for i in x} # chart const * 10
|
||||||
|
|
||||||
new_scores = []
|
new_scores = []
|
||||||
for i in save.scores_data:
|
for i in save.scores_data:
|
||||||
rating = 0
|
rating = 0
|
||||||
|
score_v2 = 0
|
||||||
if i['song_id'] in song_chart_const:
|
if i['song_id'] in song_chart_const:
|
||||||
rating = Score.calculate_rating(
|
defnum = song_chart_const[i['song_id']
|
||||||
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
][i['difficulty']] / 10
|
||||||
|
rating = Score.calculate_rating(defnum, i['score'])
|
||||||
rating = max(rating, 0)
|
rating = max(rating, 0)
|
||||||
|
|
||||||
|
score_v2 = Score.calculate_score_v2(
|
||||||
|
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
|
||||||
|
|
||||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||||
if y in clear_state:
|
if y in clear_state:
|
||||||
clear_type = clear_state[y]
|
clear_type = clear_state[y]
|
||||||
@@ -133,16 +196,16 @@ class SaveUpdateScore(BaseOperation):
|
|||||||
clear_type = 0
|
clear_type = 0
|
||||||
|
|
||||||
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||||
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2))
|
||||||
|
|
||||||
c.executemany(
|
c.executemany(
|
||||||
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||||
|
|
||||||
def _all_update(self):
|
def _all_update(self):
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart''')
|
||||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4], i[5]]
|
||||||
for i in c.fetchall()} # chart const * 10
|
for i in c.fetchall()} # chart const * 10
|
||||||
c.execute('''select user_id from user_save''')
|
c.execute('''select user_id from user_save''')
|
||||||
for y in c.fetchall():
|
for y in c.fetchall():
|
||||||
@@ -157,11 +220,16 @@ class SaveUpdateScore(BaseOperation):
|
|||||||
new_scores = []
|
new_scores = []
|
||||||
for i in save.scores_data:
|
for i in save.scores_data:
|
||||||
rating = 0
|
rating = 0
|
||||||
|
score_v2 = 0
|
||||||
if i['song_id'] in song_chart_const:
|
if i['song_id'] in song_chart_const:
|
||||||
rating = Score.calculate_rating(
|
defnum = song_chart_const[i['song_id']
|
||||||
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
][i['difficulty']] / 10
|
||||||
|
rating = Score.calculate_rating(defnum, i['score'])
|
||||||
rating = max(rating, 0)
|
rating = max(rating, 0)
|
||||||
|
|
||||||
|
score_v2 = Score.calculate_score_v2(
|
||||||
|
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
|
||||||
|
|
||||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||||
if y in clear_state:
|
if y in clear_state:
|
||||||
clear_type = clear_state[y]
|
clear_type = clear_state[y]
|
||||||
@@ -169,10 +237,10 @@ class SaveUpdateScore(BaseOperation):
|
|||||||
clear_type = 0
|
clear_type = 0
|
||||||
|
|
||||||
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||||
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2))
|
||||||
|
|
||||||
c.executemany(
|
c.executemany(
|
||||||
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||||
|
|
||||||
|
|
||||||
class UnlockUserItem(BaseOperation):
|
class UnlockUserItem(BaseOperation):
|
||||||
@@ -245,3 +313,75 @@ class UnlockUserItem(BaseOperation):
|
|||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_one_table(c, table_name, user_id):
|
||||||
|
c.execute(
|
||||||
|
f'''insert into db_deleted.{table_name} select * from {table_name} where user_id = ?''', (user_id,))
|
||||||
|
c.execute(f'''delete from {table_name} where user_id = ?''', (user_id,))
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUserScore(BaseOperation):
|
||||||
|
'''
|
||||||
|
删除单用户成绩,不包含 recent 数据
|
||||||
|
'''
|
||||||
|
_name = 'delete_user_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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
assert self.user is not None
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute('''attach database ? as db_deleted''',
|
||||||
|
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||||
|
_delete_one_table(c, 'best_score', self.user.user_id)
|
||||||
|
_delete_one_table(c, 'recent30', self.user.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteOneUser(BaseOperation):
|
||||||
|
'''
|
||||||
|
删除单用户
|
||||||
|
'''
|
||||||
|
_name = 'delete_one_user'
|
||||||
|
|
||||||
|
TABLES = ['best_score', 'recent30', 'user_char', 'user_course', 'user_item',
|
||||||
|
'user_present', 'user_redeem', 'user_role', 'user_save', 'user_world', 'user']
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
assert self.user is not None
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute('''attach database ? as db_deleted''',
|
||||||
|
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||||
|
|
||||||
|
self._clear_login(c)
|
||||||
|
self._data_save(c)
|
||||||
|
|
||||||
|
def _data_save(self, c):
|
||||||
|
c.execute(
|
||||||
|
f'''insert into db_deleted.friend select * from friend where user_id_me = ? or user_id_other = ?''', (self.user.user_id, self.user.user_id))
|
||||||
|
c.execute(f'''delete from friend where user_id_me = ? or user_id_other = ?''',
|
||||||
|
(self.user.user_id, self.user.user_id))
|
||||||
|
|
||||||
|
[_delete_one_table(c, x, self.user.user_id) for x in self.TABLES]
|
||||||
|
|
||||||
|
def _clear_login(self, c):
|
||||||
|
c.execute('''delete from login where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
c.execute('''delete from api_login where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Purchase(CollectionItemMixin):
|
|||||||
|
|
||||||
self.items: list = []
|
self.items: list = []
|
||||||
|
|
||||||
# TODO: "discount_reason": "extend"
|
# TODO: "discount_reason": extend, sale
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def price_displayed(self) -> int:
|
def price_displayed(self) -> int:
|
||||||
@@ -44,6 +44,12 @@ class Purchase(CollectionItemMixin):
|
|||||||
x.select_user_item(self.user)
|
x.select_user_item(self.user)
|
||||||
if x.amount >= 1:
|
if x.amount >= 1:
|
||||||
return 0
|
return 0
|
||||||
|
elif self.discount_reason == 'pick_ticket':
|
||||||
|
x = ItemFactory(self.c).get_item('pick_ticket')
|
||||||
|
x.item_id = 'pick_ticket'
|
||||||
|
x.select_user_item(self.user)
|
||||||
|
if x.amount >= 1:
|
||||||
|
return 0
|
||||||
return self.price
|
return self.price
|
||||||
return self.orig_price
|
return self.orig_price
|
||||||
|
|
||||||
@@ -60,7 +66,7 @@ class Purchase(CollectionItemMixin):
|
|||||||
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 not show_real_price or (self.discount_reason == 'anni5tix' and price == 0):
|
if not show_real_price or (self.discount_reason in ('anni5tix', 'pick_ticket') and price == 0):
|
||||||
r['discount_reason'] = self.discount_reason
|
r['discount_reason'] = self.discount_reason
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@@ -186,10 +192,10 @@ class Purchase(CollectionItemMixin):
|
|||||||
raise TicketNotEnough(
|
raise TicketNotEnough(
|
||||||
'The user does not have enough memories.', -6)
|
'The user does not have enough memories.', -6)
|
||||||
|
|
||||||
if not(self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to):
|
if not (self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to):
|
||||||
if price_used == 0:
|
if price_used == 0:
|
||||||
x = ItemFactory(self.c).get_item('anni5tix')
|
x = ItemFactory(self.c).get_item(self.discount_reason)
|
||||||
x.item_id = 'anni5tix'
|
x.item_id = self.discount_reason
|
||||||
x.amount = -1
|
x.amount = -1
|
||||||
x.user_claim_item(self.user)
|
x.user_claim_item(self.user)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -96,14 +96,19 @@ class SaveData:
|
|||||||
i['c'] = True
|
i['c'] = True
|
||||||
i['r'] = True
|
i['r'] = True
|
||||||
for i in self.unlocklist_data:
|
for i in self.unlocklist_data:
|
||||||
if i['unlock_key'][-3:] == '101':
|
x = i['unlock_key']
|
||||||
|
if x[-3:] == '101':
|
||||||
i['complete'] = 100
|
i['complete'] = 100
|
||||||
elif i['unlock_key'][:16] == 'aegleseeker|2|3|':
|
elif x[:16] == 'aegleseeker|2|3|':
|
||||||
i['complete'] = 10
|
i['complete'] = 10
|
||||||
elif i['unlock_key'] == 'saikyostronger|2|3|einherjar|2':
|
elif x == 'saikyostronger|2|3|einherjar|2':
|
||||||
i['complete'] = 6
|
i['complete'] = 6
|
||||||
elif i['unlock_key'] == 'saikyostronger|2|3|laqryma|2':
|
elif x == 'saikyostronger|2|3|laqryma|2':
|
||||||
i['complete'] = 3
|
i['complete'] = 3
|
||||||
|
elif x[-5:-2] == '109':
|
||||||
|
i['complete'] = 3
|
||||||
|
elif x.endswith('112'):
|
||||||
|
i['complete'] = 999
|
||||||
else:
|
else:
|
||||||
i['complete'] = 1
|
i['complete'] = 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from os import urandom
|
from os import urandom
|
||||||
|
from random import choices
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .bgtask import BGTask, logdb_execute
|
from .bgtask import BGTask, logdb_execute
|
||||||
@@ -11,7 +12,7 @@ from .item import ItemCore
|
|||||||
from .song import Chart
|
from .song import Chart
|
||||||
from .sql import Connect, Query, Sql
|
from .sql import Connect, Query, Sql
|
||||||
from .util import get_today_timestamp, md5
|
from .util import get_today_timestamp, md5
|
||||||
from .world import WorldPlay
|
from .world import BeyondWorldPlay, BreachedWorldPlay, WorldPlay
|
||||||
|
|
||||||
|
|
||||||
class Score:
|
class Score:
|
||||||
@@ -30,6 +31,7 @@ class Score:
|
|||||||
self.best_clear_type: int = None
|
self.best_clear_type: int = None
|
||||||
self.clear_type: int = None
|
self.clear_type: int = None
|
||||||
self.rating: float = None
|
self.rating: float = None
|
||||||
|
self.score_v2: float = None # for `world_rank_score` of global rank
|
||||||
|
|
||||||
def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int):
|
def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int):
|
||||||
self.score = int(score) if score is not None else 0
|
self.score = int(score) if score is not None else 0
|
||||||
@@ -93,7 +95,7 @@ class Score:
|
|||||||
'''分数有效性检查'''
|
'''分数有效性检查'''
|
||||||
if self.shiny_perfect_count < 0 or self.perfect_count < 0 or self.near_count < 0 or self.miss_count < 0 or self.score < 0 or self.time_played <= 0:
|
if self.shiny_perfect_count < 0 or self.perfect_count < 0 or self.near_count < 0 or self.miss_count < 0 or self.score < 0 or self.time_played <= 0:
|
||||||
return False
|
return False
|
||||||
if self.song.difficulty not in (0, 1, 2, 3):
|
if self.song.difficulty not in (0, 1, 2, 3, 4):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
all_note = self.all_note_count
|
all_note = self.all_note_count
|
||||||
@@ -124,12 +126,31 @@ class Score:
|
|||||||
|
|
||||||
return ptt
|
return ptt
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int) -> float:
|
||||||
|
# 计算score_v2 refer: https://www.bilibili.com/video/BV1ys421u7BY
|
||||||
|
# 谱面定数小于等于 0 视为 unranked,返回值会为 0
|
||||||
|
if not defnum or defnum <= 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
all_note = perfect_count + near_count + miss_count
|
||||||
|
if all_note == 0:
|
||||||
|
return 0
|
||||||
|
shiny_ratio = shiny_perfect_count / all_note
|
||||||
|
score_ratio = (perfect_count + near_count/2) / \
|
||||||
|
all_note + shiny_perfect_count / 10000000
|
||||||
|
acc_rating = max(0, min(shiny_ratio - 0.9, 0.095)) / 9.5 * 25
|
||||||
|
score_rating = max(0, min(score_ratio - 0.99, 0.01)) * 75
|
||||||
|
return defnum * (acc_rating + score_rating)
|
||||||
|
|
||||||
def get_rating_by_calc(self) -> float:
|
def get_rating_by_calc(self) -> float:
|
||||||
# 通过计算得到本成绩的rating
|
# 通过计算得到本成绩的 rating & score_v2
|
||||||
if not self.song.defnum:
|
if not self.song.defnum:
|
||||||
self.song.c = self.c
|
self.song.c = self.c
|
||||||
self.song.select()
|
self.song.select()
|
||||||
self.rating = self.calculate_rating(self.song.chart_const, self.score)
|
self.rating = self.calculate_rating(self.song.chart_const, self.score)
|
||||||
|
self.score_v2 = self.calculate_score_v2(
|
||||||
|
self.song.chart_const, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count)
|
||||||
return self.rating
|
return self.rating
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -181,6 +202,7 @@ class UserScore(Score):
|
|||||||
self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12])
|
self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12])
|
||||||
self.best_clear_type = int(x[11])
|
self.best_clear_type = int(x[11])
|
||||||
self.rating = float(x[13])
|
self.rating = float(x[13])
|
||||||
|
self.score_v2 = float(x[14])
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -207,7 +229,7 @@ class UserPlay(UserScore):
|
|||||||
self.submission_hash: str = None
|
self.submission_hash: str = None
|
||||||
self.beyond_gauge: int = None
|
self.beyond_gauge: int = None
|
||||||
self.unrank_flag: bool = None
|
self.unrank_flag: bool = None
|
||||||
self.first_protect_flag: bool = None
|
self.new_best_protect_flag: bool = None
|
||||||
self.ptt: 'Potential' = None
|
self.ptt: 'Potential' = None
|
||||||
|
|
||||||
self.is_world_mode: bool = None
|
self.is_world_mode: bool = None
|
||||||
@@ -223,6 +245,22 @@ class UserPlay(UserScore):
|
|||||||
self.course_play: 'CoursePlay' = None
|
self.course_play: 'CoursePlay' = None
|
||||||
|
|
||||||
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
|
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.hp_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.fever_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.rank_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.maya_gauge: int = None # 不能给 None 以外的默认值
|
||||||
|
self.nextstage_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.skill_cytusii_flag: str = None
|
||||||
|
self.skill_chinatsu_flag: str = None
|
||||||
|
self.highest_health: int = None
|
||||||
|
self.lowest_health: int = None
|
||||||
|
|
||||||
|
# room score
|
||||||
|
self.room_code: str = None
|
||||||
|
self.room_total_score: int = None
|
||||||
|
self.room_total_players: int = None
|
||||||
|
|
||||||
|
self.invasion_flag: int = None # 1: invasion_start, 2: invasion_hard
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
# 不能super
|
# 不能super
|
||||||
@@ -242,7 +280,7 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_protected(self) -> bool:
|
def is_protected(self) -> bool:
|
||||||
return self.health == -1 or int(self.score) >= 9800000 or self.first_protect_flag
|
return self.health == -1 or int(self.score) >= 9800000 or self.new_best_protect_flag
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
@@ -257,12 +295,23 @@ class UserPlay(UserScore):
|
|||||||
if songfile_hash and songfile_hash != self.song_hash:
|
if songfile_hash and songfile_hash != self.song_hash:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
|
x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{
|
||||||
|
self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
|
||||||
if self.combo_interval_bonus is not None:
|
if self.combo_interval_bonus is not None:
|
||||||
if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
|
if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
|
||||||
return False
|
return False
|
||||||
x = x + str(self.combo_interval_bonus)
|
x = x + str(self.combo_interval_bonus)
|
||||||
|
|
||||||
|
if self.hp_interval_bonus is not None and self.hp_interval_bonus < 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.fever_bonus is not None and (self.fever_bonus < 0 or self.fever_bonus > self.perfect_count * 5):
|
||||||
|
# fever 等级最高为 5
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.rank_bonus is not None and (self.rank_bonus < 0 or self.rank_bonus > 4):
|
||||||
|
return False
|
||||||
|
|
||||||
y = f'{self.user.user_id}{self.song_hash}'
|
y = f'{self.user.user_id}{self.song_hash}'
|
||||||
checksum = md5(x+md5(y))
|
checksum = md5(x+md5(y))
|
||||||
|
|
||||||
@@ -300,15 +349,28 @@ class UserPlay(UserScore):
|
|||||||
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.beyond_boost_gauge_usage = int(x[11])
|
||||||
|
self.skill_cytusii_flag = x[12]
|
||||||
|
self.skill_chinatsu_flag = x[13]
|
||||||
|
self.invasion_flag = x[14]
|
||||||
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, beyond_boost_gauge_usage: 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,
|
||||||
|
skill_cytusii_flag: str = None,
|
||||||
|
skill_chinatsu_flag: str = None
|
||||||
|
) -> 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)
|
||||||
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
|
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
|
||||||
|
self.skill_cytusii_flag = skill_cytusii_flag
|
||||||
|
self.skill_chinatsu_flag = skill_chinatsu_flag
|
||||||
if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
|
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''', {
|
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})
|
||||||
@@ -319,10 +381,6 @@ class UserPlay(UserScore):
|
|||||||
# 注意:偷懒了,没判断是否是beyond图
|
# 注意:偷懒了,没判断是否是beyond图
|
||||||
self.beyond_boost_gauge_usage = 0
|
self.beyond_boost_gauge_usage = 0
|
||||||
|
|
||||||
self.clear_play_state()
|
|
||||||
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, '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()
|
||||||
|
|
||||||
@@ -330,17 +388,26 @@ class UserPlay(UserScore):
|
|||||||
if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
|
if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
|
||||||
raise StaminaNotEnough('Stamina is not enough.')
|
raise StaminaNotEnough('Stamina is not enough.')
|
||||||
|
|
||||||
|
fatalis_stamina_multiply = 1
|
||||||
self.user.select_user_about_character()
|
self.user.select_user_about_character()
|
||||||
if not self.user.is_skill_sealed:
|
if not self.user.is_skill_sealed:
|
||||||
self.user.character.select_character_info()
|
self.user.character.select_character_info()
|
||||||
if self.user.character.skill_id_displayed == 'skill_fatalis':
|
# invasion 扔骰子
|
||||||
# 特殊判断hikari fatalis的双倍体力消耗
|
_flag = choices([0, 1, 2], [
|
||||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
max(1 - Constant.INVASION_START_WEIGHT - Constant.INVASION_HARD_WEIGHT, 0), Constant.INVASION_START_WEIGHT, Constant.INVASION_HARD_WEIGHT])[0]
|
||||||
self.stamina_multiply * 2
|
if self.user.is_insight_enabled and _flag != 0:
|
||||||
self.user.stamina.update()
|
self.invasion_flag = _flag
|
||||||
return None
|
|
||||||
|
|
||||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * self.stamina_multiply
|
elif self.user.character.skill_id_displayed == 'skill_fatalis':
|
||||||
|
# 特殊判断hikari fatalis的双倍体力消耗
|
||||||
|
fatalis_stamina_multiply = 2
|
||||||
|
|
||||||
|
self.clear_play_state()
|
||||||
|
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h,:i,:j)''', {
|
||||||
|
'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, 'h': self.skill_cytusii_flag, 'i': self.skill_chinatsu_flag, 'j': self.invasion_flag, 't': self.song_token})
|
||||||
|
|
||||||
|
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
||||||
|
self.stamina_multiply * fatalis_stamina_multiply
|
||||||
self.user.stamina.update()
|
self.user.stamina.update()
|
||||||
|
|
||||||
def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
|
def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
|
||||||
@@ -353,8 +420,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,0)''', (self.song_token, self.user.user_id, self.song.song_id,
|
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,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)
|
||||||
@@ -380,53 +447,9 @@ class UserPlay(UserScore):
|
|||||||
(self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token))
|
(self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token))
|
||||||
|
|
||||||
def clear_play_state(self) -> None:
|
def clear_play_state(self) -> None:
|
||||||
self.c.execute('''delete from songplay_token where user_id=:a ''', {
|
self.c.execute('''delete from songplay_token where user_id=:a''', {
|
||||||
'a': self.user.user_id})
|
'a': self.user.user_id})
|
||||||
|
|
||||||
def update_recent30(self) -> None:
|
|
||||||
'''更新此分数对应用户的recent30'''
|
|
||||||
old_recent_10 = self.ptt.recent_10
|
|
||||||
if self.is_protected:
|
|
||||||
old_r30 = self.ptt.r30.copy()
|
|
||||||
old_s30 = self.ptt.s30.copy()
|
|
||||||
|
|
||||||
# 寻找pop位置
|
|
||||||
songs = list(set(self.ptt.s30))
|
|
||||||
if '' in self.ptt.s30:
|
|
||||||
r30_id = 29
|
|
||||||
else:
|
|
||||||
n = len(songs)
|
|
||||||
if n >= 11:
|
|
||||||
r30_id = 29
|
|
||||||
elif self.song.song_id_difficulty not in songs and n == 10:
|
|
||||||
r30_id = 29
|
|
||||||
elif self.song.song_id_difficulty in songs and n == 10:
|
|
||||||
i = 29
|
|
||||||
while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0:
|
|
||||||
i -= 1
|
|
||||||
r30_id = i
|
|
||||||
elif self.song.song_id_difficulty not in songs and n == 9:
|
|
||||||
i = 29
|
|
||||||
while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0:
|
|
||||||
i -= 1
|
|
||||||
r30_id = i
|
|
||||||
else:
|
|
||||||
r30_id = 29
|
|
||||||
|
|
||||||
self.ptt.recent_30_update(
|
|
||||||
r30_id, self.rating, self.song.song_id_difficulty)
|
|
||||||
if self.is_protected and old_recent_10 > self.ptt.recent_10:
|
|
||||||
if self.song.song_id_difficulty in old_s30:
|
|
||||||
# 发现重复歌曲,更新到最高rating
|
|
||||||
index = old_s30.index(self.song.song_id_difficulty)
|
|
||||||
if old_r30[index] < self.rating:
|
|
||||||
old_r30[index] = self.rating
|
|
||||||
|
|
||||||
self.ptt.r30 = old_r30
|
|
||||||
self.ptt.s30 = old_s30
|
|
||||||
|
|
||||||
self.ptt.insert_recent_30()
|
|
||||||
|
|
||||||
def record_score(self) -> None:
|
def record_score(self) -> None:
|
||||||
'''向log数据库记录分数,请注意列名不同'''
|
'''向log数据库记录分数,请注意列名不同'''
|
||||||
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
|
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
|
||||||
@@ -468,23 +491,25 @@ class UserPlay(UserScore):
|
|||||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
|
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if not x:
|
if not x:
|
||||||
self.first_protect_flag = True # 初见保护
|
self.new_best_protect_flag = True # 初见保护
|
||||||
self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', {
|
self.c.execute('''insert into best_score values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating})
|
(self.user.user_id, self.song.song_id, self.song.difficulty, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count,
|
||||||
|
self.health, self.modifier, self.time_played, self.clear_type, self.clear_type, self.rating, self.score_v2))
|
||||||
self.user.update_global_rank()
|
self.user.update_global_rank()
|
||||||
else:
|
else:
|
||||||
self.first_protect_flag = False
|
self.new_best_protect_flag = False
|
||||||
if self.song_state > self.get_song_state(int(x[1])): # best状态更新
|
if self.song_state > self.get_song_state(int(x[1])): # best状态更新
|
||||||
self.c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', {
|
self.c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', {
|
||||||
'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty})
|
'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty})
|
||||||
if self.score >= int(x[0]): # best成绩更新
|
if self.score >= int(x[0]): # best成绩更新
|
||||||
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', {
|
self.new_best_protect_flag = True
|
||||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played})
|
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m, score_v2 = :n where user_id = :a and song_id = :b and difficulty = :c ''', {
|
||||||
|
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played, 'n': self.score_v2})
|
||||||
self.user.update_global_rank()
|
self.user.update_global_rank()
|
||||||
|
|
||||||
self.ptt = Potential(self.c, self.user)
|
self.ptt = Potential(self.c, self.user)
|
||||||
if not self.unrank_flag:
|
if not self.unrank_flag:
|
||||||
self.update_recent30()
|
self.ptt.r30_push_score(self)
|
||||||
|
|
||||||
# 总PTT更新
|
# 总PTT更新
|
||||||
user_rating_ptt = self.ptt.value
|
user_rating_ptt = self.ptt.value
|
||||||
@@ -495,7 +520,14 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
# 世界模式判断
|
# 世界模式判断
|
||||||
if self.is_world_mode:
|
if self.is_world_mode:
|
||||||
self.world_play = WorldPlay(self.c, self.user, self)
|
self.user.select_user_about_world_play()
|
||||||
|
self.user.current_map.select_map_info()
|
||||||
|
if self.user.current_map.is_breached:
|
||||||
|
self.world_play = BreachedWorldPlay(self.c, self.user, self)
|
||||||
|
elif self.user.current_map.is_beyond:
|
||||||
|
self.world_play = BeyondWorldPlay(self.c, self.user, self)
|
||||||
|
else:
|
||||||
|
self.world_play = WorldPlay(self.c, self.user, self)
|
||||||
self.world_play.update()
|
self.world_play.update()
|
||||||
|
|
||||||
# 课题模式判断
|
# 课题模式判断
|
||||||
@@ -514,9 +546,8 @@ class Potential:
|
|||||||
self.c = c
|
self.c = c
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.r30: 'list[float]' = None
|
self.r30_tuples: 'list[tuple[int, str, int, float]]' = None
|
||||||
self.s30: 'list[str]' = None
|
self.r30: 'list[Score]' = None
|
||||||
self.songs_selected: list = None
|
|
||||||
|
|
||||||
self.b30: list = None
|
self.b30: list = None
|
||||||
|
|
||||||
@@ -532,75 +563,120 @@ class Potential:
|
|||||||
'a': self.user.user_id})
|
'a': self.user.user_id})
|
||||||
return sum(x[0] for x in self.c.fetchall())
|
return sum(x[0] for x in self.c.fetchall())
|
||||||
|
|
||||||
def select_recent_30(self) -> None:
|
def select_recent_30_tuple(self) -> None:
|
||||||
'''获取用户recent30数据'''
|
'''获取用户recent30数据'''
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
|
'''select r_index, song_id, difficulty, rating from recent30 where user_id = ? and song_id != '' order by time_played DESC''', (self.user.user_id,))
|
||||||
x = self.c.fetchone()
|
|
||||||
if not x:
|
self.r30_tuples = self.c.fetchall()
|
||||||
raise NoData(
|
|
||||||
f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3)
|
def select_recent_30(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''select song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, time_played, clear_type, rating from recent30 where user_id = ? and song_id != '' order by time_played DESC''', (self.user.user_id,))
|
||||||
|
|
||||||
self.r30 = []
|
self.r30 = []
|
||||||
self.s30 = []
|
for x in self.c.fetchall():
|
||||||
if not x:
|
s = Score()
|
||||||
return None
|
s.song.set_chart(x[0], x[1])
|
||||||
for i in range(1, 61, 2):
|
s.set_score(*x[2:-1])
|
||||||
if x[i] is not None:
|
s.rating = x[-1]
|
||||||
self.r30.append(float(x[i]))
|
self.r30.append(s)
|
||||||
self.s30.append(x[i+1])
|
|
||||||
else:
|
|
||||||
self.r30.append(0)
|
|
||||||
self.s30.append('')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recent_10(self) -> float:
|
def recent_10(self) -> float:
|
||||||
'''获取用户recent10的总潜力值'''
|
'''获取用户recent10的总潜力值'''
|
||||||
if self.r30 is None:
|
if self.r30_tuples is None:
|
||||||
self.select_recent_30()
|
self.select_recent_30_tuple()
|
||||||
|
|
||||||
rating_sum = 0
|
max_dict = {}
|
||||||
r30, s30 = (list(t) for t in zip(
|
for x in self.r30_tuples:
|
||||||
*sorted(zip(self.r30, self.s30), reverse=True)))
|
if (x[1], x[2]) not in max_dict or max_dict[(x[1], x[2])] < x[3]:
|
||||||
|
max_dict[(x[1], x[2])] = x[3]
|
||||||
|
|
||||||
self.songs_selected = []
|
top_10_rating = sorted(max_dict.values(), reverse=True)[:10]
|
||||||
i = 0
|
return sum(top_10_rating)
|
||||||
while len(self.songs_selected) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None:
|
|
||||||
if s30[i] not in self.songs_selected:
|
|
||||||
rating_sum += r30[i]
|
|
||||||
self.songs_selected.append(s30[i])
|
|
||||||
i += 1
|
|
||||||
return rating_sum
|
|
||||||
|
|
||||||
def recent_30_to_dict_list(self) -> list:
|
def recent_30_to_dict_list(self) -> list:
|
||||||
if self.r30 is None:
|
if self.r30 is None:
|
||||||
self.select_recent_30()
|
self.select_recent_30()
|
||||||
r = []
|
|
||||||
for x, y in zip(self.s30, self.r30):
|
|
||||||
if x:
|
|
||||||
r.append({
|
|
||||||
'song_id': x[:-1],
|
|
||||||
'difficulty': int(x[-1]),
|
|
||||||
'rating': y
|
|
||||||
})
|
|
||||||
return r
|
|
||||||
|
|
||||||
def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None:
|
return [x.to_dict() for x in self.r30]
|
||||||
self.r30.pop(pop_index)
|
|
||||||
self.s30.pop(pop_index)
|
def update_one_r30(self, r_index: int, user_score: 'UserPlay | UserScore') -> None:
|
||||||
self.r30.insert(0, rating)
|
'''更新数据表中的一条数据'''
|
||||||
self.s30.insert(0, song_id_difficulty)
|
self.c.execute('''insert or replace into recent30 values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
||||||
|
(self.user.user_id, r_index, user_score.time_played, user_score.song.song_id, user_score.song.difficulty,
|
||||||
|
user_score.score, user_score.shiny_perfect_count, user_score.perfect_count, user_score.near_count, user_score.miss_count, user_score.health, user_score.modifier, user_score.clear_type, user_score.rating))
|
||||||
|
|
||||||
|
# 更新内存中的数据
|
||||||
|
x = (r_index, user_score.song.song_id,
|
||||||
|
user_score.song.difficulty, user_score.rating)
|
||||||
|
if len(self.r30_tuples) < 30:
|
||||||
|
self.r30_tuples.append(x)
|
||||||
|
return
|
||||||
|
|
||||||
def insert_recent_30(self) -> None:
|
|
||||||
'''更新r30表数据'''
|
|
||||||
sql = '''update recent30 set r0=?,song_id0=?,r1=?,song_id1=?,r2=?,song_id2=?,r3=?,song_id3=?,r4=?,song_id4=?,r5=?,song_id5=?,r6=?,song_id6=?,r7=?,song_id7=?,r8=?,song_id8=?,r9=?,song_id9=?,r10=?,song_id10=?,r11=?,song_id11=?,r12=?,song_id12=?,r13=?,song_id13=?,r14=?,song_id14=?,r15=?,song_id15=?,r16=?,song_id16=?,r17=?,song_id17=?,r18=?,song_id18=?,r19=?,song_id19=?,r20=?,song_id20=?,r21=?,song_id21=?,r22=?,song_id22=?,r23=?,song_id23=?,r24=?,song_id24=?,r25=?,song_id25=?,r26=?,song_id26=?,r27=?,song_id27=?,r28=?,song_id28=?,r29=?,song_id29=? where user_id=?'''
|
|
||||||
sql_list = []
|
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
sql_list.append(self.r30[i])
|
if self.r30_tuples[i][0] == r_index:
|
||||||
sql_list.append(self.s30[i])
|
self.r30_tuples[i] = x
|
||||||
|
break
|
||||||
|
|
||||||
sql_list.append(self.user.user_id)
|
def r30_push_score(self, user_score: 'UserPlay | UserScore') -> None:
|
||||||
|
'''根据新成绩调整 r30'''
|
||||||
|
if self.r30_tuples is None:
|
||||||
|
self.select_recent_30_tuple()
|
||||||
|
|
||||||
self.c.execute(sql, sql_list)
|
if len(self.r30_tuples) < 30:
|
||||||
|
self.update_one_r30(len(self.r30_tuples), user_score)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user_score.is_protected:
|
||||||
|
# 保护,替换最低的最旧的成绩
|
||||||
|
f_tuples = list(
|
||||||
|
filter(lambda x: x[-1] <= user_score.rating, self.r30_tuples))
|
||||||
|
f_tuples.reverse() # 从旧到新
|
||||||
|
f_tuples = sorted(f_tuples, key=lambda x: x[-1])
|
||||||
|
if not f_tuples:
|
||||||
|
# 找不到更低的成绩,不更新
|
||||||
|
return None
|
||||||
|
|
||||||
|
unique_songs: 'dict[tuple[str, int], list[tuple[int, int, float]]]' = {}
|
||||||
|
for i, x in enumerate(self.r30_tuples):
|
||||||
|
unique_songs.setdefault((x[1], x[2]), []).append((i, x[0], x[3]))
|
||||||
|
|
||||||
|
new_song = user_score.song.to_tuple()
|
||||||
|
|
||||||
|
if len(unique_songs) >= 11 or (len(unique_songs) == 10 and new_song not in unique_songs):
|
||||||
|
if user_score.is_protected:
|
||||||
|
# 保护,替换最低的最旧的成绩
|
||||||
|
self.update_one_r30(f_tuples[0][0], user_score)
|
||||||
|
else:
|
||||||
|
self.update_one_r30(self.r30_tuples[-1][0], user_score)
|
||||||
|
return None
|
||||||
|
|
||||||
|
filtered_songs = dict(filter(lambda x: len(
|
||||||
|
x[1]) > 1, unique_songs.items())) # 过滤掉只有单个成绩的
|
||||||
|
|
||||||
|
if new_song in unique_songs and new_song not in filtered_songs:
|
||||||
|
# 如果新成绩有相同谱面的唯一成绩在 r30 中,则它也应该有可能被替换
|
||||||
|
filtered_songs[new_song] = unique_songs[new_song]
|
||||||
|
|
||||||
|
if user_score.is_protected:
|
||||||
|
# 保护,替换最低的最旧的成绩,此时需在 filtered_songs 中
|
||||||
|
for x in f_tuples:
|
||||||
|
if (x[1], x[2]) in filtered_songs:
|
||||||
|
self.update_one_r30(x[0], user_score)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# 找到符合条件的最旧成绩
|
||||||
|
max_idx = -1
|
||||||
|
max_r_index = -1
|
||||||
|
for x in filtered_songs.values():
|
||||||
|
for y in x:
|
||||||
|
if y[0] > max_idx:
|
||||||
|
max_idx = y[0]
|
||||||
|
max_r_index = y[1]
|
||||||
|
|
||||||
|
self.update_one_r30(max_r_index, user_score)
|
||||||
|
|
||||||
|
|
||||||
class UserScoreList:
|
class UserScoreList:
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class Chart:
|
|||||||
'difficulty': self.difficulty,
|
'difficulty': self.difficulty,
|
||||||
'chart_const': self.chart_const
|
'chart_const': self.chart_const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_tuple(self) -> tuple:
|
||||||
|
return (self.song_id, self.difficulty)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart_const(self) -> float:
|
def chart_const(self) -> float:
|
||||||
@@ -33,7 +36,7 @@ class Chart:
|
|||||||
|
|
||||||
def select(self) -> None:
|
def select(self) -> None:
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''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, rating_etr 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:
|
||||||
if Config.ALLOW_SCORE_WITH_NO_SONG:
|
if Config.ALLOW_SCORE_WITH_NO_SONG:
|
||||||
@@ -63,19 +66,20 @@ class Song:
|
|||||||
self.song_id = x[0]
|
self.song_id = x[0]
|
||||||
self.name = x[1]
|
self.name = x[1]
|
||||||
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
|
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)]
|
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3), Chart(self.c, self.song_id, 4)]
|
||||||
self.charts[0].defnum = x[2]
|
self.charts[0].defnum = x[2]
|
||||||
self.charts[1].defnum = x[3]
|
self.charts[1].defnum = x[3]
|
||||||
self.charts[2].defnum = x[4]
|
self.charts[2].defnum = x[4]
|
||||||
self.charts[3].defnum = x[5]
|
self.charts[3].defnum = x[5]
|
||||||
|
self.charts[4].defnum = x[6]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def from_dict(self, d: dict) -> 'Song':
|
def from_dict(self, d: dict) -> 'Song':
|
||||||
self.song_id = d['song_id']
|
self.song_id = d['song_id']
|
||||||
self.name = d.get('name', '')
|
self.name = d.get('name', '')
|
||||||
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
|
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)]
|
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3), Chart(self.c, self.song_id, 4)]
|
||||||
for i in range(4):
|
for i in range(5):
|
||||||
self.charts[i].defnum = -10
|
self.charts[i].defnum = -10
|
||||||
for chart in d['charts']:
|
for chart in d['charts']:
|
||||||
self.charts[chart['difficulty']].defnum = round(
|
self.charts[chart['difficulty']].defnum = round(
|
||||||
@@ -89,11 +93,11 @@ class Song:
|
|||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
'''全部更新'''
|
'''全部更新'''
|
||||||
self.c.execute(
|
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))
|
'''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=?, rating_etr=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.charts[4].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, self.charts[4].defnum))
|
||||||
|
|
||||||
def select_exists(self, song_id: str = None) -> bool:
|
def select_exists(self, song_id: str = None) -> bool:
|
||||||
if song_id is not None:
|
if song_id is not None:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from atexit import register
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
|
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
|
||||||
from .error import ArcError, InputError
|
from .error import ArcError, InputError
|
||||||
|
from .util import parse_version
|
||||||
|
|
||||||
|
|
||||||
class Connect:
|
class Connect:
|
||||||
@@ -349,10 +350,20 @@ class Sql:
|
|||||||
|
|
||||||
class DatabaseMigrator:
|
class DatabaseMigrator:
|
||||||
|
|
||||||
|
SPECIAL_UPDATE_VERSION = {
|
||||||
|
'2.11.3.11': '_version_2_11_3_11',
|
||||||
|
'2.11.3.13': '_version_2_11_3_13'
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, c1_path: str, c2_path: str) -> None:
|
def __init__(self, c1_path: str, c2_path: str) -> None:
|
||||||
self.c1_path = c1_path
|
self.c1_path = c1_path
|
||||||
self.c2_path = c2_path
|
self.c2_path = c2_path
|
||||||
|
|
||||||
|
self.c1 = None
|
||||||
|
self.c2 = None
|
||||||
|
|
||||||
|
self.tables = Constant.DATABASE_MIGRATE_TABLES
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_one_table(c1, c2, table_name: str) -> bool:
|
def update_one_table(c1, c2, table_name: str) -> bool:
|
||||||
'''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖'''
|
'''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖'''
|
||||||
@@ -387,7 +398,7 @@ class DatabaseMigrator:
|
|||||||
c.execute('''delete from user_char_full''')
|
c.execute('''delete from user_char_full''')
|
||||||
for i in x:
|
for i in x:
|
||||||
exp = 25000 if i[1] == 30 else 10000
|
exp = 25000 if i[1] == 30 else 10000
|
||||||
c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [
|
c.executemany('''insert into user_char_full values(?,?,?,?,?,?,0)''', [
|
||||||
(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])
|
||||||
|
|
||||||
def update_database(self) -> None:
|
def update_database(self) -> None:
|
||||||
@@ -397,7 +408,11 @@ class DatabaseMigrator:
|
|||||||
'''
|
'''
|
||||||
with Connect(self.c2_path) as c2:
|
with Connect(self.c2_path) as c2:
|
||||||
with Connect(self.c1_path) as c1:
|
with Connect(self.c1_path) as c1:
|
||||||
for i in Constant.DATABASE_MIGRATE_TABLES:
|
self.c1 = c1
|
||||||
|
self.c2 = c2
|
||||||
|
self.special_update()
|
||||||
|
|
||||||
|
for i in self.tables:
|
||||||
self.update_one_table(c1, c2, i)
|
self.update_one_table(c1, c2, i)
|
||||||
|
|
||||||
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||||
@@ -405,6 +420,57 @@ class DatabaseMigrator:
|
|||||||
|
|
||||||
self.update_user_char_full(c2) # 更新user_char_full
|
self.update_user_char_full(c2) # 更新user_char_full
|
||||||
|
|
||||||
|
def special_update(self):
|
||||||
|
old_version = self.c1.execute(
|
||||||
|
'''select value from config where id = "version"''').fetchone()
|
||||||
|
new_version = self.c2.execute(
|
||||||
|
'''select value from config where id = "version"''').fetchone()
|
||||||
|
old_version = old_version[0] if old_version else '0.0.0'
|
||||||
|
new_version = new_version[0] if new_version else '0.0.0'
|
||||||
|
old_version = parse_version(old_version)
|
||||||
|
new_version = parse_version(new_version)
|
||||||
|
|
||||||
|
for k, v in self.SPECIAL_UPDATE_VERSION.items():
|
||||||
|
if old_version < parse_version(k) <= new_version:
|
||||||
|
getattr(self, v)()
|
||||||
|
|
||||||
|
def _version_2_11_3_11(self):
|
||||||
|
'''
|
||||||
|
2.11.3.11 版本特殊更新,调整 recent30 表结构
|
||||||
|
recent30 表从 (user_id: int PK, rating<index>: real, song_id<index>: text, ...) \
|
||||||
|
更改为 (user_id: int PK, r_index: int PK, time_played: int, song_id: text, difficulty: int, score: int, sp, p, n, m, hp, mod, clear_type, rating: real)
|
||||||
|
'''
|
||||||
|
|
||||||
|
self.tables = [x for x in self.tables if x != 'recent30']
|
||||||
|
|
||||||
|
x = self.c1.execute('''select * from recent30''')
|
||||||
|
sql_list = []
|
||||||
|
for i in x:
|
||||||
|
user_id = int(i[0])
|
||||||
|
for j in range(30):
|
||||||
|
rating = i[1 + j * 2]
|
||||||
|
rating = float(rating) if rating else 0
|
||||||
|
song_id_difficulty: str = i[2 + j * 2]
|
||||||
|
if song_id_difficulty:
|
||||||
|
song_id = song_id_difficulty[:-1]
|
||||||
|
difficulty = song_id_difficulty[-1]
|
||||||
|
difficulty = int(difficulty) if difficulty.isdigit() else 0
|
||||||
|
else:
|
||||||
|
song_id = ''
|
||||||
|
difficulty = 0
|
||||||
|
|
||||||
|
sql_list.append(
|
||||||
|
(user_id, j, 100-j, song_id, difficulty, rating))
|
||||||
|
|
||||||
|
self.c2.executemany(
|
||||||
|
'''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list)
|
||||||
|
|
||||||
|
def _version_2_11_3_13(self):
|
||||||
|
'''
|
||||||
|
2.11.3.13 版本特殊更新,world_rank_score 机制调整,需清空用户分数
|
||||||
|
'''
|
||||||
|
self.c1.execute('''update user set world_rank_score = 0''')
|
||||||
|
|
||||||
|
|
||||||
class LogDatabaseMigrator:
|
class LogDatabaseMigrator:
|
||||||
|
|
||||||
@@ -440,11 +506,54 @@ class MemoryDatabase:
|
|||||||
self.c.execute('''PRAGMA synchronous = 0''')
|
self.c.execute('''PRAGMA synchronous = 0''')
|
||||||
self.c.execute('''create table if not exists download_token(user_id int,
|
self.c.execute('''create table if not exists download_token(user_id int,
|
||||||
song_id text,file_name text,token text,time int,primary key(user_id, song_id, file_name));''')
|
song_id text,file_name text,token text,time int,primary key(user_id, song_id, file_name));''')
|
||||||
|
self.c.execute('''create table if not exists bundle_download_token(token text primary key,
|
||||||
|
file_path text, time int, device_id text);''')
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
||||||
|
self.c.execute('''
|
||||||
|
create table if not exists notification(
|
||||||
|
user_id int, id int,
|
||||||
|
type text, content text,
|
||||||
|
sender_user_id int, sender_name text,
|
||||||
|
timestamp int,
|
||||||
|
primary key(user_id, id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@register
|
@register
|
||||||
def atexit():
|
def atexit():
|
||||||
MemoryDatabase.conn.close()
|
MemoryDatabase.conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class UserKVTable:
|
||||||
|
'''用户键值对表'''
|
||||||
|
|
||||||
|
def __init__(self, c=None, user_id: int = None, class_name: str = None) -> None:
|
||||||
|
self.c = c
|
||||||
|
self.user_id = user_id
|
||||||
|
self.class_name = class_name
|
||||||
|
|
||||||
|
def get(self, key: str, idx: int = 0):
|
||||||
|
'''获取键值对'''
|
||||||
|
x = self.c.execute(
|
||||||
|
'''select value from user_kvdata where user_id = ? and class = ? and key = ? and idx = ?''', (self.user_id, self.class_name, key, idx)).fetchone()
|
||||||
|
return x[0] if x else None
|
||||||
|
|
||||||
|
def set(self, key: str, value, idx: int = 0) -> None:
|
||||||
|
'''设置键值对'''
|
||||||
|
self.c.execute('''insert or replace into user_kvdata values(?,?,?,?,?)''',
|
||||||
|
(self.user_id, self.class_name, key, idx, value))
|
||||||
|
|
||||||
|
def __getitem__(self, args):
|
||||||
|
if isinstance(args, tuple):
|
||||||
|
return self.get(*args)
|
||||||
|
else:
|
||||||
|
return self.get(args)
|
||||||
|
|
||||||
|
def __setitem__(self, args, value):
|
||||||
|
if isinstance(args, tuple):
|
||||||
|
self.set(args[0], value, args[1])
|
||||||
|
else:
|
||||||
|
self.set(args, value)
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
|||||||
NoData, RateLimit, UserBan)
|
NoData, RateLimit, UserBan)
|
||||||
from .item import UserItemList
|
from .item import UserItemList
|
||||||
from .limiter import ArcLimiter
|
from .limiter import ArcLimiter
|
||||||
|
from .mission import UserMissionList
|
||||||
from .score import Score
|
from .score import Score
|
||||||
from .sql import Query, Sql
|
from .sql import Query, Sql, UserKVTable
|
||||||
from .world import Map, UserMap, UserStamina
|
from .world import Map, MapParser, UserMap, UserStamina
|
||||||
|
|
||||||
|
|
||||||
def code_get_id(c, user_code: str) -> int:
|
def code_get_id(c, user_code: str) -> int:
|
||||||
@@ -46,6 +47,8 @@ class User:
|
|||||||
self.world_rank_score: int = None
|
self.world_rank_score: int = None
|
||||||
self.ban_flag = None
|
self.ban_flag = None
|
||||||
|
|
||||||
|
self.is_allow_marketing_email = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash_pwd(self) -> str:
|
def hash_pwd(self) -> str:
|
||||||
'''`password`的SHA-256值'''
|
'''`password`的SHA-256值'''
|
||||||
@@ -53,6 +56,12 @@ class User:
|
|||||||
|
|
||||||
|
|
||||||
class UserRegister(User):
|
class UserRegister(User):
|
||||||
|
|
||||||
|
limiter_ip = ArcLimiter(
|
||||||
|
Config.GAME_REGISTER_IP_RATE_LIMIT, 'game_register_ip')
|
||||||
|
limiter_device = ArcLimiter(
|
||||||
|
Config.GAME_REGISTER_DEVICE_RATE_LIMIT, 'game_register_device')
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
@@ -127,9 +136,9 @@ class UserRegister(User):
|
|||||||
|
|
||||||
def _insert_user_char(self):
|
def _insert_user_char(self):
|
||||||
# 为用户添加初始角色
|
# 为用户添加初始角色
|
||||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
|
||||||
(self.user_id, 0, 1, 0, 0, 0))
|
(self.user_id, 0, 1, 0, 0, 0))
|
||||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
|
||||||
(self.user_id, 1, 1, 0, 0, 0))
|
(self.user_id, 1, 1, 0, 0, 0))
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select character_id, max_level, is_uncapped from character''')
|
'''select character_id, max_level, is_uncapped from character''')
|
||||||
@@ -137,10 +146,16 @@ class UserRegister(User):
|
|||||||
if x:
|
if x:
|
||||||
for i in x:
|
for i in x:
|
||||||
exp = 25000 if i[1] == 30 else 10000
|
exp = 25000 if i[1] == 30 else 10000
|
||||||
self.c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
|
self.c.execute('''insert or replace into user_char_full values(?,?,?,?,?,?,0)''',
|
||||||
(self.user_id, i[0], i[1], exp, i[2], 0))
|
(self.user_id, i[0], i[1], exp, i[2], 0))
|
||||||
|
|
||||||
def register(self):
|
def register(self, device_id: str = None, ip: str = None):
|
||||||
|
if device_id is not None and not self.limiter_device.hit(device_id):
|
||||||
|
raise RateLimit(f'''Too many register attempts of device `{
|
||||||
|
device_id}`''', 124, -213)
|
||||||
|
if ip is not None and ip != '127.0.0.1' and not self.limiter_ip.hit(ip):
|
||||||
|
raise RateLimit(f'''Too many register attempts of ip `{
|
||||||
|
ip}`''', 124, -213)
|
||||||
now = int(time.time() * 1000)
|
now = int(time.time() * 1000)
|
||||||
if self.user_code is None:
|
if self.user_code is None:
|
||||||
self._build_user_code()
|
self._build_user_code()
|
||||||
@@ -149,11 +164,9 @@ class UserRegister(User):
|
|||||||
self._insert_user_char()
|
self._insert_user_char()
|
||||||
|
|
||||||
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
|
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
|
||||||
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
|
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email, is_allow_marketing_email)
|
||||||
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
|
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email, :is_allow_marketing_email)
|
||||||
''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email})
|
''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email, 'is_allow_marketing_email': self.is_allow_marketing_email})
|
||||||
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
|
||||||
'user_id': self.user_id})
|
|
||||||
|
|
||||||
|
|
||||||
class UserLogin(User):
|
class UserLogin(User):
|
||||||
@@ -306,13 +319,19 @@ 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.mp_notification_enabled = True
|
||||||
self.prog_boost: int = 0
|
self.prog_boost: int = 0
|
||||||
self.beyond_boost_gauge: float = 0
|
self.beyond_boost_gauge: float = 0
|
||||||
|
self.kanae_stored_prog: 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
|
||||||
self.current_map: 'Map' = None
|
self.current_map: 'Map' = None
|
||||||
self.stamina: 'UserStamina' = None
|
self.stamina: 'UserStamina' = None
|
||||||
|
|
||||||
|
self.insight_state: int = None
|
||||||
|
|
||||||
|
self.custom_banner = None
|
||||||
|
|
||||||
self.__cores: list = None
|
self.__cores: list = None
|
||||||
self.__packs: list = None
|
self.__packs: list = None
|
||||||
self.__singles: list = None
|
self.__singles: list = None
|
||||||
@@ -323,6 +342,12 @@ class UserInfo(User):
|
|||||||
self.curr_available_maps: list = None
|
self.curr_available_maps: list = None
|
||||||
self.__course_banners: list = None
|
self.__course_banners: list = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_insight_enabled(self) -> bool:
|
||||||
|
if self.insight_state is None:
|
||||||
|
self.select_user_one_column('insight_state', 4, int)
|
||||||
|
return self.insight_state == 3 or self.insight_state == 5
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cores(self) -> list:
|
def cores(self) -> list:
|
||||||
if self.__cores is None:
|
if self.__cores is None:
|
||||||
@@ -348,6 +373,13 @@ class UserInfo(User):
|
|||||||
|
|
||||||
return self.__packs
|
return self.__packs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pick_ticket(self) -> int:
|
||||||
|
x = UserItemList(self.c, self).select_from_type('pick_ticket')
|
||||||
|
if not x.items:
|
||||||
|
return 0
|
||||||
|
return x.items[0].amount
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def world_unlocks(self) -> list:
|
def world_unlocks(self) -> list:
|
||||||
if self.__world_unlocks is None:
|
if self.__world_unlocks is None:
|
||||||
@@ -488,7 +520,9 @@ class UserInfo(User):
|
|||||||
"settings": {
|
"settings": {
|
||||||
"favorite_character": favorite_character_id,
|
"favorite_character": favorite_character_id,
|
||||||
"is_hide_rating": self.is_hide_rating,
|
"is_hide_rating": self.is_hide_rating,
|
||||||
"max_stamina_notification_enabled": self.max_stamina_notification_enabled
|
"max_stamina_notification_enabled": self.max_stamina_notification_enabled,
|
||||||
|
"mp_notification_enabled": self.mp_notification_enabled,
|
||||||
|
"is_allow_marketing_email": self.is_allow_marketing_email,
|
||||||
},
|
},
|
||||||
"user_id": self.user_id,
|
"user_id": self.user_id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
@@ -501,6 +535,7 @@ class UserInfo(User):
|
|||||||
"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,
|
"beyond_boost_gauge": self.beyond_boost_gauge,
|
||||||
|
"kanae_stored_prog": self.kanae_stored_prog,
|
||||||
"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,
|
||||||
@@ -518,7 +553,24 @@ class UserInfo(User):
|
|||||||
'country': '',
|
'country': '',
|
||||||
'course_banners': self.course_banners,
|
'course_banners': self.course_banners,
|
||||||
'world_mode_locked_end_ts': self.world_mode_locked_end_ts,
|
'world_mode_locked_end_ts': self.world_mode_locked_end_ts,
|
||||||
'locked_char_ids': [] # [1]
|
'locked_char_ids': [], # [1]
|
||||||
|
'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(),
|
||||||
|
'pick_ticket': self.pick_ticket,
|
||||||
|
|
||||||
|
# 'custom_banner': 'online_banner_2024_06',
|
||||||
|
'custom_banner': self.custom_banner,
|
||||||
|
# 'subscription_multiplier': 114,
|
||||||
|
# 'memory_boost_ticket': 5,
|
||||||
|
'insight_state': self.insight_state, # 0~2 不可选,3 技能激活,4 未激活,5 激活可选,6 未激活可选
|
||||||
|
|
||||||
|
# 'enabled_features': [
|
||||||
|
# {
|
||||||
|
# "metadata": ["USA"],
|
||||||
|
# "feature": "paymentlink"
|
||||||
|
# }
|
||||||
|
# ],
|
||||||
|
|
||||||
|
'has_email': self.email != '',
|
||||||
}
|
}
|
||||||
|
|
||||||
def from_list(self, x: list) -> 'UserInfo':
|
def from_list(self, x: list) -> 'UserInfo':
|
||||||
@@ -559,6 +611,14 @@ class UserInfo(User):
|
|||||||
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
|
self.beyond_boost_gauge = x[35] if x[35] else 0
|
||||||
|
self.kanae_stored_prog = x[36] if x[36] else 0
|
||||||
|
|
||||||
|
self.mp_notification_enabled = x[37] == 1
|
||||||
|
|
||||||
|
self.insight_state = x[38]
|
||||||
|
|
||||||
|
self.custom_banner = x[39] if x[39] is not None else ''
|
||||||
|
self.is_allow_marketing_email = x[40] == 1
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -616,7 +676,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, beyond_boost_gauge 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, kanae_stored_prog 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)
|
||||||
@@ -630,14 +690,29 @@ class UserInfo(User):
|
|||||||
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
|
self.beyond_boost_gauge = x[8] if x[8] else 0
|
||||||
|
self.kanae_stored_prog = x[9] if x[9] else 0
|
||||||
|
|
||||||
|
def select_user_about_link_play(self) -> None:
|
||||||
|
'''
|
||||||
|
查询 user 表有关 link play 的信息
|
||||||
|
'''
|
||||||
|
self.c.execute(
|
||||||
|
'''select name, rating_ptt, is_hide_rating from user where user_id=?''', (self.user_id,))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData('No user.', 108, -3)
|
||||||
|
|
||||||
|
self.name = x[0]
|
||||||
|
self.rating_ptt = x[1]
|
||||||
|
self.is_hide_rating = x[2] == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def global_rank(self) -> int:
|
def global_rank(self) -> int:
|
||||||
'''用户世界排名,如果超过设定最大值,返回0'''
|
'''用户世界排名,如果超过设定最大值,返回0'''
|
||||||
if self.world_rank_score is None:
|
if self.world_rank_score is None:
|
||||||
self.select_user_one_column('world_rank_score', 0)
|
self.select_user_one_column('world_rank_score', 0)
|
||||||
if self.world_rank_score is None:
|
if not self.world_rank_score:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,))
|
'''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,))
|
||||||
@@ -650,40 +725,59 @@ class UserInfo(User):
|
|||||||
def update_global_rank(self) -> None:
|
def update_global_rank(self) -> None:
|
||||||
'''用户世界排名计算,有新增成绩则要更新'''
|
'''用户世界排名计算,有新增成绩则要更新'''
|
||||||
|
|
||||||
self.c.execute('''select song_id, rating_ftr, rating_byn from chart''')
|
self.c.execute(
|
||||||
x = self.c.fetchall()
|
'''
|
||||||
|
with user_scores as (
|
||||||
|
select song_id, difficulty, score_v2 from best_score where user_id = ? and difficulty in (2, 3, 4)
|
||||||
|
)
|
||||||
|
select sum(a) from(
|
||||||
|
select sum(score_v2) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0)
|
||||||
|
union
|
||||||
|
select sum(score_v2) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0)
|
||||||
|
union
|
||||||
|
select sum(score_v2) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0)
|
||||||
|
)
|
||||||
|
''',
|
||||||
|
(self.user_id,)
|
||||||
|
)
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if x[0] is None:
|
||||||
|
return
|
||||||
|
|
||||||
song_list_ftr = [self.user_id]
|
self.c.execute(
|
||||||
song_list_byn = [self.user_id]
|
'''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id))
|
||||||
for i in x:
|
self.world_rank_score = x[0]
|
||||||
if i[1] > 0:
|
|
||||||
song_list_ftr.append(i[0])
|
|
||||||
if i[2] > 0:
|
|
||||||
song_list_byn.append(i[0])
|
|
||||||
|
|
||||||
score_sum = 0
|
def update_user_world_complete_info(self) -> None:
|
||||||
if len(song_list_ftr) >= 2:
|
'''
|
||||||
|
更新用户的世界模式完成信息,包括两个部分
|
||||||
|
|
||||||
|
1. 每个章节的完成地图数量,为了 salt 技能
|
||||||
|
2. 全世界模式完成台阶数之和,为了 fatalis 技能
|
||||||
|
'''
|
||||||
|
kvd = UserKVTable(self.c, self.user_id, 'world')
|
||||||
|
|
||||||
|
for chapter_id, map_ids in MapParser.chapter_info_without_repeatable.items():
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
f'''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({','.join(['?']*(len(song_list_ftr)-1))})''', tuple(song_list_ftr))
|
f'''select map_id, curr_position from user_world where user_id = ? and map_id in ({','.join(['?']*len(map_ids))})''',
|
||||||
|
(self.user_id, *map_ids)
|
||||||
|
)
|
||||||
|
x = self.c.fetchall()
|
||||||
|
n = 0
|
||||||
|
for map_id, curr_position in x:
|
||||||
|
step_count = MapParser.world_info[map_id]['step_count']
|
||||||
|
if curr_position == step_count - 1:
|
||||||
|
n += 1
|
||||||
|
kvd['chapter_complete_count', chapter_id] = n
|
||||||
|
|
||||||
x = self.c.fetchone()
|
self.c.execute(
|
||||||
if x[0] is not None:
|
'''select sum(curr_position) + count(*) from user_world where user_id = ?''', (self.user_id,)
|
||||||
score_sum += x[0]
|
)
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if x is not None:
|
||||||
|
kvd['total_step_count'] = x[0] or 0
|
||||||
|
|
||||||
if len(song_list_byn) >= 2:
|
def select_user_one_column(self, column_name: str, default_value=None, data_type=None) -> None:
|
||||||
self.c.execute(
|
|
||||||
f'''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({','.join(['?']*(len(song_list_byn)-1))})''', tuple(song_list_byn))
|
|
||||||
|
|
||||||
x = self.c.fetchone()
|
|
||||||
if x[0] is not None:
|
|
||||||
score_sum += x[0]
|
|
||||||
|
|
||||||
self.c.execute('''update user set world_rank_score = :b where user_id = :a''', {
|
|
||||||
'a': self.user_id, 'b': score_sum})
|
|
||||||
|
|
||||||
self.world_rank_score = score_sum
|
|
||||||
|
|
||||||
def select_user_one_column(self, column_name: str, default_value=None) -> None:
|
|
||||||
'''
|
'''
|
||||||
查询user表的某个属性
|
查询user表的某个属性
|
||||||
请注意必须是一个普通属性,不能是一个类的实例
|
请注意必须是一个普通属性,不能是一个类的实例
|
||||||
@@ -696,7 +790,12 @@ class UserInfo(User):
|
|||||||
if not x:
|
if not x:
|
||||||
raise NoData('No user.', 108, -3)
|
raise NoData('No user.', 108, -3)
|
||||||
|
|
||||||
self.__dict__[column_name] = x[0] if x[0] else default_value
|
data = x[0] if x[0] is not None else default_value
|
||||||
|
|
||||||
|
if data_type is not None:
|
||||||
|
data = data_type(data)
|
||||||
|
|
||||||
|
self.__dict__[column_name] = data
|
||||||
|
|
||||||
def update_user_one_column(self, column_name: str, value=None) -> None:
|
def update_user_one_column(self, column_name: str, value=None) -> None:
|
||||||
'''
|
'''
|
||||||
@@ -754,6 +853,19 @@ class UserOnline(UserInfo):
|
|||||||
self.c.execute('''update user set favorite_character = :a where user_id = :b''',
|
self.c.execute('''update user set favorite_character = :a where user_id = :b''',
|
||||||
{'a': self.favorite_character.character_id, 'b': self.user_id})
|
{'a': self.favorite_character.character_id, 'b': self.user_id})
|
||||||
|
|
||||||
|
def toggle_invasion(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''select insight_state from user where user_id = ?''', (self.user_id,))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData('No user.', 108, -3)
|
||||||
|
self.insight_state = x[0]
|
||||||
|
rq = Constant.INSIGHT_TOGGLE_STATES
|
||||||
|
self.insight_state = rq[(rq.index(self.insight_state) + 1) % len(rq)]
|
||||||
|
|
||||||
|
self.c.execute(
|
||||||
|
'''update user set insight_state = ? where user_id = ?''', (self.insight_state, self.user_id))
|
||||||
|
|
||||||
|
|
||||||
class UserChanger(UserInfo, UserRegister):
|
class UserChanger(UserInfo, UserRegister):
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ import os
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from time import mktime
|
from time import mktime
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
|
||||||
|
def aes_gcm_128_encrypt(key, plaintext, associated_data):
|
||||||
|
iv = os.urandom(12)
|
||||||
|
encryptor = Cipher(
|
||||||
|
algorithms.AES(key),
|
||||||
|
modes.GCM(iv, min_tag_length=16),
|
||||||
|
).encryptor()
|
||||||
|
encryptor.authenticate_additional_data(associated_data)
|
||||||
|
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||||
|
return (iv, ciphertext, encryptor.tag)
|
||||||
|
|
||||||
|
|
||||||
|
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
|
||||||
|
decryptor = Cipher(
|
||||||
|
algorithms.AES(key),
|
||||||
|
modes.GCM(iv, tag, min_tag_length=16),
|
||||||
|
).decryptor()
|
||||||
|
decryptor.authenticate_additional_data(associated_data)
|
||||||
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
|
||||||
|
|
||||||
def md5(code: str) -> str:
|
def md5(code: str) -> str:
|
||||||
# md5加密算法
|
# md5加密算法
|
||||||
@@ -44,3 +66,9 @@ def try_rename(path: str, new_path: str) -> str:
|
|||||||
def get_today_timestamp():
|
def get_today_timestamp():
|
||||||
'''相对于本机本地时间的今天0点的时间戳'''
|
'''相对于本机本地时间的今天0点的时间戳'''
|
||||||
return int(mktime(date.today().timetuple()))
|
return int(mktime(date.today().timetuple()))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(s: str) -> 'list[int]':
|
||||||
|
'''解析版本号'''
|
||||||
|
s_number = "".join(x for x in s if x.isdigit() or x == '.')
|
||||||
|
return list(map(int, [x for x in s_number.split('.') if x != '']))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
5
latest version/database/bundle/README.md
Normal file
5
latest version/database/bundle/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## Arcaea Bundle Folder
|
||||||
|
|
||||||
|
[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler)
|
||||||
|
|
||||||
|
There must be pairs of bundle file and JSON metadata file with the same name. The bundle file's suffix is `.cb` and the metadata file's suffix is `.json`.
|
||||||
@@ -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', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)']
|
'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', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe', 'Forlorn(Ascendant - 6th Seeker)', 'Chinatsu', 'Tsumugi', 'Nai', 'Selene Sheryl (MIR-203)', 'Salt', 'Acid', 'Hikari & Selene Sheryl (Fracture & MIR-203)', 'Hikari & El Clear', 'Tairitsu & El Fail', 'Nami & Sui (Twilight)', 'Nonoka (Azure)', 'Vita (Wanderer)', 'Hikari & Tairitsu[ANS] (Breakthrough)', 'Hikari & Tairitsu[ANS] (Debut)', 'VIIM', 'Helena', 'Yuno', 'Shirahime (Flow)']
|
||||||
|
|
||||||
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', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
|
'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_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid', 'skill_hikari_selene', 'skill_hikari_clear', 'skill_tairitsu_fail', 'skill_nami_sui', 'skill_nonoka', 'skill_vita_arc', '', 'skill_hikari_tairitsu_debut', 'skill_hp_slow_drain', 'skill_hprate_based_on_hp', 'skill_lost_to_85', 'skill_frag_doubled_after_earning_X']
|
||||||
|
|
||||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', '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_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_maya_uncap', '', 'skill_luin_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_nonoka_uncap', '', '', '', '', '', '', '']
|
||||||
|
|
||||||
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, 0, 0, 0, 0, 0, 0, 0]
|
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40]
|
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, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50, 38, 22, 63, 37, 23, 59, 45, 20, 43, 50, 22, 37, 26, 47, 34, 45, 23, 47, 42, 70, 41]
|
||||||
|
|
||||||
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, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40]
|
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, 26, 35, 40, 33, 58, 31, 40, 50, 45, 41, 12, 31, 72, 40, 16, 33, 35, 23, 24, 46, 26, 49, 32, 35, 43, 45, 62, 77, 42, 44, 33]
|
||||||
|
|
||||||
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, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40]
|
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, 29, 42.5, 40, 33, 58, 31, 35, 34, 45, 41, 12, 19, 38, 40, 26, 39, 56, 20, 25, 46, 18, 71, 29, 25, 73, 45, 62, 47, 71, 44, 11]
|
||||||
|
|
||||||
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, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50]
|
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, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 115, 70, 50, 61.6, 48, 37, 90, 60, 50, 92, 66, 44, 79, 50, 47, 55, 49, 79, 52, 90, 35, 68, 63, 103, 65]
|
||||||
|
|
||||||
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, 75, 100, 60, 50, 68, 51, 50, 53, 85]
|
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, 50, 53, 85, 58, 96, 47, 80, 80, 67, 41, 55, 50, 103, 66, 35, 52, 65, 50, 43, 84, 55, 73, 59, 60, 64, 67, 94, 110, 63, 65, 58]
|
||||||
|
|
||||||
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, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 60]
|
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, 54, 65.5, 59.5, 58, 96, 47, 75, 54, 90, 41, 34, 30, 55, 66, 55, 62, 81, 44, 46, 84, 39, 105, 55, 43, 110, 50, 94, 68, 105, 65, 13]
|
||||||
|
|
||||||
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, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50]
|
65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 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, 90, 80, 50, 51, 64, 100, 50, 58, 51, 50, 115, 80, 50, 61.6, 48, 37, 90, 60, 50, 102, 76, 44, 89, 50, 47, 55, 49, 89, 52, 90, 35, 68, 63, 103, 65]
|
||||||
|
|
||||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 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, 75, 100, 60, 50, 68, 51, 60, 53, 85]
|
80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 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, 60, 53, 85, 58, 96, 57, 80, 90, 67, 41, 55, 50, 103, 66, 35, 62, 75, 50, 53, 84, 55, 73, 59, 70, 64, 67, 94, 110, 63, 65, 58]
|
||||||
|
|
||||||
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, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 60]
|
56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 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, 64, 65.5, 59.5, 58, 96, 57, 75, 64, 90, 41, 34, 30, 55, 66, 55, 72, 91, 44, 56, 84, 39, 105, 55, 53, 110, 50, 94, 68, 105, 65, 13]
|
||||||
|
|
||||||
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, 2, 0, 0, 2, 0, 2, 2, 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, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 4, 2, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 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}],
|
||||||
@@ -60,19 +60,29 @@ class InitData:
|
|||||||
19: [{'core_id': 'core_colorful', 'amount': 30}],
|
19: [{'core_id': 'core_colorful', 'amount': 30}],
|
||||||
10: [{'core_id': 'core_umbral', 'amount': 30}],
|
10: [{'core_id': 'core_umbral', 'amount': 30}],
|
||||||
66: [{'core_id': 'core_chunithm', 'amount': 15}],
|
66: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
5: [{'core_id': 'core_hollow', 'amount': 0}]
|
5: [{'core_id': 'core_hollow', 'amount': 0}],
|
||||||
|
73: [{'core_id': 'core_wacca', 'amount': 15}],
|
||||||
|
30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}],
|
||||||
|
34: [{'core_id': 'core_tanoc', 'amount': 15}],
|
||||||
|
23: [{'core_id': 'core_desolate', 'amount': 5}, {'core_id': 'core_serene', 'amount': 25}],
|
||||||
|
81: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
82: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
84: [{'core_id': 'core_maimai', 'amount': 15}],
|
||||||
|
89: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_alluring', 'amount': 25}],
|
||||||
|
71: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
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_umbral']
|
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene', 'core_maimai', 'core_alluring']
|
||||||
|
|
||||||
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', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris']
|
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy', 'hailstone', 'furetemitai', 'prayer', 'astralexe', 'trpno', 'blackmirror', 'tau', 'snowwhite3', 'altale3', 'energysynergymatrix3', 'anokumene3', 'nhelv3', 'wontbackdown', 'someday', 'disintegration', 'acheron', 'rostpagegene', 'awakening', 'mod', 'particle', 'paradoxpalette', 'xxium', 'tabootears', 'undefined', 'mask']
|
||||||
|
|
||||||
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", "scenery_chap8", "scenery_beyond"]
|
||||||
|
|
||||||
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
||||||
|
# TODO: online_banners
|
||||||
|
|
||||||
role = ['system', 'admin', 'user', 'selecter']
|
role = ['system', 'admin', 'user', 'selecter']
|
||||||
role_caption = ['系统', '管理员', '用户', '查询接口']
|
role_caption = ['系统', '管理员', '用户', '查询接口']
|
||||||
|
|||||||
@@ -445,5 +445,50 @@
|
|||||||
"core_generic_20",
|
"core_generic_20",
|
||||||
"course_banner_11"
|
"course_banner_11"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"course_id": "6.0-dan-12",
|
||||||
|
"style": 12,
|
||||||
|
"songs": [
|
||||||
|
{
|
||||||
|
"flag_as_hidden": true,
|
||||||
|
"difficulty": 2,
|
||||||
|
"id": "abstrusedilemma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flag_as_hidden": true,
|
||||||
|
"difficulty": 3,
|
||||||
|
"id": "worldender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flag_as_hidden": true,
|
||||||
|
"difficulty": 4,
|
||||||
|
"id": "alterego"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flag_as_hidden": true,
|
||||||
|
"difficulty": 3,
|
||||||
|
"id": "designant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gauge_requirement": "default",
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"value": "4.0-dan-11",
|
||||||
|
"type": "course"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flag_as_hidden_when_requirements_not_met": true,
|
||||||
|
"dan_name": "Phase 12",
|
||||||
|
"course_name": "侵蚀万物的色彩",
|
||||||
|
"can_start": true,
|
||||||
|
"is_completed": false,
|
||||||
|
"high_score": 0,
|
||||||
|
"best_clear_type": -1,
|
||||||
|
"rewards": [
|
||||||
|
"fragment2000",
|
||||||
|
"core_generic_20",
|
||||||
|
"course_banner_12"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -682,5 +682,419 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 500,
|
"orig_price": 500,
|
||||||
"price": 500
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cytusii",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "cytusii",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cytusii_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "cytusii_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eden",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "eden",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eden_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "eden_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eden_append_2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "eden_append_2",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 3,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 300,
|
||||||
|
"price": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wacca_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "wacca_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extend_3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "extend_3",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 7,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"price": 700,
|
||||||
|
"orig_price": 700
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "groovecoaster_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "groovecoaster_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 3,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"price": 300,
|
||||||
|
"orig_price": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nihil",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "nihil",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rotaeno",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "rotaeno",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ongeki_append_2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "ongeki_append_2",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 4,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 400,
|
||||||
|
"price": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lephon",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "lephon",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chunithm_append_3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "chunithm_append_3",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 4,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 400,
|
||||||
|
"price": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maimai_append_2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "maimai_append_2",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 4,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 400,
|
||||||
|
"price": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "undertale",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "undertale",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "djmax",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "djmax",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "djmax_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "djmax_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "anima",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "anima",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "anima_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "anima_append_1",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eclipse",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "eclipse",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extend_4",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "extend_4",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 7,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 700,
|
||||||
|
"price": 700
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nextstage",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "nextstage",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "megarex",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "megarex",
|
||||||
|
"type": "pack",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1468,5 +1468,779 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "internetyamero",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "internetyamero",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sacrifice",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "sacrifice",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tothefurthestdream",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "tothefurthestdream",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bbkkbkk",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "bbkkbkk",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wishuponasnow",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "wishuponasnow",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "overdrive",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "overdrive",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lightmyway",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "lightmyway",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lunarossa",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "lunarossa",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ionostream",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "ionostream",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "masqueradelegion",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "masqueradelegion",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kyorenromance",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "kyorenromance",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "qovat",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "qovat",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mirinae",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "mirinae",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ultradiaxon",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "ultradiaxon",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "distortedfate",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "distortedfate",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "floatingworld",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "floatingworld",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chromafill",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "chromafill",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backtobasics",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "backtobasics",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tasogare",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "tasogare",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "beautifuldreamer",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "beautifuldreamer",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hypervision",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "hypervision",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stargateextreme",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "stargateextreme",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shrink",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "shrink",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rainofconflict",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "rainofconflict",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gensounosatellite",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "gensounosatellite",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "thirdsun",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "thirdsun",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crimsonquartz",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "crimsonquartz",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "diesirae",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "diesirae",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spiritdauntless",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "spiritdauntless",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "placebobattler",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "placebobattler",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "megalovaniarmx",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "megalovaniarmx",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shinchoku",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "shinchoku",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dullblade",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "dullblade",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tictactoe",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "tictactoe",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dontdie",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "dontdie",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "miles",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "miles",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "eternitybreak",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "eternitybreak",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "xterfusion",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "xterfusion",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "angelsboundary",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "angelsboundary",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "grandspell",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "grandspell",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paranoidarc",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "paranoidarc",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "subsphere",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "subsphere",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "theraft",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "theraft",
|
||||||
|
"type": "single",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_generic",
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -34,7 +34,12 @@ 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
|
beyond_boost_gauge real default 0,
|
||||||
|
kanae_stored_prog real default 0,
|
||||||
|
mp_notification_enabled int default 1,
|
||||||
|
insight_state default 4, -- 这里写 4 是为了避免不能切换状态和无故被侵入
|
||||||
|
custom_banner text,
|
||||||
|
is_allow_marketing_email int 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,
|
||||||
@@ -60,7 +65,8 @@ modifier int,
|
|||||||
time_played int,
|
time_played int,
|
||||||
best_clear_type int,
|
best_clear_type int,
|
||||||
clear_type int,
|
clear_type int,
|
||||||
rating real,
|
rating real default 0,
|
||||||
|
score_v2 real default 0,
|
||||||
primary key(user_id, song_id, difficulty)
|
primary key(user_id, song_id, difficulty)
|
||||||
);
|
);
|
||||||
create table if not exists user_char(user_id int,
|
create table if not exists user_char(user_id int,
|
||||||
@@ -69,6 +75,7 @@ level int,
|
|||||||
exp real,
|
exp real,
|
||||||
is_uncapped int,
|
is_uncapped int,
|
||||||
is_uncapped_override int,
|
is_uncapped_override int,
|
||||||
|
skill_flag int,
|
||||||
primary key(user_id, character_id)
|
primary key(user_id, character_id)
|
||||||
);
|
);
|
||||||
create table if not exists user_char_full(user_id int,
|
create table if not exists user_char_full(user_id int,
|
||||||
@@ -77,6 +84,7 @@ level int,
|
|||||||
exp real,
|
exp real,
|
||||||
is_uncapped int,
|
is_uncapped int,
|
||||||
is_uncapped_override int,
|
is_uncapped_override int,
|
||||||
|
skill_flag int,
|
||||||
primary key(user_id, character_id)
|
primary key(user_id, character_id)
|
||||||
);
|
);
|
||||||
create table if not exists character(character_id int primary key,
|
create table if not exists character(character_id int primary key,
|
||||||
@@ -104,67 +112,22 @@ type text,
|
|||||||
amount int,
|
amount int,
|
||||||
primary key(character_id, item_id, type)
|
primary key(character_id, item_id, type)
|
||||||
);
|
);
|
||||||
create table if not exists recent30(user_id int primary key,
|
create table if not exists recent30(
|
||||||
r0 real,
|
user_id int,
|
||||||
song_id0 text,
|
r_index int,
|
||||||
r1 real,
|
time_played int,
|
||||||
song_id1 text,
|
song_id text,
|
||||||
r2 real,
|
difficulty int,
|
||||||
song_id2 text,
|
score int default 0,
|
||||||
r3 real,
|
shiny_perfect_count int default 0,
|
||||||
song_id3 text,
|
perfect_count int default 0,
|
||||||
r4 real,
|
near_count int default 0,
|
||||||
song_id4 text,
|
miss_count int default 0,
|
||||||
r5 real,
|
health int default 0,
|
||||||
song_id5 text,
|
modifier int default 0,
|
||||||
r6 real,
|
clear_type int default 0,
|
||||||
song_id6 text,
|
rating real default 0,
|
||||||
r7 real,
|
primary key(user_id, r_index)
|
||||||
song_id7 text,
|
|
||||||
r8 real,
|
|
||||||
song_id8 text,
|
|
||||||
r9 real,
|
|
||||||
song_id9 text,
|
|
||||||
r10 real,
|
|
||||||
song_id10 text,
|
|
||||||
r11 real,
|
|
||||||
song_id11 text,
|
|
||||||
r12 real,
|
|
||||||
song_id12 text,
|
|
||||||
r13 real,
|
|
||||||
song_id13 text,
|
|
||||||
r14 real,
|
|
||||||
song_id14 text,
|
|
||||||
r15 real,
|
|
||||||
song_id15 text,
|
|
||||||
r16 real,
|
|
||||||
song_id16 text,
|
|
||||||
r17 real,
|
|
||||||
song_id17 text,
|
|
||||||
r18 real,
|
|
||||||
song_id18 text,
|
|
||||||
r19 real,
|
|
||||||
song_id19 text,
|
|
||||||
r20 real,
|
|
||||||
song_id20 text,
|
|
||||||
r21 real,
|
|
||||||
song_id21 text,
|
|
||||||
r22 real,
|
|
||||||
song_id22 text,
|
|
||||||
r23 real,
|
|
||||||
song_id23 text,
|
|
||||||
r24 real,
|
|
||||||
song_id24 text,
|
|
||||||
r25 real,
|
|
||||||
song_id25 text,
|
|
||||||
r26 real,
|
|
||||||
song_id26 text,
|
|
||||||
r27 real,
|
|
||||||
song_id27 text,
|
|
||||||
r28 real,
|
|
||||||
song_id28 text,
|
|
||||||
r29 real,
|
|
||||||
song_id29 text
|
|
||||||
);
|
);
|
||||||
create table if not exists user_world(user_id int,
|
create table if not exists user_world(user_id int,
|
||||||
map_id text,
|
map_id text,
|
||||||
@@ -184,7 +147,10 @@ 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
|
beyond_boost_gauge_usage int,
|
||||||
|
skill_cytusii_flag text,
|
||||||
|
skill_chinatsu_flag text,
|
||||||
|
invasion_flag int
|
||||||
);
|
);
|
||||||
create table if not exists item(item_id text,
|
create table if not exists item(item_id text,
|
||||||
type text,
|
type text,
|
||||||
@@ -237,10 +203,11 @@ primary key(present_id, item_id, type)
|
|||||||
);
|
);
|
||||||
create table if not exists chart(song_id text primary key,
|
create table if not exists chart(song_id text primary key,
|
||||||
name text,
|
name text,
|
||||||
rating_pst int,
|
rating_pst int default -1,
|
||||||
rating_prs int,
|
rating_prs int default -1,
|
||||||
rating_ftr int,
|
rating_ftr int default -1,
|
||||||
rating_byn int
|
rating_byn int default -1,
|
||||||
|
rating_etr int default -1
|
||||||
);
|
);
|
||||||
create table if not exists redeem(code text primary key,
|
create table if not exists redeem(code text primary key,
|
||||||
type int
|
type int
|
||||||
@@ -307,6 +274,23 @@ type text,
|
|||||||
amount int,
|
amount int,
|
||||||
primary key(course_id, item_id, type)
|
primary key(course_id, item_id, type)
|
||||||
);
|
);
|
||||||
|
create table if not exists user_mission(
|
||||||
|
user_id int,
|
||||||
|
mission_id text,
|
||||||
|
status int,
|
||||||
|
primary key(user_id, mission_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- value 无类型
|
||||||
|
create table if not exists user_kvdata(
|
||||||
|
user_id int,
|
||||||
|
class text,
|
||||||
|
key text,
|
||||||
|
idx int,
|
||||||
|
value,
|
||||||
|
primary key(user_id, class, key, idx)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
create index if not exists best_score_1 on best_score (song_id, difficulty);
|
create index if not exists best_score_1 on best_score (song_id, difficulty);
|
||||||
|
|
||||||
|
|||||||
80
latest version/database/map/byd_inkarusi.json
Normal file
80
latest version/database/map/byd_inkarusi.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"is_legacy": false,
|
||||||
|
"character_affinity": [],
|
||||||
|
"affinity_multiplier": [],
|
||||||
|
"chapter": 2001,
|
||||||
|
"available_from": -1,
|
||||||
|
"available_to": 9999999999999,
|
||||||
|
"is_repeatable": false,
|
||||||
|
"require_id": "inkarusi2",
|
||||||
|
"require_type": "chart_unlock",
|
||||||
|
"is_breached": true,
|
||||||
|
"stamina_cost": 3,
|
||||||
|
"new_law": "over100_step50",
|
||||||
|
"disable_over": true,
|
||||||
|
"coordinate": "500,0",
|
||||||
|
"beyond_health": 200,
|
||||||
|
"step_count": 5,
|
||||||
|
"custom_bg": "",
|
||||||
|
"is_beyond": true,
|
||||||
|
"requires": [
|
||||||
|
{
|
||||||
|
"type": "chart_unlock",
|
||||||
|
"id": "inkarusi2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"position": 0,
|
||||||
|
"capture": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"position": 1,
|
||||||
|
"capture": 50,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"id": "core_generic",
|
||||||
|
"amount": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"position": 2,
|
||||||
|
"capture": 50,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "fragment",
|
||||||
|
"amount": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"position": 3,
|
||||||
|
"capture": 50,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"id": "core_generic",
|
||||||
|
"amount": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map_id": "byd_inkarusi",
|
||||||
|
"position": 4,
|
||||||
|
"capture": 0,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "inkarusi3",
|
||||||
|
"type": "world_song"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +1,27 @@
|
|||||||
{
|
{
|
||||||
"songs": [
|
"songs": [
|
||||||
{
|
|
||||||
"id": "dement",
|
|
||||||
"difficulties": [
|
|
||||||
{
|
{
|
||||||
"ratingClass": 3
|
"id": "dement",
|
||||||
|
"set": "base",
|
||||||
|
"purchase": "",
|
||||||
|
"difficulties": [
|
||||||
|
{
|
||||||
|
"ratingClass": 0,
|
||||||
|
"rating": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 1,
|
||||||
|
"rating": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 2,
|
||||||
|
"rating": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 3,
|
||||||
|
"rating": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ def encrypt(key, plaintext, associated_data):
|
|||||||
iv = urandom(12)
|
iv = urandom(12)
|
||||||
encryptor = Cipher(
|
encryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, min_tag_length=12),
|
modes.GCM(iv, min_tag_length=16),
|
||||||
).encryptor()
|
).encryptor()
|
||||||
encryptor.authenticate_additional_data(associated_data)
|
encryptor.authenticate_additional_data(associated_data)
|
||||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||||
@@ -18,7 +18,7 @@ def encrypt(key, plaintext, associated_data):
|
|||||||
def decrypt(key, associated_data, iv, ciphertext, tag):
|
def decrypt(key, associated_data, iv, ciphertext, tag):
|
||||||
decryptor = Cipher(
|
decryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, tag, min_tag_length=12),
|
modes.GCM(iv, tag, min_tag_length=16),
|
||||||
).decryptor()
|
).decryptor()
|
||||||
decryptor.authenticate_additional_data(associated_data)
|
decryptor.authenticate_additional_data(associated_data)
|
||||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
|||||||
@@ -4,26 +4,36 @@ class Config:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
'''
|
'''
|
||||||
服务器地址、端口号、校验码
|
服务器地址、端口号、校验码、传输加密密钥
|
||||||
Server address, port and verification code
|
Server address, port, verification code, and encryption key
|
||||||
'''
|
'''
|
||||||
HOST = '0.0.0.0'
|
HOST = '0.0.0.0'
|
||||||
UDP_PORT = 10900
|
UDP_PORT = 10900
|
||||||
TCP_PORT = 10901
|
TCP_PORT = 10901
|
||||||
AUTHENTICATION = 'my_link_play_server'
|
AUTHENTICATION = 'my_link_play_server'
|
||||||
|
TCP_SECRET_KEY = '1145141919810'
|
||||||
'''
|
'''
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||||
|
|
||||||
TIME_LIMIT = 3600000
|
TIME_LIMIT = 3600000
|
||||||
|
|
||||||
COMMAND_INTERVAL = 1000000
|
COMMAND_INTERVAL = 1000000
|
||||||
|
|
||||||
COUNTDOWM_TIME = 3999
|
|
||||||
|
|
||||||
PLAYER_PRE_TIMEOUT = 3000000
|
PLAYER_PRE_TIMEOUT = 3000000
|
||||||
PLAYER_TIMEOUT = 20000000
|
PLAYER_TIMEOUT = 15000000
|
||||||
|
|
||||||
LINK_PLAY_UNLOCK_LENGTH = 512
|
LINK_PLAY_UNLOCK_LENGTH = 1024
|
||||||
|
|
||||||
|
COUNTDOWN_SONG_READY = 4 * 1000000
|
||||||
|
COUNTDOWN_SONG_START = 6 * 1000000
|
||||||
|
|
||||||
|
# 计时模式
|
||||||
|
COUNTDOWN_MATCHING = 15 * 1000000
|
||||||
|
COUNTDOWN_SELECT_SONG = 45 * 1000000
|
||||||
|
COUNTDOWN_SELECT_DIFFICULTY = 45 * 1000000
|
||||||
|
COUNTDOWN_RESULT = 60 * 1000000
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import binascii
|
|||||||
import logging
|
import logging
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
|
from json import dumps, loads
|
||||||
|
|
||||||
from .aes import decrypt, encrypt
|
from .aes import decrypt, encrypt
|
||||||
from .config import Config
|
from .config import Config
|
||||||
@@ -16,14 +17,16 @@ logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message
|
|||||||
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
|
||||||
|
# print(client_msg)
|
||||||
try:
|
try:
|
||||||
token = client_msg[:8]
|
token = client_msg[:8]
|
||||||
iv = client_msg[8:20]
|
iv = client_msg[8:20]
|
||||||
tag = client_msg[20:32]
|
tag = client_msg[20:36]
|
||||||
ciphertext = client_msg[32:]
|
ciphertext = client_msg[36:]
|
||||||
if bi(token) not in Store.link_play_data:
|
|
||||||
|
user = Store.link_play_data.get(bi(token))
|
||||||
|
if user is None:
|
||||||
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:
|
||||||
@@ -50,27 +53,55 @@ class UDP_handler(socketserver.BaseRequestHandler):
|
|||||||
# logging.info(
|
# logging.info(
|
||||||
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
||||||
|
|
||||||
server.sendto(token + iv + tag[:12] +
|
server.sendto(token + iv + tag + ciphertext, self.client_address)
|
||||||
ciphertext, self.client_address)
|
|
||||||
|
|
||||||
|
AUTH_LEN = len(Config.AUTHENTICATION)
|
||||||
|
TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16]
|
||||||
|
|
||||||
|
|
||||||
class TCP_handler(socketserver.StreamRequestHandler):
|
class TCP_handler(socketserver.StreamRequestHandler):
|
||||||
def handle(self):
|
|
||||||
self.data = self.rfile.readline().strip()
|
|
||||||
|
|
||||||
message = self.data.decode('utf-8')
|
def handle(self):
|
||||||
if Config.DEBUG:
|
try:
|
||||||
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
|
if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
|
||||||
data = message.split('|')
|
self.wfile.write(b'No authentication')
|
||||||
if data[0] != Config.AUTHENTICATION:
|
logging.warning(
|
||||||
self.wfile.write(b'No authentication')
|
f'TCP-{self.client_address[0]}-No authentication')
|
||||||
logging.warning(f'TCP-{self.client_address[0]}-No authentication')
|
return None
|
||||||
|
|
||||||
|
cipher_len = int.from_bytes(self.rfile.read(8), byteorder='little')
|
||||||
|
if cipher_len > Config.TCP_MAX_LENGTH:
|
||||||
|
self.wfile.write(b'Body too long')
|
||||||
|
logging.warning(f'TCP-{self.client_address[0]}-Body too long')
|
||||||
|
return None
|
||||||
|
|
||||||
|
iv = self.rfile.read(12)
|
||||||
|
tag = self.rfile.read(16)
|
||||||
|
ciphertext = self.rfile.read(cipher_len)
|
||||||
|
|
||||||
|
self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||||
|
message = self.data.decode('utf-8')
|
||||||
|
data = loads(message)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
r = TCPRouter(data[1:]).handle()
|
|
||||||
if Config.DEBUG:
|
if Config.DEBUG:
|
||||||
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
|
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
|
||||||
self.wfile.write(r.encode('utf-8'))
|
|
||||||
|
r = TCPRouter(data).handle()
|
||||||
|
try:
|
||||||
|
r = dumps(r)
|
||||||
|
if Config.DEBUG:
|
||||||
|
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
|
||||||
|
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
|
||||||
|
r = len(ciphertext).to_bytes(
|
||||||
|
8, byteorder='little') + iv + tag + ciphertext
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
return None
|
||||||
|
self.wfile.write(r)
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ from time import time
|
|||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .udp_class import Player, Room, bi
|
from .udp_class import Player, Room, bi
|
||||||
|
from .udp_sender import CommandSender
|
||||||
|
|
||||||
|
|
||||||
class Store:
|
class Store:
|
||||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||||
link_play_data = {}
|
link_play_data = {}
|
||||||
room_id_dict = {} # 'room_id': Room
|
room_id_dict: "dict[int, Room]" = {} # 'room_id': Room
|
||||||
room_code_dict = {} # 'room_code': Room
|
room_code_dict = {} # 'room_code': Room
|
||||||
player_dict = {} # 'player_id' : Player
|
player_dict = {} # 'player_id' : Player
|
||||||
|
|
||||||
|
share_token_dict = {} # 'share_token': Room
|
||||||
|
|
||||||
lock = RLock()
|
lock = RLock()
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +31,14 @@ def random_room_code():
|
|||||||
return re
|
return re
|
||||||
|
|
||||||
|
|
||||||
|
def random_share_token():
|
||||||
|
CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
re = ''
|
||||||
|
for _ in range(10):
|
||||||
|
re += CHARSET[randint(0, 35)]
|
||||||
|
return re
|
||||||
|
|
||||||
|
|
||||||
def unique_random(dataset, length=8, random_func=None):
|
def unique_random(dataset, length=8, random_func=None):
|
||||||
'''无重复随机,且默认非0,没处理可能的死循环'''
|
'''无重复随机,且默认非0,没处理可能的死循环'''
|
||||||
if random_func is None:
|
if random_func is None:
|
||||||
@@ -43,17 +54,29 @@ def unique_random(dataset, length=8, random_func=None):
|
|||||||
|
|
||||||
def clear_player(token):
|
def clear_player(token):
|
||||||
# 清除玩家信息和token
|
# 清除玩家信息和token
|
||||||
del Store.player_dict[Store.link_play_data[token]['player_id']]
|
player_id = Store.link_play_data[token]['player_id']
|
||||||
del Store.link_play_data[token]
|
logging.info(f'Clean player `{Store.player_dict[player_id].name}`')
|
||||||
|
with Store.lock:
|
||||||
|
if player_id in Store.player_dict:
|
||||||
|
del Store.player_dict[player_id]
|
||||||
|
if token in Store.link_play_data:
|
||||||
|
del Store.link_play_data[token]
|
||||||
|
|
||||||
|
|
||||||
def clear_room(room):
|
def clear_room(room):
|
||||||
# 清除房间信息
|
# 清除房间信息
|
||||||
room_id = room.room_id
|
room_id = room.room_id
|
||||||
room_code = room.room_code
|
room_code = room.room_code
|
||||||
del Store.room_id_dict[room_id]
|
share_token = room.share_token
|
||||||
del Store.room_code_dict[room_code]
|
logging.info(f'Clean room `{room_code}`')
|
||||||
del room
|
with Store.lock:
|
||||||
|
if room_id in Store.room_id_dict:
|
||||||
|
del Store.room_id_dict[room_id]
|
||||||
|
if room_code in Store.room_code_dict:
|
||||||
|
del Store.room_code_dict[room_code]
|
||||||
|
if share_token in Store.share_token_dict:
|
||||||
|
del Store.share_token_dict[share_token]
|
||||||
|
del room
|
||||||
|
|
||||||
|
|
||||||
def memory_clean(now):
|
def memory_clean(now):
|
||||||
@@ -84,19 +107,24 @@ def memory_clean(now):
|
|||||||
class TCPRouter:
|
class TCPRouter:
|
||||||
clean_timer = 0
|
clean_timer = 0
|
||||||
router = {
|
router = {
|
||||||
'0': 'debug',
|
'debug',
|
||||||
'1': 'create_room',
|
'create_room',
|
||||||
'2': 'join_room',
|
'join_room',
|
||||||
'3': 'update_room',
|
'update_room',
|
||||||
|
'get_rooms',
|
||||||
|
'select_room',
|
||||||
|
'get_match_rooms'
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, data: list):
|
def __init__(self, raw_data: 'dict | list'):
|
||||||
self.data = data # data: list[str] = [command, ...]
|
self.raw_data = raw_data # data: dict {endpoint: str, data: dict}
|
||||||
|
self.data = raw_data['data']
|
||||||
|
self.endpoint = raw_data['endpoint']
|
||||||
|
|
||||||
def debug(self):
|
def debug(self) -> dict:
|
||||||
if Config.DEBUG:
|
if Config.DEBUG:
|
||||||
return eval(self.data[1])
|
return {'result': eval(self.data['code'])}
|
||||||
return 'ok'
|
return {'hello_world': 'ok'}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_check():
|
def clean_check():
|
||||||
@@ -106,14 +134,21 @@ class TCPRouter:
|
|||||||
TCPRouter.clean_timer = now
|
TCPRouter.clean_timer = now
|
||||||
memory_clean(now)
|
memory_clean(now)
|
||||||
|
|
||||||
def handle(self) -> str:
|
def handle(self) -> dict:
|
||||||
self.clean_check()
|
self.clean_check()
|
||||||
if self.data[0] not in self.router:
|
if self.endpoint not in self.router:
|
||||||
return None
|
return {'code': 999}
|
||||||
r = getattr(self, self.router[self.data[0]])()
|
try:
|
||||||
if isinstance(r, tuple):
|
r = getattr(self, self.endpoint)()
|
||||||
return '|'.join(map(str, r))
|
except Exception as e:
|
||||||
return str(r)
|
logging.error(e)
|
||||||
|
return {'code': 999}
|
||||||
|
if isinstance(r, int):
|
||||||
|
return {'code': r}
|
||||||
|
return {
|
||||||
|
'code': 0,
|
||||||
|
'data': r
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_player(name: str) -> Player:
|
def generate_player(name: str) -> Player:
|
||||||
@@ -131,7 +166,7 @@ class TCPRouter:
|
|||||||
room_id = unique_random(Store.room_id_dict)
|
room_id = unique_random(Store.room_id_dict)
|
||||||
room = Room()
|
room = Room()
|
||||||
room.room_id = room_id
|
room.room_id = room_id
|
||||||
room.timestamp = round(time() * 1000)
|
room.timestamp = round(time() * 1000000)
|
||||||
Store.room_id_dict[room_id] = room
|
Store.room_id_dict[room_id] = room
|
||||||
|
|
||||||
room_code = unique_random(
|
room_code = unique_random(
|
||||||
@@ -139,14 +174,22 @@ class TCPRouter:
|
|||||||
room.room_code = room_code
|
room.room_code = room_code
|
||||||
Store.room_code_dict[room_code] = room
|
Store.room_code_dict[room_code] = room
|
||||||
|
|
||||||
|
share_token = unique_random(
|
||||||
|
Store.share_token_dict, random_func=random_share_token)
|
||||||
|
room.share_token = share_token
|
||||||
|
Store.share_token_dict[share_token] = room
|
||||||
|
|
||||||
return room
|
return room
|
||||||
|
|
||||||
def create_room(self) -> tuple:
|
def create_room(self) -> dict:
|
||||||
# 开房
|
# 开房
|
||||||
# data = ['1', name, song_unlock, ]
|
# data = ['1', name, song_unlock, ]
|
||||||
# song_unlock: base64 str
|
# song_unlock: base64 str
|
||||||
name = self.data[1]
|
name = self.data['name']
|
||||||
song_unlock = b64decode(self.data[2])
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
match_times = self.data.get('match_times', None)
|
||||||
|
|
||||||
key = urandom(16)
|
key = urandom(16)
|
||||||
with Store.lock:
|
with Store.lock:
|
||||||
@@ -154,6 +197,9 @@ class TCPRouter:
|
|||||||
player = self.generate_player(name)
|
player = self.generate_player(name)
|
||||||
|
|
||||||
player.song_unlock = song_unlock
|
player.song_unlock = song_unlock
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
|
player.player_index = 0
|
||||||
room.song_unlock = song_unlock
|
room.song_unlock = song_unlock
|
||||||
room.host_id = player.player_id
|
room.host_id = player.player_id
|
||||||
room.players[0] = player
|
room.players[0] = player
|
||||||
@@ -161,6 +207,12 @@ class TCPRouter:
|
|||||||
token = room.room_id
|
token = room.room_id
|
||||||
player.token = token
|
player.token = token
|
||||||
|
|
||||||
|
# 匹配模式追加
|
||||||
|
if match_times is not None:
|
||||||
|
room.is_public = 1
|
||||||
|
room.round_mode = 3
|
||||||
|
room.timed_mode = 1
|
||||||
|
|
||||||
Store.link_play_data[token] = {
|
Store.link_play_data[token] = {
|
||||||
'key': key,
|
'key': key,
|
||||||
'room': room,
|
'room': room,
|
||||||
@@ -169,63 +221,190 @@ class TCPRouter:
|
|||||||
}
|
}
|
||||||
|
|
||||||
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`')
|
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)
|
return {
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'token': token,
|
||||||
|
'key': b64encode(key).decode('utf-8'),
|
||||||
|
'player_id': player.player_id
|
||||||
|
}
|
||||||
|
|
||||||
def join_room(self) -> tuple:
|
def join_room(self) -> 'dict | int':
|
||||||
# 入房
|
# 入房
|
||||||
# data = ['2', name, song_unlock, room_code]
|
# data = ['2', name, song_unlock, room_code]
|
||||||
# song_unlock: base64 str
|
# song_unlock: base64 str
|
||||||
room_code = self.data[3].upper()
|
room_code = self.data['room_code'].upper()
|
||||||
key = urandom(16)
|
key = urandom(16)
|
||||||
name = self.data[1]
|
name = self.data['name']
|
||||||
song_unlock = b64decode(self.data[2])
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
match_times = self.data.get('match_times', None)
|
||||||
|
|
||||||
with Store.lock:
|
with Store.lock:
|
||||||
if room_code not in Store.room_code_dict:
|
if room_code not in Store.room_code_dict:
|
||||||
# 房间号错误 / 房间不存在
|
# 房间号错误 / 房间不存在
|
||||||
return '1202'
|
return 1202
|
||||||
room: Room = Store.room_code_dict[room_code]
|
room: Room = Store.room_code_dict[room_code]
|
||||||
|
|
||||||
player_num = room.player_num
|
player_num = room.player_num
|
||||||
if player_num == 4:
|
if player_num == 4:
|
||||||
# 满人
|
# 满人
|
||||||
return '1201'
|
return 1201
|
||||||
if player_num == 0:
|
if player_num == 0:
|
||||||
# 房间不存在
|
# 房间不存在
|
||||||
return '1202'
|
return 1202
|
||||||
if room.state != 2:
|
if room.state not in (0, 1, 2) or (room.is_public and match_times is None):
|
||||||
# 无法加入
|
# 无法加入
|
||||||
return '1205'
|
return 1205
|
||||||
|
|
||||||
token = unique_random(Store.link_play_data)
|
token = unique_random(Store.link_play_data)
|
||||||
|
|
||||||
player = self.generate_player(name)
|
player = self.generate_player(name)
|
||||||
player.token = token
|
player.token = token
|
||||||
player.song_unlock = song_unlock
|
player.song_unlock = song_unlock
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
room.update_song_unlock()
|
room.update_song_unlock()
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
if room.players[i].player_id == 0:
|
if room.players[i].player_id == 0:
|
||||||
room.players[i] = player
|
room.players[i] = player
|
||||||
player_index = i
|
player.player_index = i
|
||||||
break
|
break
|
||||||
Store.link_play_data[token] = {
|
Store.link_play_data[token] = {
|
||||||
'key': key,
|
'key': key,
|
||||||
'room': room,
|
'room': room,
|
||||||
'player_index': player_index,
|
'player_index': player.player_index,
|
||||||
'player_id': player.player_id
|
'player_id': player.player_id
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.info(f'TCP-Player `{name}` joins room `{room_code}`')
|
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'))
|
return {
|
||||||
|
'room_code': room_code,
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'token': token,
|
||||||
|
'key': b64encode(key).decode('utf-8'),
|
||||||
|
'player_id': player.player_id,
|
||||||
|
'song_unlock': b64encode(room.song_unlock).decode('utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
def update_room(self) -> tuple:
|
def update_room(self) -> dict:
|
||||||
# 房间信息更新
|
# 房间信息更新
|
||||||
# data = ['3', token]
|
# data = ['3', token]
|
||||||
token = int(self.data[1])
|
token = int(self.data['token'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
|
||||||
with Store.lock:
|
with Store.lock:
|
||||||
if token not in Store.link_play_data:
|
if token not in Store.link_play_data:
|
||||||
return '108'
|
return 108
|
||||||
r = Store.link_play_data[token]
|
r = Store.link_play_data[token]
|
||||||
room = r['room']
|
room = r['room']
|
||||||
|
|
||||||
|
# 更新玩家信息
|
||||||
|
player_index = r['player_index']
|
||||||
|
player = room.players[player_index]
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
|
cs = CommandSender(room)
|
||||||
|
room.command_queue.append(cs.command_12(player_index))
|
||||||
|
|
||||||
logging.info(f'TCP-Room `{room.room_code}` info update')
|
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'))
|
return {
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'key': b64encode(r['key']).decode('utf-8'),
|
||||||
|
# changed from room.players[r['player_index']].player_id,
|
||||||
|
'player_id': r['player_id'],
|
||||||
|
'song_unlock': b64encode(room.song_unlock).decode('utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_rooms(self) -> dict:
|
||||||
|
# 获取房间列表与详细信息
|
||||||
|
|
||||||
|
offset = int(self.data.get('offset', 0))
|
||||||
|
if offset < 0:
|
||||||
|
offset = 0
|
||||||
|
limit = min(int(self.data.get('limit', 100)), 100)
|
||||||
|
if limit < 0:
|
||||||
|
limit = 100
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
m = 0
|
||||||
|
rooms = []
|
||||||
|
f = False
|
||||||
|
f2 = False
|
||||||
|
for room in Store.room_id_dict.values():
|
||||||
|
if room.player_num == 0:
|
||||||
|
continue
|
||||||
|
if m < offset:
|
||||||
|
m += 1
|
||||||
|
continue
|
||||||
|
if f:
|
||||||
|
# 处理刚好有 limit 个房间的情况
|
||||||
|
f2 = True
|
||||||
|
break
|
||||||
|
n += 1
|
||||||
|
rooms.append(room.to_dict())
|
||||||
|
if n >= limit:
|
||||||
|
f = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
'amount': n,
|
||||||
|
'offset': offset,
|
||||||
|
'limit': limit,
|
||||||
|
'has_more': f2,
|
||||||
|
'rooms': rooms
|
||||||
|
}
|
||||||
|
|
||||||
|
def select_room(self) -> dict:
|
||||||
|
# 查询房间信息
|
||||||
|
|
||||||
|
room_code = self.data.get('room_code', None)
|
||||||
|
share_token = self.data.get('share_token', None)
|
||||||
|
|
||||||
|
if room_code is not None:
|
||||||
|
room = Store.room_code_dict.get(room_code, None)
|
||||||
|
elif share_token is not None:
|
||||||
|
room = Store.share_token_dict.get(share_token, None)
|
||||||
|
if room is None:
|
||||||
|
return 108
|
||||||
|
|
||||||
|
return {
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'share_token': room.share_token,
|
||||||
|
'is_enterable': room.is_enterable,
|
||||||
|
'is_matchable': room.is_matchable,
|
||||||
|
'is_playing': room.is_playing,
|
||||||
|
'is_public': room.is_public == 1,
|
||||||
|
'timed_mode': room.timed_mode == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_match_rooms(self):
|
||||||
|
n = 0
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for room in Store.room_id_dict.values():
|
||||||
|
if not room.is_matchable:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rooms.append({
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'share_token': room.share_token,
|
||||||
|
'is_matchable': room.is_matchable,
|
||||||
|
'next_state_timestamp': room.next_state_timestamp,
|
||||||
|
'song_unlock': b64encode(room.song_unlock).decode('utf-8'),
|
||||||
|
'players': [{
|
||||||
|
'player_id': i.player_id,
|
||||||
|
'name': i.name,
|
||||||
|
'rating_ptt': i.rating_ptt
|
||||||
|
} for i in room.players]
|
||||||
|
})
|
||||||
|
if n >= 100:
|
||||||
|
break
|
||||||
|
return {
|
||||||
|
'amount': n,
|
||||||
|
'rooms': rooms
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import logging
|
||||||
from time import time
|
from time import time
|
||||||
|
from random import randint
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -11,26 +13,73 @@ def bi(value):
|
|||||||
return int.from_bytes(value, byteorder='little')
|
return int.from_bytes(value, byteorder='little')
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Score:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.difficulty = 0xff
|
||||||
|
|
||||||
|
self.score = 0
|
||||||
|
self.cleartype = 0
|
||||||
|
self.timer = 0
|
||||||
|
|
||||||
|
self.best_score_flag = 0 # personal best
|
||||||
|
self.best_player_flag = 0 # high score
|
||||||
|
|
||||||
|
# 5.10 新增
|
||||||
|
self.shiny_perfect_count = 0 # 2 bytes
|
||||||
|
self.perfect_count = 0 # 2 bytes
|
||||||
|
self.near_count = 0 # 2 bytes
|
||||||
|
self.miss_count = 0 # 2 bytes
|
||||||
|
self.early_count = 0 # 2 bytes
|
||||||
|
self.late_count = 0 # 2 bytes
|
||||||
|
|
||||||
|
self.healthy = 0 # 4 bytes signed? 不确定,但似乎没影响
|
||||||
|
|
||||||
|
def copy(self, x: 'Score'):
|
||||||
|
self.difficulty = x.difficulty
|
||||||
|
self.score = x.score
|
||||||
|
self.cleartype = x.cleartype
|
||||||
|
self.timer = x.timer
|
||||||
|
self.best_score_flag = x.best_score_flag
|
||||||
|
self.best_player_flag = x.best_player_flag
|
||||||
|
self.shiny_perfect_count = x.shiny_perfect_count
|
||||||
|
self.perfect_count = x.perfect_count
|
||||||
|
self.near_count = x.near_count
|
||||||
|
self.miss_count = x.miss_count
|
||||||
|
self.early_count = x.early_count
|
||||||
|
self.late_count = x.late_count
|
||||||
|
self.healthy = x.healthy
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.difficulty = 0xff
|
||||||
|
self.score = 0
|
||||||
|
self.cleartype = 0
|
||||||
|
self.timer = 0
|
||||||
|
self.best_score_flag = 0
|
||||||
|
self.best_player_flag = 0
|
||||||
|
self.shiny_perfect_count = 0
|
||||||
|
self.perfect_count = 0
|
||||||
|
self.near_count = 0
|
||||||
|
self.miss_count = 0
|
||||||
|
self.early_count = 0
|
||||||
|
self.late_count = 0
|
||||||
|
self.healthy = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Score: {self.score}, Cleartype: {self.cleartype}, Difficulty: {self.difficulty}, Timer: {self.timer}, Best Score Flag: {self.best_score_flag}, Best Player Flag: {self.best_player_flag}, Shiny Perfect: {self.shiny_perfect_count}, Perfect: {self.perfect_count}, Near: {self.near_count}, Miss: {self.miss_count}, Early: {self.early_count}, Late: {self.late_count}, Healthy: {self.healthy}'
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, player_index: int = 0) -> None:
|
||||||
self.player_id = 0
|
self.player_id = 0
|
||||||
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
||||||
self.token = 0
|
self.token = 0
|
||||||
|
|
||||||
self.character_id = 0xff
|
self.character_id = 0xff
|
||||||
self.last_character_id = 0xff
|
|
||||||
self.is_uncapped = 0
|
self.is_uncapped = 0
|
||||||
|
|
||||||
self.difficulty = 0xff
|
self.score = Score()
|
||||||
self.last_difficulty = 0xff
|
self.last_score = Score()
|
||||||
self.score = 0
|
|
||||||
self.last_score = 0
|
|
||||||
self.timer = 0
|
|
||||||
self.last_timer = 0
|
|
||||||
self.cleartype = 0
|
|
||||||
self.last_cleartype = 0
|
|
||||||
self.best_score_flag = 0
|
|
||||||
self.best_player_flag = 0
|
|
||||||
self.finish_flag = 0
|
self.finish_flag = 0
|
||||||
|
|
||||||
self.player_state = 1
|
self.player_state = 1
|
||||||
@@ -44,6 +93,49 @@ class Player:
|
|||||||
|
|
||||||
self.start_command_num = 0
|
self.start_command_num = 0
|
||||||
|
|
||||||
|
# 5.10 新增
|
||||||
|
|
||||||
|
self.voting: int = 0x8000 # 2 bytes, song_idx, 0xffff 为不选择,0x8000 为默认值
|
||||||
|
self.player_index: int = player_index # 1 byte 不确定对不对
|
||||||
|
self.switch_2: int = 0 # 1 byte
|
||||||
|
|
||||||
|
self.rating_ptt: int = 0 # 2 bytes
|
||||||
|
self.is_hide_rating: int = 0 # 1 byte
|
||||||
|
self.is_staff: int = 0 # 1 byte
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.player_name.decode('ascii').rstrip('\x00')
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'multiplay_player_id': self.player_id,
|
||||||
|
'name': self.name,
|
||||||
|
'is_online': self.online == 1,
|
||||||
|
'character_id': self.character_id,
|
||||||
|
'is_uncapped': self.is_uncapped == 1,
|
||||||
|
'rating_ptt': self.rating_ptt,
|
||||||
|
'is_hide_rating': self.is_hide_rating == 1,
|
||||||
|
'last_song': {
|
||||||
|
'difficulty': self.last_score.difficulty,
|
||||||
|
'score': self.last_score.score,
|
||||||
|
'cleartype': self.last_score.cleartype,
|
||||||
|
'shine_perfect': self.last_score.shiny_perfect_count,
|
||||||
|
'perfect': self.last_score.perfect_count,
|
||||||
|
'near': self.last_score.near_count,
|
||||||
|
'miss': self.last_score.miss_count,
|
||||||
|
'early': self.last_score.early_count,
|
||||||
|
'late': self.last_score.late_count,
|
||||||
|
},
|
||||||
|
'song': {
|
||||||
|
'difficulty': self.score.difficulty,
|
||||||
|
'score': self.score.score,
|
||||||
|
'cleartype': self.score.cleartype,
|
||||||
|
},
|
||||||
|
'player_state': self.player_state,
|
||||||
|
'last_timestamp': self.last_timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
def set_player_name(self, player_name: str):
|
def set_player_name(self, player_name: str):
|
||||||
self.player_name = player_name.encode('ascii')
|
self.player_name = player_name.encode('ascii')
|
||||||
if len(self.player_name) > 16:
|
if len(self.player_name) > 16:
|
||||||
@@ -51,37 +143,153 @@ class Player:
|
|||||||
else:
|
else:
|
||||||
self.player_name += b'\x00' * (16 - len(self.player_name))
|
self.player_name += b'\x00' * (16 - len(self.player_name))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> bytes:
|
||||||
|
re = bytearray()
|
||||||
|
re.extend(b(self.player_id, 8))
|
||||||
|
re.append(self.character_id)
|
||||||
|
re.append(self.is_uncapped)
|
||||||
|
re.append(self.score.difficulty)
|
||||||
|
re.extend(b(self.score.score, 4))
|
||||||
|
re.extend(b(self.score.timer, 4))
|
||||||
|
re.append(self.score.cleartype)
|
||||||
|
re.append(self.player_state)
|
||||||
|
re.append(self.download_percent)
|
||||||
|
re.append(self.online)
|
||||||
|
|
||||||
|
re.extend(b(self.voting, 2))
|
||||||
|
re.append(self.player_index)
|
||||||
|
re.append(self.switch_2)
|
||||||
|
re.extend(b(self.rating_ptt, 2))
|
||||||
|
re.append(self.is_hide_rating)
|
||||||
|
re.append(self.is_staff)
|
||||||
|
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_score_info(self) -> bytes:
|
||||||
|
if self.player_id == 0:
|
||||||
|
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
x = self.last_score
|
||||||
|
re = bytearray()
|
||||||
|
re.append(self.character_id)
|
||||||
|
re.append(x.difficulty)
|
||||||
|
re.extend(b(x.score, 4))
|
||||||
|
re.append(x.cleartype)
|
||||||
|
re.append(x.best_score_flag)
|
||||||
|
re.append(x.best_player_flag)
|
||||||
|
re.extend(b(x.shiny_perfect_count, 2))
|
||||||
|
re.extend(b(x.perfect_count, 2))
|
||||||
|
re.extend(b(x.near_count, 2))
|
||||||
|
re.extend(b(x.miss_count, 2))
|
||||||
|
re.extend(b(x.early_count, 2))
|
||||||
|
re.extend(b(x.late_count, 2))
|
||||||
|
re.extend(b(x.healthy, 4))
|
||||||
|
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.room_id = 0
|
self.room_id = 0
|
||||||
self.room_code = 'AAAA00'
|
self.room_code = 'AAAA00'
|
||||||
|
self.share_token = 'abcde12345' # 5.10 新增
|
||||||
|
|
||||||
self.countdown = 0xffffffff
|
self.countdown = 0xffffffff
|
||||||
self.timestamp = 0
|
self.timestamp = 0
|
||||||
self.state = 0
|
self._state = 0
|
||||||
self.song_idx = 0xffff
|
self.song_idx = 0xffff # 疑似 idx * 5
|
||||||
self.last_song_idx = 0xffff
|
self.last_song_idx = 0xffff # 疑似 idx * 5
|
||||||
|
|
||||||
self.song_unlock = b'\xFF' * 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(0), Player(1), Player(2), Player(3)]
|
||||||
|
|
||||||
self.interval = 1000
|
self.interval = 1000
|
||||||
self.times = 100
|
self.times = 100 # ???
|
||||||
|
|
||||||
self.round_switch = 0
|
self.round_mode: int = 1 # 5.10 从 bool 修改为 int 1~3
|
||||||
|
self.is_public = 0 # 5.10 新增
|
||||||
|
self.timed_mode = 0 # 5.10 新增
|
||||||
|
|
||||||
|
self.selected_voter_player_id: int = 0 # 5.10 新增
|
||||||
|
|
||||||
self.command_queue = []
|
self.command_queue = []
|
||||||
|
|
||||||
|
self.next_state_timestamp = 0 # 计时模式下一个状态时间
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> int:
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, value: int):
|
||||||
|
self._state = value
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
p = [i.to_dict() for i in self.players if i.player_id != 0]
|
||||||
|
for i in p:
|
||||||
|
i['is_host'] = i['multiplay_player_id'] == self.host_id
|
||||||
|
return {
|
||||||
|
'room_id': self.room_id,
|
||||||
|
'room_code': self.room_code,
|
||||||
|
'share_token': self.share_token,
|
||||||
|
'state': self.state,
|
||||||
|
'song_idx': self.song_idx,
|
||||||
|
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
|
||||||
|
'host_id': self.host_id,
|
||||||
|
'players': p,
|
||||||
|
'round_mode': self.round_mode,
|
||||||
|
'last_timestamp': self.timestamp,
|
||||||
|
'is_enterable': self.is_enterable,
|
||||||
|
'is_matchable': self.is_matchable,
|
||||||
|
'is_playing': self.is_playing,
|
||||||
|
'is_public': self.is_public == 1,
|
||||||
|
'timed_mode': self.timed_mode == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def room_info(self) -> bytes:
|
||||||
|
re = bytearray()
|
||||||
|
re.extend(b(self.host_id, 8))
|
||||||
|
re.append(self.state)
|
||||||
|
re.extend(b(self.countdown, 4))
|
||||||
|
re.extend(b(self.timestamp, 8))
|
||||||
|
re.extend(b(self.song_idx, 2))
|
||||||
|
re.extend(b(self.interval, 2))
|
||||||
|
re.extend(b(self.times, 7))
|
||||||
|
re.extend(self.get_player_last_score())
|
||||||
|
re.extend(b(self.last_song_idx, 2))
|
||||||
|
re.append(self.round_mode)
|
||||||
|
re.append(self.is_public)
|
||||||
|
re.append(self.timed_mode)
|
||||||
|
re.extend(b(self.selected_voter_player_id, 8))
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_enterable(self) -> bool:
|
||||||
|
return 0 < self.player_num < 4 and self.state == 2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_matchable(self) -> bool:
|
||||||
|
return self.is_public and 0 < self.player_num < 4 and self.state == 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_playing(self) -> bool:
|
||||||
|
return self.state in (4, 5, 6, 7)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def command_queue_length(self) -> int:
|
def command_queue_length(self) -> int:
|
||||||
return len(self.command_queue)
|
return len(self.command_queue)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def player_num(self) -> int:
|
def player_num(self) -> int:
|
||||||
self.check_player_online()
|
now = round(time() * 1000000)
|
||||||
|
if now - self.timestamp >= 1000000:
|
||||||
|
self.check_player_online(now)
|
||||||
return sum(i.player_id != 0 for i in self.players)
|
return sum(i.player_id != 0 for i in self.players)
|
||||||
|
|
||||||
def check_player_online(self, now: int = None):
|
def check_player_online(self, now: int = None):
|
||||||
@@ -104,49 +312,54 @@ class Room:
|
|||||||
|
|
||||||
def get_players_info(self):
|
def get_players_info(self):
|
||||||
# 获取所有玩家信息
|
# 获取所有玩家信息
|
||||||
re = b''
|
re = bytearray()
|
||||||
for i in self.players:
|
for i in self.players:
|
||||||
re += b(i.player_id, 8) + b(i.character_id) + b(i.is_uncapped) + b(i.difficulty) + b(i.score, 4) + \
|
re.extend(i.info)
|
||||||
b(i.timer, 4) + b(i.cleartype) + b(i.player_state) + \
|
re.append(0)
|
||||||
b(i.download_percent) + b(i.online) + b'\x00' + i.player_name
|
re.extend(i.player_name)
|
||||||
return re
|
return bytes(re)
|
||||||
|
|
||||||
def get_player_last_score(self):
|
def get_player_last_score(self):
|
||||||
# 获取上次曲目玩家分数,返回bytes
|
# 获取上次曲目玩家分数,返回bytes
|
||||||
if self.last_song_idx == 0xffff:
|
if self.last_song_idx == 0xffff:
|
||||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' * 4
|
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' * 4
|
||||||
re = b''
|
return b''.join(i.last_score_info for i in self.players)
|
||||||
|
|
||||||
for i in range(4):
|
|
||||||
player = self.players[i]
|
|
||||||
|
|
||||||
if player.player_id != 0:
|
|
||||||
re += b(player.last_character_id) + b(player.last_difficulty) + b(player.last_score, 4) + b(
|
|
||||||
player.last_cleartype) + b(player.best_score_flag) + b(player.best_player_flag)
|
|
||||||
else:
|
|
||||||
re += b'\xff\xff\x00\x00\x00\x00\x00\x00\x00'
|
|
||||||
|
|
||||||
return re
|
|
||||||
|
|
||||||
def make_round(self):
|
def make_round(self):
|
||||||
# 轮换房主
|
# 轮换房主
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
if self.players[i].player_id == self.host_id:
|
if self.players[i].player_id == self.host_id:
|
||||||
for j in range(1, 4):
|
for j in range(1, 4):
|
||||||
if self.players[(i + j) % 4].player_id != 0:
|
player = self.players[(i + j) % 4]
|
||||||
self.host_id = self.players[(i + j) % 4].player_id
|
if player.player_id != 0:
|
||||||
|
self.host_id = player.player_id
|
||||||
|
logging.info(
|
||||||
|
f'Player `{player.name}` becomes the host of room `{self.room_code}`')
|
||||||
break
|
break
|
||||||
break
|
break
|
||||||
|
|
||||||
def delete_player(self, player_index: int):
|
def delete_player(self, player_index: int):
|
||||||
# 删除某个玩家
|
# 删除某个玩家
|
||||||
if self.players[player_index].player_id == self.host_id:
|
player = self.players[player_index]
|
||||||
|
if player.player_id == self.host_id:
|
||||||
self.make_round()
|
self.make_round()
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f'Player `{player.name}` leaves room `{self.room_code}`')
|
||||||
|
|
||||||
self.players[player_index].online = 0
|
self.players[player_index].online = 0
|
||||||
self.players[player_index] = Player()
|
self.players[player_index] = Player(player_index)
|
||||||
self.update_song_unlock()
|
self.update_song_unlock()
|
||||||
|
|
||||||
|
if self.state in (2, 3):
|
||||||
|
self.state = 1
|
||||||
|
self.song_idx = 0xffff
|
||||||
|
self.voting_clear()
|
||||||
|
|
||||||
|
if self.state in (1, 2) and self.timed_mode and self.player_num <= 1:
|
||||||
|
self.next_state_timestamp = 0
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
|
||||||
def update_song_unlock(self):
|
def update_song_unlock(self):
|
||||||
# 更新房间可用歌曲
|
# 更新房间可用歌曲
|
||||||
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||||
@@ -168,7 +381,7 @@ class Room:
|
|||||||
|
|
||||||
def is_finish(self):
|
def is_finish(self):
|
||||||
# 是否全部进入结算
|
# 是否全部进入结算
|
||||||
if self.state == 8:
|
if self.state != 7:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for i in self.players:
|
for i in self.players:
|
||||||
@@ -186,20 +399,110 @@ class Room:
|
|||||||
max_score_i = []
|
max_score_i = []
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
player = self.players[i]
|
player = self.players[i]
|
||||||
if player.player_id != 0:
|
if player.player_id == 0:
|
||||||
player.finish_flag = 0
|
continue
|
||||||
player.last_timer = player.timer
|
player.finish_flag = 0
|
||||||
player.last_score = player.score
|
player.last_score.copy(player.score)
|
||||||
player.last_cleartype = player.cleartype
|
player.last_score.best_player_flag = 0
|
||||||
player.last_character_id = player.character_id
|
|
||||||
player.last_difficulty = player.difficulty
|
|
||||||
player.best_player_flag = 0
|
|
||||||
|
|
||||||
if player.last_score > max_score:
|
if player.last_score.score > max_score:
|
||||||
max_score = player.last_score
|
max_score = player.last_score.score
|
||||||
max_score_i = [i]
|
max_score_i = [i]
|
||||||
elif player.last_score == max_score:
|
elif player.last_score.score == max_score:
|
||||||
max_score_i.append(i)
|
max_score_i.append(i)
|
||||||
|
|
||||||
for i in max_score_i:
|
for i in max_score_i:
|
||||||
self.players[i].best_player_flag = 1
|
self.players[i].last_score.best_player_flag = 1
|
||||||
|
|
||||||
|
self.voting_clear()
|
||||||
|
for i in self.players:
|
||||||
|
i.score.clear()
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
|
||||||
|
for i in self.players:
|
||||||
|
if i.player_id != 0:
|
||||||
|
logging.info(f'- Player `{i.name}` - {i.last_score}')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_player_voted(self) -> bool:
|
||||||
|
# 是否所有玩家都投票
|
||||||
|
if self.state != 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for i in self.players:
|
||||||
|
if i.player_id != 0 and i.voting == 0x8000:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def random_song(self):
|
||||||
|
random_list = []
|
||||||
|
for i in range(Config.LINK_PLAY_UNLOCK_LENGTH):
|
||||||
|
for j in range(8):
|
||||||
|
if self.song_unlock[i] & (1 << j):
|
||||||
|
random_list.append(i * 8 + j)
|
||||||
|
|
||||||
|
if not random_list:
|
||||||
|
self.song_idx = 0
|
||||||
|
else:
|
||||||
|
self.song_idx = random_list[randint(0, len(random_list) - 1)]
|
||||||
|
|
||||||
|
def make_voting(self):
|
||||||
|
# 投票
|
||||||
|
self.state = 3
|
||||||
|
self.selected_voter_player_id = 0
|
||||||
|
|
||||||
|
random_list = []
|
||||||
|
random_list_player_id = []
|
||||||
|
for i in self.players:
|
||||||
|
if i.player_id == 0 or i.voting == 0xffff or i.voting == 0x8000:
|
||||||
|
continue
|
||||||
|
random_list.append(i.voting)
|
||||||
|
random_list_player_id.append(i.player_id)
|
||||||
|
|
||||||
|
if random_list:
|
||||||
|
idx = randint(0, len(random_list) - 1)
|
||||||
|
self.song_idx = random_list[idx] * 5
|
||||||
|
self.selected_voter_player_id = random_list_player_id[idx]
|
||||||
|
else:
|
||||||
|
self.random_song()
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f'Room `{self.room_code}` votes song `{self.song_idx}`')
|
||||||
|
|
||||||
|
def voting_clear(self):
|
||||||
|
# 清除投票
|
||||||
|
self.selected_voter_player_id = 0
|
||||||
|
for i in self.players:
|
||||||
|
i.voting = 0x8000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_next_state(self) -> bool:
|
||||||
|
if not self.timed_mode and self.state not in (4, 5, 6):
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
return False
|
||||||
|
now = round(time() * 1000000)
|
||||||
|
if self.countdown == 0xffffffff:
|
||||||
|
# 还没开始计时
|
||||||
|
if self.is_public and self.state == 1:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_MATCHING
|
||||||
|
elif self.state == 2:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_SONG
|
||||||
|
elif self.state == 3:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_DIFFICULTY
|
||||||
|
elif self.state == 4:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_READY
|
||||||
|
elif self.state == 5 or self.state == 6:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_START
|
||||||
|
elif self.state == 8:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_RESULT
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不是哥们,616 你脑子怎么长的,上个版本是毫秒时间戳,新版本变成了微秒???那你这倒计时怎么还是毫秒啊!!!
|
||||||
|
self.countdown = (self.next_state_timestamp - now) // 1000
|
||||||
|
if self.countdown <= 0:
|
||||||
|
self.countdown = 0
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import time
|
import logging
|
||||||
|
|
||||||
from .udp_class import Room, bi
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
from .udp_class import Room, bi
|
||||||
from .udp_sender import CommandSender
|
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']
|
route = {
|
||||||
|
0x01: 'command_01',
|
||||||
|
0x02: 'command_02',
|
||||||
|
0x03: 'command_03',
|
||||||
|
0x04: 'command_04',
|
||||||
|
0x06: 'command_06',
|
||||||
|
0x07: 'command_07',
|
||||||
|
0x08: 'command_08',
|
||||||
|
0x09: 'command_09',
|
||||||
|
0x0a: 'command_0a',
|
||||||
|
0x0b: 'command_0b',
|
||||||
|
0x20: 'command_20',
|
||||||
|
0x22: 'command_22',
|
||||||
|
0x23: 'command_23',
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -30,7 +44,7 @@ class CommandParser:
|
|||||||
re.append(self.room.command_queue[i])
|
re.append(self.room.command_queue[i])
|
||||||
|
|
||||||
if self.room.players[self.player_index].extra_command_queue:
|
if self.room.players[self.player_index].extra_command_queue:
|
||||||
re += self.room.players[self.player_index].extra_command_queue
|
re += self.room.players[self.player_index].extra_command_queue[-12:]
|
||||||
self.room.players[self.player_index].extra_command_queue = []
|
self.room.players[self.player_index].extra_command_queue = []
|
||||||
|
|
||||||
if r:
|
if r:
|
||||||
@@ -44,15 +58,21 @@ class CommandParser:
|
|||||||
for i in self.room.players:
|
for i in self.room.players:
|
||||||
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
|
||||||
|
logging.info(
|
||||||
|
f'Player `{i.name}` becomes the host of room `{self.room.room_code}`')
|
||||||
|
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
self.room.command_queue.append(self.s.command_10())
|
self.room.command_queue.append(self.s.command_10())
|
||||||
|
|
||||||
def command_02(self):
|
def command_02(self):
|
||||||
|
# 房主选歌
|
||||||
|
if self.room.round_mode == 3:
|
||||||
|
logging.warning('Error: round_mode == 3 in command 02')
|
||||||
|
return None
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
song_idx = bi(self.command[24:26])
|
song_idx = bi(self.command[24:26])
|
||||||
|
|
||||||
flag = 2
|
flag = 5
|
||||||
if self.room.state == 2:
|
if self.room.state == 2:
|
||||||
flag = 0
|
flag = 0
|
||||||
self.room.state = 3
|
self.room.state = 3
|
||||||
@@ -66,10 +86,17 @@ class CommandParser:
|
|||||||
# 尝试进入结算
|
# 尝试进入结算
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.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.score = bi(self.command[24:28])
|
||||||
player.cleartype = self.command[28]
|
player.score.cleartype = self.command[28]
|
||||||
player.difficulty = self.command[29]
|
player.score.difficulty = self.command[29]
|
||||||
player.best_score_flag = self.command[30]
|
player.score.best_score_flag = self.command[30]
|
||||||
|
player.score.shiny_perfect_count = bi(self.command[31:33])
|
||||||
|
player.score.perfect_count = bi(self.command[33:35])
|
||||||
|
player.score.near_count = bi(self.command[35:37])
|
||||||
|
player.score.miss_count = bi(self.command[37:39])
|
||||||
|
player.score.early_count = bi(self.command[39:41])
|
||||||
|
player.score.late_count = bi(self.command[41:43])
|
||||||
|
player.score.healthy = bi(self.command[43:47])
|
||||||
player.finish_flag = 1
|
player.finish_flag = 1
|
||||||
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
|
||||||
@@ -91,36 +118,38 @@ class CommandParser:
|
|||||||
flag = 1
|
flag = 1
|
||||||
self.room.delete_player(i)
|
self.room.delete_player(i)
|
||||||
self.room.command_queue.append(self.s.command_12(i))
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
self.room.update_song_unlock()
|
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
break
|
break
|
||||||
|
|
||||||
return [self.s.command_0d(flag)]
|
return [self.s.command_0d(flag)]
|
||||||
|
|
||||||
def command_05(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def command_06(self):
|
def command_06(self):
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.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.voting_clear()
|
||||||
|
|
||||||
self.room.command_queue.append(self.s.command_13())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
def command_07(self):
|
def command_07(self):
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.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:24 +
|
||||||
|
Config.LINK_PLAY_UNLOCK_LENGTH]
|
||||||
self.room.update_song_unlock()
|
self.room.update_song_unlock()
|
||||||
|
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
|
# 07 可能需要一个 0d 响应,code = 0x0b
|
||||||
|
|
||||||
def command_08(self):
|
def command_08(self):
|
||||||
self.room.round_switch = bi(self.command[24:25])
|
# 可能弃用
|
||||||
self.s.random_code = self.command[16:24]
|
logging.warning('Command 08 is outdated')
|
||||||
self.room.command_queue.append(self.s.command_13())
|
pass
|
||||||
|
# self.room.round_mode = bi(self.command[24:25])
|
||||||
|
# self.s.random_code = self.command[16:24]
|
||||||
|
# self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
def command_09(self):
|
def command_09(self):
|
||||||
re = []
|
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
player = self.room.players[self.player_index]
|
player = self.room.players[self.player_index]
|
||||||
|
|
||||||
@@ -130,131 +159,171 @@ class CommandParser:
|
|||||||
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.append(self.s.command_15())
|
self.room.command_queue.append(self.s.command_15())
|
||||||
else:
|
return None
|
||||||
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
|
||||||
re.append(self.s.command_0c())
|
|
||||||
player.last_timestamp = self.s.timestamp
|
|
||||||
|
|
||||||
# 离线判断
|
flag_0c = False
|
||||||
flag_13, player_index_list = self.room.check_player_online(
|
|
||||||
self.s.timestamp)
|
|
||||||
for i in player_index_list:
|
|
||||||
self.room.command_queue.append(self.s.command_12(i))
|
|
||||||
|
|
||||||
flag_11 = False
|
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
||||||
flag_12 = False
|
flag_0c = True
|
||||||
|
player.last_timestamp = self.s.timestamp
|
||||||
|
|
||||||
if player.online == 0:
|
# 离线判断
|
||||||
flag_12 = True
|
flag_13, player_index_list = self.room.check_player_online(
|
||||||
player.online = 1
|
self.s.timestamp)
|
||||||
|
for i in player_index_list:
|
||||||
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
|
|
||||||
if self.room.is_ready(1, 1):
|
flag_11 = False
|
||||||
|
flag_12 = False
|
||||||
|
|
||||||
|
if player.online == 0:
|
||||||
|
flag_12 = True
|
||||||
|
player.online = 1
|
||||||
|
|
||||||
|
if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8:
|
||||||
|
# 还在结算给踢了
|
||||||
|
# 冗余,为了保险
|
||||||
|
self.room.delete_player(self.player_index)
|
||||||
|
self.room.command_queue.append(
|
||||||
|
self.s.command_12(self.player_index))
|
||||||
|
self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
|
if self.room.is_ready(1, 1) and ((self.room.player_num > 1 and not self.room.is_public) or (self.room.is_public and self.room.player_num == 4)):
|
||||||
|
flag_13 = True
|
||||||
|
self.room.state = 2
|
||||||
|
|
||||||
|
if self.room.state == 1 and self.room.is_public and self.room.player_num > 1 and self.room.should_next_state:
|
||||||
|
flag_0c = True
|
||||||
|
flag_13 = True
|
||||||
|
self.room.state = 2
|
||||||
|
|
||||||
|
if self.room.state in (2, 3) and self.room.player_num < 2:
|
||||||
|
flag_13 = True
|
||||||
|
self.room.state = 1
|
||||||
|
|
||||||
|
if self.room.state == 2 and self.room.should_next_state:
|
||||||
|
flag_0c = True
|
||||||
|
self.room.state = 3
|
||||||
|
flag_13 = True
|
||||||
|
if self.room.round_mode == 3:
|
||||||
|
self.room.make_voting()
|
||||||
|
else:
|
||||||
|
self.room.random_song()
|
||||||
|
|
||||||
|
if player.player_state != self.command[32]:
|
||||||
|
flag_12 = True
|
||||||
|
player.player_state = self.command[32]
|
||||||
|
|
||||||
|
if player.score.difficulty != self.command[33] and player.player_state not in (5, 6, 7, 8):
|
||||||
|
flag_12 = True
|
||||||
|
player.score.difficulty = self.command[33]
|
||||||
|
|
||||||
|
if player.score.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
||||||
|
flag_12 = True
|
||||||
|
player.score.cleartype = self.command[34]
|
||||||
|
|
||||||
|
if player.download_percent != self.command[35]:
|
||||||
|
flag_12 = True
|
||||||
|
player.download_percent = self.command[35]
|
||||||
|
|
||||||
|
if player.character_id != self.command[36]:
|
||||||
|
flag_12 = True
|
||||||
|
player.character_id = self.command[36]
|
||||||
|
|
||||||
|
if player.is_uncapped != self.command[37]:
|
||||||
|
flag_12 = True
|
||||||
|
player.is_uncapped = self.command[37]
|
||||||
|
|
||||||
|
if self.room.state == 3 and player.score.score != bi(self.command[24:28]):
|
||||||
|
flag_12 = True
|
||||||
|
player.score.score = bi(self.command[24:28])
|
||||||
|
|
||||||
|
if self.room.is_ready(3, 4) or (self.room.state == 3 and self.room.should_next_state):
|
||||||
|
flag_13 = True
|
||||||
|
flag_0c = True
|
||||||
|
self.room.state = 4
|
||||||
|
|
||||||
|
if self.room.round_mode == 2:
|
||||||
|
# 将换房主时间提前到此刻
|
||||||
|
self.room.make_round()
|
||||||
|
logging.info(f'Room `{self.room.room_code}` starts playing')
|
||||||
|
|
||||||
|
for p in self.room.players:
|
||||||
|
# 防止提前结算
|
||||||
|
p.finish_flag = 0
|
||||||
|
|
||||||
|
if self.room.state == 4:
|
||||||
|
# 这好像会误判
|
||||||
|
# if player.download_percent < 99:
|
||||||
|
# # 有人没下载完把他踢了!
|
||||||
|
# self.room.delete_player(self.player_index)
|
||||||
|
# self.room.command_queue.append(
|
||||||
|
# self.s.command_12(self.player_index))
|
||||||
|
# self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
|
if self.room.should_next_state:
|
||||||
|
self.room.state = 5
|
||||||
|
flag_11 = True
|
||||||
flag_13 = True
|
flag_13 = True
|
||||||
self.room.state = 2
|
|
||||||
|
|
||||||
if player.player_state != self.command[32]:
|
if self.room.state == 5:
|
||||||
flag_12 = True
|
flag_13 = True
|
||||||
player.player_state = self.command[32]
|
if self.room.is_ready(5, 6):
|
||||||
|
self.room.state = 6
|
||||||
|
if self.room.is_ready(5, 7):
|
||||||
|
self.room.state = 7
|
||||||
|
|
||||||
if player.difficulty != self.command[33] and player.player_state != 5 and player.player_state != 6 and player.player_state != 7 and player.player_state != 8:
|
if self.room.state in (5, 6) and self.room.should_next_state:
|
||||||
flag_12 = True
|
# 此处不清楚
|
||||||
player.difficulty = self.command[33]
|
self.room.state = 7
|
||||||
|
flag_13 = True
|
||||||
|
|
||||||
if player.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
if self.room.state in (7, 8):
|
||||||
flag_12 = True
|
player_now_timer = bi(self.command[28:32])
|
||||||
player.cleartype = self.command[34]
|
if player.score.timer < player_now_timer or player_now_timer == 0 and player.score.timer != 0:
|
||||||
|
player.last_score.timer = player.score.timer
|
||||||
|
player.last_score.score = player.score.score
|
||||||
|
player.score.timer = player_now_timer
|
||||||
|
player.score.score = bi(self.command[24:28])
|
||||||
|
|
||||||
if player.download_percent != self.command[35]:
|
if player.score.timer != 0 or self.room.state != 8:
|
||||||
flag_12 = True
|
for i in self.room.players:
|
||||||
player.download_percent = self.command[35]
|
i.extra_command_queue.append(
|
||||||
|
self.s.command_0e(self.player_index))
|
||||||
|
|
||||||
if player.character_id != self.command[36]:
|
if self.room.is_ready(8, 1):
|
||||||
flag_12 = True
|
|
||||||
player.character_id = self.command[36]
|
|
||||||
|
|
||||||
if player.is_uncapped != self.command[37]:
|
|
||||||
flag_12 = True
|
|
||||||
player.is_uncapped = self.command[37]
|
|
||||||
|
|
||||||
if self.room.state == 3 and player.score != bi(self.command[24:28]):
|
|
||||||
flag_12 = True
|
|
||||||
player.score = bi(self.command[24:28])
|
|
||||||
|
|
||||||
if self.room.is_ready(3, 4):
|
|
||||||
flag_13 = True
|
flag_13 = True
|
||||||
self.room.countdown = Config.COUNTDOWM_TIME
|
self.room.state = 1
|
||||||
self.room.timestamp = round(time.time() * 1000)
|
self.room.song_idx = 0xffff
|
||||||
self.room.state = 4
|
|
||||||
if self.room.round_switch == 1:
|
|
||||||
# 将换房主时间提前到此刻
|
|
||||||
self.room.make_round()
|
|
||||||
|
|
||||||
if self.room.state in (4, 5, 6):
|
if self.room.state == 8 and self.room.should_next_state:
|
||||||
timestamp = round(time.time() * 1000)
|
flag_0c = True
|
||||||
self.room.countdown -= timestamp - self.room.timestamp
|
flag_13 = True
|
||||||
self.room.timestamp = timestamp
|
self.room.state = 1
|
||||||
if self.room.state == 4 and self.room.countdown <= 0:
|
self.room.song_idx = 0xffff
|
||||||
# 此处不清楚
|
|
||||||
self.room.state = 5
|
|
||||||
self.room.countdown = 5999
|
|
||||||
flag_11 = True
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.is_ready(5, 6):
|
if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8:
|
||||||
self.room.state = 6
|
# 还在结算给踢了
|
||||||
flag_13 = True
|
self.room.delete_player(self.player_index)
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.is_ready(5, 7):
|
|
||||||
self.room.state = 7
|
|
||||||
self.room.countdown = 0xffffffff
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.countdown <= 0:
|
|
||||||
print('我怎么知道这是啥')
|
|
||||||
|
|
||||||
if self.room.state == 6 and self.room.countdown <= 0:
|
|
||||||
# 此处不清楚
|
|
||||||
self.room.state = 7
|
|
||||||
self.room.countdown = 0xffffffff
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0
|
|
||||||
|
|
||||||
if self.room.state in (7, 8):
|
|
||||||
if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0:
|
|
||||||
player.last_timer = player.timer
|
|
||||||
player.last_score = player.score
|
|
||||||
player.timer = bi(self.command[28:32])
|
|
||||||
player.score = bi(self.command[24:28])
|
|
||||||
|
|
||||||
if player.timer != 0 or self.room.state != 8:
|
|
||||||
for i in self.room.players:
|
|
||||||
i.extra_command_queue.append(
|
|
||||||
self.s.command_0e(self.player_index))
|
|
||||||
|
|
||||||
if self.room.is_ready(8, 1):
|
|
||||||
flag_13 = True
|
|
||||||
self.room.state = 1
|
|
||||||
self.room.song_idx = 0xffff
|
|
||||||
|
|
||||||
for i in self.room.players:
|
|
||||||
i.timer = 0
|
|
||||||
i.score = 0
|
|
||||||
|
|
||||||
if self.room.is_finish():
|
|
||||||
# 有人退房导致的结算
|
|
||||||
self.room.make_finish()
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if flag_11:
|
|
||||||
self.room.command_queue.append(self.s.command_11())
|
|
||||||
if flag_12:
|
|
||||||
self.room.command_queue.append(
|
self.room.command_queue.append(
|
||||||
self.s.command_12(self.player_index))
|
self.s.command_12(self.player_index))
|
||||||
if flag_13:
|
self.room.command_queue.append(self.s.command_14())
|
||||||
self.room.command_queue.append(self.s.command_13())
|
|
||||||
|
|
||||||
return re
|
if self.room.is_finish():
|
||||||
|
# 有人退房导致的结算
|
||||||
|
self.room.make_finish()
|
||||||
|
flag_13 = True
|
||||||
|
|
||||||
|
if flag_11:
|
||||||
|
self.room.command_queue.append(self.s.command_11())
|
||||||
|
if flag_12:
|
||||||
|
self.room.command_queue.append(
|
||||||
|
self.s.command_12(self.player_index))
|
||||||
|
if flag_13:
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
|
if flag_0c:
|
||||||
|
return [self.s.command_0c()]
|
||||||
|
|
||||||
def command_0a(self):
|
def command_0a(self):
|
||||||
# 退出房间
|
# 退出房间
|
||||||
@@ -262,9 +331,6 @@ class CommandParser:
|
|||||||
|
|
||||||
self.room.command_queue.append(self.s.command_12(self.player_index))
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
|
|
||||||
if self.room.state in (2, 3):
|
|
||||||
self.room.state = 1
|
|
||||||
self.room.song_idx = 0xffff
|
|
||||||
# self.room.command_queue.append(self.s.command_11())
|
# self.room.command_queue.append(self.s.command_11())
|
||||||
self.room.command_queue.append(self.s.command_13())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
@@ -276,3 +342,45 @@ class CommandParser:
|
|||||||
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(
|
||||||
self.s.command_0f(self.player_index, song_idx))
|
self.s.command_0f(self.player_index, song_idx))
|
||||||
|
|
||||||
|
def command_20(self):
|
||||||
|
# 表情
|
||||||
|
sticker_id = bi(self.command[16:18])
|
||||||
|
for i in range(4):
|
||||||
|
if self.player_index != i and self.room.players[i].online == 1:
|
||||||
|
self.room.players[i].extra_command_queue.append(
|
||||||
|
self.s.command_21(self.player_index, sticker_id))
|
||||||
|
|
||||||
|
def command_22(self):
|
||||||
|
# 房间设置,懒得判断房主
|
||||||
|
self.s.random_code = self.command[16:24]
|
||||||
|
self.room.is_public = self.command[25]
|
||||||
|
if self.room.is_public == 0:
|
||||||
|
self.room.round_mode = self.command[24]
|
||||||
|
self.room.timed_mode = self.command[26]
|
||||||
|
else:
|
||||||
|
self.room.round_mode = 3
|
||||||
|
self.room.timed_mode = 1
|
||||||
|
self.room.state = 1
|
||||||
|
self.room.command_queue.append(self.s.command_11())
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
return [self.s.command_0d(1)]
|
||||||
|
|
||||||
|
def command_23(self):
|
||||||
|
# 歌曲投票
|
||||||
|
self.s.random_code = self.command[16:24]
|
||||||
|
if self.room.player_num < 2:
|
||||||
|
return [self.s.command_0d(6)]
|
||||||
|
if self.room.state != 2:
|
||||||
|
return [self.s.command_0d(5)]
|
||||||
|
player = self.room.players[self.player_index]
|
||||||
|
player.voting = bi(self.command[24:26])
|
||||||
|
logging.info(
|
||||||
|
f'Player `{player.name}` votes for song `{player.voting}`')
|
||||||
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
|
|
||||||
|
if self.room.is_all_player_voted:
|
||||||
|
self.room.make_voting()
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
|
return [self.s.command_0d(1)]
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
|
from os import urandom
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .udp_class import Room, b
|
from .udp_class import Room, b
|
||||||
|
|
||||||
|
|
||||||
|
PADDING = [b(i) * i for i in range(16)] + [b'']
|
||||||
|
|
||||||
|
|
||||||
class CommandSender:
|
class CommandSender:
|
||||||
|
|
||||||
PROTOCOL_NAME = b'\x06\x16'
|
PROTOCOL_NAME = b'\x06\x16'
|
||||||
PROTOCOL_VERSION = b'\x09'
|
PROTOCOL_VERSION = b'\x0E'
|
||||||
|
|
||||||
def __init__(self, room: Room = None) -> None:
|
def __init__(self, room: Room = None) -> None:
|
||||||
self.room = room
|
self.room = room
|
||||||
self.timestamp = round(time() * 1000000)
|
self.timestamp = round(time() * 1000000)
|
||||||
|
self.room.timestamp = self.timestamp + 1
|
||||||
|
|
||||||
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
|
self._random_code = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def random_code(self):
|
||||||
|
if self._random_code is None:
|
||||||
|
self._random_code = urandom(4) + b'\x00\x00\x00\x00'
|
||||||
|
return self._random_code
|
||||||
|
|
||||||
|
@random_code.setter
|
||||||
|
def random_code(self, value):
|
||||||
|
self._random_code = value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def command_encode(t: tuple):
|
def command_encode(t: tuple):
|
||||||
r = b''.join(t)
|
r = b''.join(t)
|
||||||
x = 16 - len(r) % 16
|
x = 16 - len(r) % 16
|
||||||
return r + b(x) * x
|
return r + PADDING[x]
|
||||||
|
|
||||||
def command_prefix(self, command: bytes):
|
def command_prefix(self, command: bytes):
|
||||||
length = self.room.command_queue_length
|
length = self.room.command_queue_length
|
||||||
if command >= b'\x10':
|
if b'\x10' <= command <= b'\x1f':
|
||||||
length += 1
|
length += 1
|
||||||
|
|
||||||
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
||||||
@@ -31,12 +46,18 @@ class CommandSender:
|
|||||||
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)))
|
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):
|
||||||
|
# 3 你不是房主
|
||||||
|
# 5 有玩家目前无法开始
|
||||||
|
# 6 需要更多玩家以开始
|
||||||
|
# 7 有玩家无法游玩这首歌
|
||||||
|
|
||||||
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
|
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):
|
||||||
# 分数广播
|
# 分数广播
|
||||||
|
# 我猜,616 写错了,首先 4 个 00 大概是分数使用了 8 bytes 转换,其次上一个分数根本就不需要哈哈哈哈哈哈!
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
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)))
|
return self.command_encode((*self.command_prefix(b'\x0e'), player.info, b(player.last_score.score, 4), b'\x00' * 4, b(player.last_score.timer, 4), b'\x00' * 4))
|
||||||
|
|
||||||
def command_0f(self, player_index: int, song_idx: int):
|
def command_0f(self, player_index: int, song_idx: int):
|
||||||
# 歌曲推荐
|
# 歌曲推荐
|
||||||
@@ -52,13 +73,17 @@ class CommandSender:
|
|||||||
|
|
||||||
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 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)))
|
return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), player.info))
|
||||||
|
|
||||||
def command_13(self):
|
def command_13(self):
|
||||||
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)))
|
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, self.room.room_info))
|
||||||
|
|
||||||
def command_14(self):
|
def command_14(self):
|
||||||
return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
|
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 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)))
|
return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, self.room.room_info))
|
||||||
|
|
||||||
|
def command_21(self, player_index: int, sticker_id: int):
|
||||||
|
player = self.room.players[player_index]
|
||||||
|
return self.command_encode((*self.command_prefix(b'\x21'), b(player.player_id, 8), b(sticker_id, 2)))
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from core.config_manager import Config, ConfigManager
|
|||||||
|
|
||||||
if os.path.exists('config.py') or os.path.exists('config'):
|
if os.path.exists('config.py') or os.path.exists('config'):
|
||||||
# 导入用户自定义配置
|
# 导入用户自定义配置
|
||||||
ConfigManager.load(import_module('config').Config)
|
ConfigManager.load(import_module("config").Config)
|
||||||
|
# TODO: More config file formats
|
||||||
|
|
||||||
if Config.DEPLOY_MODE == 'gevent':
|
if Config.DEPLOY_MODE == 'gevent':
|
||||||
# 异步
|
# 异步
|
||||||
@@ -17,7 +18,7 @@ if Config.DEPLOY_MODE == 'gevent':
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
from multiprocessing import Process, set_start_method
|
from multiprocessing import Process, current_process, set_start_method
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from flask import Flask, make_response, request, send_from_directory
|
from flask import Flask, make_response, request, send_from_directory
|
||||||
@@ -26,6 +27,8 @@ import api
|
|||||||
import server
|
import server
|
||||||
import web.index
|
import web.index
|
||||||
import web.login
|
import web.login
|
||||||
|
# import webapi
|
||||||
|
from core.bundle import BundleDownload
|
||||||
from core.constant import Constant
|
from core.constant import Constant
|
||||||
from core.download import UserDownload
|
from core.download import UserDownload
|
||||||
from core.error import ArcError, NoAccess, RateLimit
|
from core.error import ArcError, NoAccess, RateLimit
|
||||||
@@ -53,7 +56,8 @@ app.config['SESSION_TYPE'] = 'filesystem'
|
|||||||
app.register_blueprint(web.login.bp)
|
app.register_blueprint(web.login.bp)
|
||||||
app.register_blueprint(web.index.bp)
|
app.register_blueprint(web.index.bp)
|
||||||
app.register_blueprint(api.bp)
|
app.register_blueprint(api.bp)
|
||||||
app.register_blueprint(server.bp)
|
list(map(app.register_blueprint, server.get_bps()))
|
||||||
|
# app.register_blueprint(webapi.bp)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -79,7 +83,8 @@ def download(file_path):
|
|||||||
x.song_id, x.file_name = file_path.split('/', 1)
|
x.song_id, x.file_name = file_path.split('/', 1)
|
||||||
x.select_for_check()
|
x.select_for_check()
|
||||||
if x.is_limited:
|
if x.is_limited:
|
||||||
raise RateLimit('You have reached the download limit.', 903)
|
raise RateLimit(
|
||||||
|
f'User `{x.user.user_id}` has reached the download limit.', 903)
|
||||||
if not x.is_valid:
|
if not x.is_valid:
|
||||||
raise NoAccess('Expired token.')
|
raise NoAccess('Expired token.')
|
||||||
x.download_hit()
|
x.download_hit()
|
||||||
@@ -97,6 +102,26 @@ def download(file_path):
|
|||||||
return error_return()
|
return error_return()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/bundle_download/<string:token>', methods=['GET']) # 热更新下载
|
||||||
|
def bundle_download(token: str):
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
try:
|
||||||
|
file_path = BundleDownload(c_m).get_path_by_token(
|
||||||
|
token, request.remote_addr)
|
||||||
|
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||||
|
# nginx X-Accel-Redirect
|
||||||
|
response = make_response()
|
||||||
|
response.headers['Content-Type'] = 'application/octet-stream'
|
||||||
|
response.headers['X-Accel-Redirect'] = Config.BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX + file_path
|
||||||
|
return response
|
||||||
|
return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
||||||
|
except ArcError as e:
|
||||||
|
if Config.ALLOW_WARNING_LOG:
|
||||||
|
app.logger.warning(format_exc())
|
||||||
|
return error_return(e)
|
||||||
|
return error_return()
|
||||||
|
|
||||||
|
|
||||||
if Config.DEPLOY_MODE == 'waitress':
|
if Config.DEPLOY_MODE == 'waitress':
|
||||||
# 给waitress加个日志
|
# 给waitress加个日志
|
||||||
@app.after_request
|
@app.after_request
|
||||||
@@ -122,7 +147,7 @@ def tcp_server_run():
|
|||||||
elif Config.DEPLOY_MODE == 'waitress':
|
elif Config.DEPLOY_MODE == 'waitress':
|
||||||
# waitress WSGI server
|
# waitress WSGI server
|
||||||
import logging
|
import logging
|
||||||
from waitress import serve
|
from waitress import serve # type: ignore
|
||||||
logger = logging.getLogger('waitress')
|
logger = logging.getLogger('waitress')
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
serve(app, host=Config.HOST, port=Config.PORT)
|
serve(app, host=Config.HOST, port=Config.PORT)
|
||||||
@@ -146,7 +171,7 @@ def generate_log_file_dict(level: str, filename: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def pre_main():
|
||||||
log_dict = {
|
log_dict = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'root': {
|
'root': {
|
||||||
@@ -159,7 +184,7 @@ def main():
|
|||||||
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||||
'formatter': 'default'
|
'formatter': 'default'
|
||||||
},
|
},
|
||||||
"error_file": generate_log_file_dict('ERROR', './log/error.log')
|
"error_file": generate_log_file_dict('ERROR', f'{Config.LOG_FOLDER_PATH}/error.log')
|
||||||
},
|
},
|
||||||
'formatters': {
|
'formatters': {
|
||||||
'default': {
|
'default': {
|
||||||
@@ -170,11 +195,11 @@ 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'] = generate_log_file_dict(
|
log_dict['handlers']['info_file'] = generate_log_file_dict(
|
||||||
'INFO', './log/info.log')
|
'INFO', f'{Config.LOG_FOLDER_PATH}/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'] = generate_log_file_dict(
|
log_dict['handlers']['warning_file'] = generate_log_file_dict(
|
||||||
'WARNING', './log/warning.log')
|
'WARNING', f'{Config.LOG_FOLDER_PATH}/warning.log')
|
||||||
|
|
||||||
dictConfig(log_dict)
|
dictConfig(log_dict)
|
||||||
|
|
||||||
@@ -184,6 +209,8 @@ def main():
|
|||||||
input('Press ENTER key to exit.')
|
input('Press ENTER key to exit.')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
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=(
|
||||||
@@ -199,6 +226,11 @@ def main():
|
|||||||
tcp_server_run()
|
tcp_server_run()
|
||||||
|
|
||||||
|
|
||||||
|
# must run for init
|
||||||
|
# this ensures avoiding duplicate init logs for some reason
|
||||||
|
if current_process().name == 'MainProcess':
|
||||||
|
pre_main()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
set_start_method("spawn")
|
set_start_method("spawn")
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
flask>=2.0.2
|
flask>=2.0.2
|
||||||
cryptography>=3.0.0
|
cryptography>=35.0.0
|
||||||
limits>=2.7.0
|
limits>=2.7.0
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
|
|
||||||
from . import (auth, course, friend, multiplayer, others, present, purchase,
|
from . import (auth, course, friend, multiplayer, others, present, purchase,
|
||||||
score, user, world)
|
score, user, world, mission)
|
||||||
|
|
||||||
bp = Blueprint('server', __name__, url_prefix=Config.GAME_API_PREFIX)
|
|
||||||
bp.register_blueprint(user.bp)
|
__bp_old = Blueprint('old_server', __name__)
|
||||||
bp.register_blueprint(auth.bp)
|
|
||||||
bp.register_blueprint(friend.bp)
|
|
||||||
bp.register_blueprint(score.bp)
|
@__bp_old.route('/<path:any>', methods=['GET', 'POST']) # 旧版 API 提示
|
||||||
bp.register_blueprint(world.bp)
|
def server_hello(any):
|
||||||
bp.register_blueprint(purchase.bp)
|
return jsonify({"success": False, "error_code": 5})
|
||||||
bp.register_blueprint(present.bp)
|
|
||||||
bp.register_blueprint(others.bp)
|
|
||||||
bp.register_blueprint(multiplayer.bp)
|
def get_bps():
|
||||||
bp.register_blueprint(course.bp)
|
def string_to_list(s):
|
||||||
|
if isinstance(s, str):
|
||||||
|
s = [s]
|
||||||
|
elif not isinstance(s, list):
|
||||||
|
s = []
|
||||||
|
return s
|
||||||
|
|
||||||
|
bp = Blueprint('server', __name__)
|
||||||
|
list(map(bp.register_blueprint, [user.bp2, user.bp, auth.bp, friend.bp, score.bp,
|
||||||
|
world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp, mission.bp]))
|
||||||
|
|
||||||
|
bps = [Blueprint(x, __name__, url_prefix=x)
|
||||||
|
for x in string_to_list(Config.GAME_API_PREFIX)]
|
||||||
|
for x in bps:
|
||||||
|
x.register_blueprint(bp)
|
||||||
|
|
||||||
|
old_bps = [Blueprint(x, __name__, url_prefix=x)
|
||||||
|
for x in string_to_list(Config.OLD_GAME_API_PREFIX)]
|
||||||
|
for x in old_bps:
|
||||||
|
x.register_blueprint(__bp_old)
|
||||||
|
|
||||||
|
return bps + old_bps
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ def login():
|
|||||||
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/verify', methods=['POST']) # 邮箱验证进度查询
|
|
||||||
@arc_try
|
|
||||||
def email_verify():
|
|
||||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
|
||||||
|
|
||||||
|
|
||||||
def auth_required(req):
|
def auth_required(req):
|
||||||
# arcaea登录验证,写成了修饰器
|
# arcaea登录验证,写成了修饰器
|
||||||
def decorator(view):
|
def decorator(view):
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ from traceback import format_exc
|
|||||||
|
|
||||||
from flask import current_app, g, jsonify
|
from flask import current_app, g, jsonify
|
||||||
|
|
||||||
|
from core.bundle import BundleParser
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
from core.error import ArcError, NoAccess
|
from core.error import ArcError, LowVersion, NoAccess
|
||||||
|
|
||||||
has_arc_hash = False
|
has_arc_hash = False
|
||||||
try:
|
try:
|
||||||
@@ -22,7 +23,9 @@ def error_return(e: ArcError = default_error): # 错误返回
|
|||||||
# -4 您的账号已在别处登录
|
# -4 您的账号已在别处登录
|
||||||
# -3 无法连接至服务器
|
# -3 无法连接至服务器
|
||||||
# 2 Arcaea服务器正在维护
|
# 2 Arcaea服务器正在维护
|
||||||
|
# 5 请更新 Arcaea 到最新版本
|
||||||
# 9 新版本请等待几分钟
|
# 9 新版本请等待几分钟
|
||||||
|
# 11 有游戏内容需要更新,即将返回主界面
|
||||||
# 100 无法在此ip地址下登录游戏
|
# 100 无法在此ip地址下登录游戏
|
||||||
# 101 用户名占用
|
# 101 用户名占用
|
||||||
# 102 电子邮箱已注册
|
# 102 电子邮箱已注册
|
||||||
@@ -41,7 +44,7 @@ def error_return(e: ArcError = default_error): # 错误返回
|
|||||||
# 150 非常抱歉您已被限制使用此功能
|
# 150 非常抱歉您已被限制使用此功能
|
||||||
# 151 目前无法使用此功能
|
# 151 目前无法使用此功能
|
||||||
# 160 账户未邮箱认证,请检查邮箱
|
# 160 账户未邮箱认证,请检查邮箱
|
||||||
# 161 账户认证过期,请重新注册
|
# 161 账户认证过期,请重新注册
|
||||||
# 401 用户不存在
|
# 401 用户不存在
|
||||||
# 403 无法连接至服务器
|
# 403 无法连接至服务器
|
||||||
# 501 502 -6 此物品目前无法获取
|
# 501 502 -6 此物品目前无法获取
|
||||||
@@ -65,7 +68,7 @@ def error_return(e: ArcError = default_error): # 错误返回
|
|||||||
# 9803 下载已取消
|
# 9803 下载已取消
|
||||||
# 9905 没有在云端发现任何数据
|
# 9905 没有在云端发现任何数据
|
||||||
# 9907 更新数据时发生了问题
|
# 9907 更新数据时发生了问题
|
||||||
# 9908 服务器只支持最新的版本,请更新Arcaea
|
# 9908 服务器只支持最新的版本,请更新 Arcaea
|
||||||
# 其它 发生未知错误
|
# 其它 发生未知错误
|
||||||
r = {"success": False, "error_code": e.error_code}
|
r = {"success": False, "error_code": e.error_code}
|
||||||
if e.extra_data:
|
if e.extra_data:
|
||||||
@@ -106,7 +109,9 @@ def header_check(request) -> ArcError:
|
|||||||
headers = request.headers
|
headers = request.headers
|
||||||
if Config.ALLOW_APPVERSION: # 版本检查
|
if Config.ALLOW_APPVERSION: # 版本检查
|
||||||
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||||
return NoAccess('Invalid app version', 1203)
|
return LowVersion('Invalid app version', 5)
|
||||||
|
if request.method == 'GET' and 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version.get(headers.get('AppVersion', ''), '0.0.0'):
|
||||||
|
return LowVersion('Invalid content bundle version', 11)
|
||||||
|
|
||||||
if has_arc_hash and not ArcHashChecker(request).check():
|
if has_arc_hash and not ArcHashChecker(request).check():
|
||||||
return NoAccess('Invalid request')
|
return NoAccess('Invalid request')
|
||||||
|
|||||||
68
latest version/server/mission.py
Normal file
68
latest version/server/mission.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import NoData
|
||||||
|
from core.mission import MISSION_DICT
|
||||||
|
from core.sql import Connect
|
||||||
|
from core.user import UserOnline
|
||||||
|
|
||||||
|
from .auth import auth_required
|
||||||
|
from .func import arc_try, success_return
|
||||||
|
|
||||||
|
bp = Blueprint('mission', __name__, url_prefix='/mission')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mission_form(multidict) -> list:
|
||||||
|
r = []
|
||||||
|
|
||||||
|
x = multidict.get('mission_1')
|
||||||
|
i = 1
|
||||||
|
while x:
|
||||||
|
r.append(x)
|
||||||
|
x = multidict.get(f'mission_{i + 1}')
|
||||||
|
i += 1
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/clear', methods=['POST']) # 新手任务确认完成
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def mission_clear(user_id):
|
||||||
|
m = parse_mission_form(request.form)
|
||||||
|
r = []
|
||||||
|
for i, mission_id in enumerate(m):
|
||||||
|
if mission_id not in MISSION_DICT:
|
||||||
|
return NoData(f'Mission `{mission_id}` not found')
|
||||||
|
with Connect() as c:
|
||||||
|
x = MISSION_DICT[mission_id](c)
|
||||||
|
x.user_clear_mission(UserOnline(c, user_id))
|
||||||
|
d = x.to_dict()
|
||||||
|
d['request_id'] = i + 1
|
||||||
|
r.append(d)
|
||||||
|
|
||||||
|
return success_return({'missions': r})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/claim', methods=['POST']) # 领取新手任务奖励
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def mission_claim(user_id):
|
||||||
|
m = parse_mission_form(request.form)
|
||||||
|
r = []
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
user = UserOnline(c, user_id)
|
||||||
|
|
||||||
|
for i, mission_id in enumerate(m):
|
||||||
|
if mission_id not in MISSION_DICT:
|
||||||
|
return NoData(f'Mission `{mission_id}` not found')
|
||||||
|
|
||||||
|
x = MISSION_DICT[mission_id](c)
|
||||||
|
x.user_claim_mission(user)
|
||||||
|
d = x.to_dict(has_items=True)
|
||||||
|
d['request_id'] = i + 1
|
||||||
|
r.append(d)
|
||||||
|
|
||||||
|
return success_return({
|
||||||
|
'missions': r,
|
||||||
|
'user': user.to_dict()
|
||||||
|
})
|
||||||
@@ -2,7 +2,8 @@ from flask import Blueprint, request
|
|||||||
|
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
from core.error import ArcError
|
from core.error import ArcError
|
||||||
from core.linkplay import Player, RemoteMultiPlayer, Room
|
from core.linkplay import MatchStore, Player, RemoteMultiPlayer, Room
|
||||||
|
from core.notification import RoomInviteNotification
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
@@ -68,3 +69,104 @@ def multiplayer_update(user_id):
|
|||||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
return success_return(r)
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/room/<room_code>/invite', methods=['POST']) # 邀请
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def room_invite(user_id, room_code):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
other_user_id = request.form.get('to', type=int)
|
||||||
|
|
||||||
|
x = RemoteMultiPlayer()
|
||||||
|
share_token = x.select_room(room_code=room_code)['share_token']
|
||||||
|
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
with Connect() as c:
|
||||||
|
sender = Player(c, user_id)
|
||||||
|
sender.select_user_about_link_play()
|
||||||
|
n = RoomInviteNotification.from_sender(
|
||||||
|
sender, Player(c, other_user_id), share_token, c_m)
|
||||||
|
n.insert()
|
||||||
|
|
||||||
|
return success_return({}) # 无返回
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/room/status', methods=['POST']) # 房间号码获取
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def room_status(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
share_token = request.form.get('shareToken', type=str)
|
||||||
|
|
||||||
|
x = RemoteMultiPlayer()
|
||||||
|
room_code = x.select_room(share_token=share_token)['room_code']
|
||||||
|
|
||||||
|
return success_return({
|
||||||
|
'roomId': room_code,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/join/', methods=['POST']) # 匹配
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_join(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
user = Player(None, user_id)
|
||||||
|
user.get_song_unlock(request.json['clientSongMap'])
|
||||||
|
|
||||||
|
x = MatchStore(c)
|
||||||
|
x.init_player(user)
|
||||||
|
r = x.match(user_id)
|
||||||
|
|
||||||
|
if r is None:
|
||||||
|
return success_return({
|
||||||
|
'userId': user_id,
|
||||||
|
'status': 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
r['endPoint'] = request.host.split(
|
||||||
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/status/', methods=['POST']) # 匹配状态,5s 一次
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_status(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
|
||||||
|
r = MatchStore(c).match(user_id)
|
||||||
|
if r is None:
|
||||||
|
return success_return({
|
||||||
|
'userId': user_id,
|
||||||
|
'status': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
r['endPoint'] = request.host.split(
|
||||||
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/leave/', methods=['POST']) # 退出匹配
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_leave(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
MatchStore().clear_player(user_id)
|
||||||
|
|
||||||
|
return success_return({})
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ from urllib.parse import parse_qs, urlparse
|
|||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
|
from core.bundle import BundleDownload
|
||||||
|
from core.character import UserCharacter
|
||||||
from core.download import DownloadList
|
from core.download import DownloadList
|
||||||
from core.error import RateLimit
|
from core.error import ArcError, RateLimit
|
||||||
|
from core.item import ItemCharacter
|
||||||
|
from core.notification import NotificationFactory
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.system import GameInfo
|
from core.system import GameInfo
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
@@ -13,7 +17,7 @@ from core.user import UserOnline
|
|||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, error_return, success_return
|
from .func import arc_try, error_return, success_return
|
||||||
from .present import present_info
|
from .present import present_info
|
||||||
from .purchase import bundle_bundle, bundle_pack
|
from .purchase import bundle_bundle, bundle_pack, get_single
|
||||||
from .score import song_score_friend
|
from .score import song_score_friend
|
||||||
from .user import user_me
|
from .user import user_me
|
||||||
from .world import world_all
|
from .world import world_all
|
||||||
@@ -26,19 +30,44 @@ def game_info():
|
|||||||
return success_return(GameInfo().to_dict())
|
return success_return(GameInfo().to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/notification/me', methods=['GET']) # 通知
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def notification_me(user_id):
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
x = NotificationFactory(c_m, UserOnline(c_m, user_id))
|
||||||
|
return success_return([i.to_dict() for i in x.get_notification()])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/game/content_bundle', methods=['GET']) # 热更新
|
||||||
|
@arc_try
|
||||||
|
def game_content_bundle():
|
||||||
|
# error code 5, 9 work
|
||||||
|
app_version = request.headers.get('AppVersion')
|
||||||
|
bundle_version = request.headers.get('ContentBundle')
|
||||||
|
device_id = request.headers.get('DeviceId')
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
x = BundleDownload(c_m)
|
||||||
|
x.set_client_info(app_version, bundle_version, device_id)
|
||||||
|
return success_return({
|
||||||
|
'orderedResults': x.get_bundle_list()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载
|
@bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载
|
||||||
@auth_required(request)
|
@auth_required(request)
|
||||||
@arc_try
|
@arc_try
|
||||||
def download_song(user_id):
|
def download_song(user_id):
|
||||||
with Connect(in_memory=True) as c:
|
with Connect(in_memory=True) as c_m:
|
||||||
x = DownloadList(c, UserOnline(None, user_id))
|
with Connect() as c:
|
||||||
x.song_ids = request.args.getlist('sid')
|
x = DownloadList(c_m, UserOnline(c, user_id))
|
||||||
x.url_flag = json.loads(request.args.get('url', 'true'))
|
x.song_ids = request.args.getlist('sid')
|
||||||
if x.url_flag and x.is_limited:
|
x.url_flag = json.loads(request.args.get('url', 'true'))
|
||||||
raise RateLimit('You have reached the download limit.', 903)
|
if x.url_flag and x.is_limited:
|
||||||
|
raise RateLimit('You have reached the download limit.', 903)
|
||||||
|
|
||||||
x.add_songs()
|
x.add_songs()
|
||||||
return success_return(x.urls)
|
return success_return(x.urls)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/finale/progress', methods=['GET'])
|
@bp.route('/finale/progress', methods=['GET'])
|
||||||
@@ -48,34 +77,98 @@ def finale_progress():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/finale/finale_start', methods=['POST'])
|
@bp.route('/finale/finale_start', methods=['POST'])
|
||||||
def finale_start():
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def finale_start(user_id):
|
||||||
# testify开始,对立再见
|
# testify开始,对立再见
|
||||||
# 没数据
|
# 但是对立不再见
|
||||||
return success_return({})
|
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemCharacter(c)
|
||||||
|
item.set_id('55') # Hikari (Fatalis)
|
||||||
|
item.user_claim_item(UserOnline(c, user_id))
|
||||||
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/finale/finale_end', methods=['POST'])
|
@bp.route('/finale/finale_end', methods=['POST'])
|
||||||
def finale_end():
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def finale_end(user_id):
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemCharacter(c)
|
||||||
|
item.set_id('5') # Hikari & Tairitsu (Reunion)
|
||||||
|
item.user_claim_item(UserOnline(c, user_id))
|
||||||
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/insight/me/complete/<string:pack_id>', methods=['POST'])
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def insight_complete(user_id, pack_id):
|
||||||
|
# insight state change
|
||||||
|
with Connect() as c:
|
||||||
|
u = UserOnline(c, user_id)
|
||||||
|
if pack_id == 'eden_append_1':
|
||||||
|
item = ItemCharacter(c)
|
||||||
|
item.set_id('72') # Insight (Ascendant - 8th Seeker)
|
||||||
|
item.user_claim_item(u)
|
||||||
|
u.update_user_one_column('insight_state', 1)
|
||||||
|
elif pack_id == 'lephon':
|
||||||
|
u.update_user_one_column('insight_state', 3)
|
||||||
|
else:
|
||||||
|
raise ArcError('Invalid pack_id', 151, status=404)
|
||||||
|
|
||||||
|
return success_return({
|
||||||
|
'insight_state': u.insight_state
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/unlock/me/awaken_maya', methods=['POST'])
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def awaken_maya(user_id):
|
||||||
|
with Connect() as c:
|
||||||
|
ch = UserCharacter(c, 71, UserOnline(c, user_id))
|
||||||
|
ch.select_character_info()
|
||||||
|
try:
|
||||||
|
ch.character_uncap()
|
||||||
|
# ensure no error raised
|
||||||
|
except ArcError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return success_return({
|
||||||
|
'user_id': user_id,
|
||||||
|
'updated_characters': [ch.to_dict()]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/applog/me/log', methods=['POST'])
|
||||||
|
def applog_me():
|
||||||
|
# 异常日志,不处理
|
||||||
return success_return({})
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
map_dict = {'/user/me': user_me,
|
map_dict = {
|
||||||
'/purchase/bundle/pack': bundle_pack,
|
'/user/me': user_me,
|
||||||
'/serve/download/me/song': download_song,
|
'/purchase/bundle/pack': bundle_pack,
|
||||||
'/game/info': game_info,
|
'/serve/download/me/song': download_song,
|
||||||
'/present/me': present_info,
|
'/game/info': game_info,
|
||||||
'/world/map/me': world_all,
|
'/present/me': present_info,
|
||||||
'/score/song/friend': song_score_friend,
|
'/world/map/me': world_all,
|
||||||
'/purchase/bundle/bundle': bundle_bundle,
|
'/score/song/friend': song_score_friend,
|
||||||
'/finale/progress': finale_progress}
|
'/purchase/bundle/bundle': bundle_bundle,
|
||||||
|
'/finale/progress': finale_progress,
|
||||||
|
'/purchase/bundle/single': get_single
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/compose/aggregate', methods=['GET']) # 集成式请求
|
@bp.route('/compose/aggregate', methods=['GET']) # 集成式请求
|
||||||
def aggregate():
|
def aggregate():
|
||||||
try:
|
try:
|
||||||
#global request
|
# global request
|
||||||
finally_response = {'success': True, 'value': []}
|
finally_response = {'success': True, 'value': []}
|
||||||
#request_ = request
|
# request_ = request
|
||||||
get_list = json.loads(request.args.get('calls'))
|
get_list = json.loads(request.args.get('calls'))
|
||||||
if len(get_list) > 10:
|
if len(get_list) > 10:
|
||||||
# 请求太多驳回
|
# 请求太多驳回
|
||||||
@@ -100,13 +193,13 @@ def aggregate():
|
|||||||
'error_code'), 'id': i['id']}
|
'error_code'), 'id': i['id']}
|
||||||
if "extra" in resp:
|
if "extra" in resp:
|
||||||
finally_response['extra'] = resp['extra']
|
finally_response['extra'] = resp['extra']
|
||||||
#request = request_
|
# request = request_
|
||||||
return jsonify(finally_response)
|
return jsonify(finally_response)
|
||||||
|
|
||||||
finally_response['value'].append(
|
finally_response['value'].append(
|
||||||
{'id': i.get('id'), 'value': resp['value'] if hasattr(resp, 'get') else resp})
|
{'id': i.get('id'), 'value': resp['value'] if hasattr(resp, 'get') else resp})
|
||||||
|
|
||||||
#request = request_
|
# request = request_
|
||||||
return jsonify(finally_response)
|
return jsonify(finally_response)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return error_return()
|
return error_return()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from time import time
|
|||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.constant import Constant
|
||||||
from core.error import InputError, ItemUnavailable, PostError
|
from core.error import InputError, ItemUnavailable, PostError
|
||||||
from core.item import ItemFactory, Stamina6
|
from core.item import ItemFactory, Stamina6
|
||||||
from core.purchase import Purchase, PurchaseList
|
from core.purchase import Purchase, PurchaseList
|
||||||
@@ -105,6 +106,8 @@ def buy_special(user_id):
|
|||||||
x.discount_from = -1
|
x.discount_from = -1
|
||||||
x.discount_to = -1
|
x.discount_to = -1
|
||||||
x.items = [ItemFactory(c).get_item(item_id)]
|
x.items = [ItemFactory(c).get_item(item_id)]
|
||||||
|
# request.form['ticket_used'] == 'true'
|
||||||
|
# memory_boost_ticket: x-1
|
||||||
x.buy()
|
x.buy()
|
||||||
|
|
||||||
r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}
|
r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}
|
||||||
@@ -130,7 +133,7 @@ def purchase_stamina(user_id, buy_stamina_type):
|
|||||||
return ItemUnavailable('Buying stamina by fragment is not available yet.', 905)
|
return ItemUnavailable('Buying stamina by fragment is not available yet.', 905)
|
||||||
|
|
||||||
user.update_user_one_column(
|
user.update_user_one_column(
|
||||||
'next_fragstam_ts', now + 24 * 3600 * 1000)
|
'next_fragstam_ts', now + Constant.FRAGSTAM_RECOVER_TICK)
|
||||||
s = Stamina6(c)
|
s = Stamina6(c)
|
||||||
s.user_claim_item(user)
|
s.user_claim_item(user)
|
||||||
return success_return({
|
return success_return({
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from random import randint
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.constant import Constant
|
||||||
from core.course import CoursePlay
|
from core.course import CoursePlay
|
||||||
from core.error import InputError
|
from core.error import InputError
|
||||||
from core.rank import RankList
|
from core.rank import RankList
|
||||||
@@ -25,22 +27,50 @@ def score_token():
|
|||||||
@arc_try
|
@arc_try
|
||||||
def score_token_world(user_id):
|
def score_token_world(user_id):
|
||||||
|
|
||||||
stamina_multiply = int(request.args.get('stamina_multiply', 1))
|
d = request.args.get
|
||||||
fragment_multiply = int(request.args.get('fragment_multiply', 100))
|
|
||||||
prog_boost_multiply = int(request.args.get('prog_boost_multiply', 0))
|
stamina_multiply = d('stamina_multiply', 1, type=int)
|
||||||
beyond_boost_gauge_use = int(request.args.get('beyond_boost_gauge_use', 0))
|
fragment_multiply = d('fragment_multiply', 100, type=int)
|
||||||
|
prog_boost_multiply = d('prog_boost_multiply', 0, type=int)
|
||||||
|
beyond_boost_gauge_use = d('beyond_boost_gauge_use', 0, type=int)
|
||||||
|
skill_cytusii_flag = None
|
||||||
|
skill_chinatsu_flag = None
|
||||||
|
skill_id = d('skill_id')
|
||||||
|
|
||||||
|
if (skill_id == 'skill_ilith_ivy' or skill_id == 'skill_hikari_vanessa') and d('is_skill_sealed') == 'false':
|
||||||
|
# 处理 ivy 技能或者 vanessa 技能
|
||||||
|
# TODO: 需要重构整个 user_play,世界模式 / 课题模式,所以现在临时 work 一下
|
||||||
|
skill_cytusii_flag = ''.join([str(randint(0, 2)) for _ in range(5)])
|
||||||
|
|
||||||
|
if skill_id == 'skill_chinatsu' and d('is_skill_sealed') == 'false':
|
||||||
|
skill_chinatsu_flag = ''.join([str(randint(0, 2)) for _ in range(7)])
|
||||||
|
|
||||||
|
skill_flag = skill_cytusii_flag or skill_chinatsu_flag
|
||||||
|
|
||||||
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(d('song_id'), d('difficulty', type=int))
|
||||||
request.args['difficulty']))
|
|
||||||
x.set_play_state_for_world(
|
x.set_play_state_for_world(
|
||||||
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use)
|
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag, skill_chinatsu_flag)
|
||||||
return success_return({
|
|
||||||
|
r = {
|
||||||
"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,
|
||||||
"token": x.song_token
|
"token": x.song_token,
|
||||||
|
'play_parameters': {},
|
||||||
}
|
}
|
||||||
)
|
if skill_flag and skill_id:
|
||||||
|
r['play_parameters'] = {
|
||||||
|
skill_id: list(
|
||||||
|
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_flag)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.invasion_flag == 1:
|
||||||
|
r['play_parameters']['invasion_start'] = True
|
||||||
|
elif x.invasion_flag == 2:
|
||||||
|
r['play_parameters']['invasion_hard'] = True
|
||||||
|
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
||||||
@@ -87,16 +117,27 @@ def score_token_course(user_id):
|
|||||||
def song_score_post(user_id):
|
def song_score_post(user_id):
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
x = UserPlay(c, UserOnline(c, user_id))
|
x = UserPlay(c, UserOnline(c, user_id))
|
||||||
x.song_token = request.form['song_token']
|
f = request.form
|
||||||
x.song_hash = request.form['song_hash']
|
x.song_token = f['song_token']
|
||||||
x.song.set_chart(
|
x.song_hash = f['song_hash']
|
||||||
request.form['song_id'], request.form['difficulty'])
|
x.song.set_chart(f['song_id'], f['difficulty'])
|
||||||
x.set_score(request.form['score'], request.form['shiny_perfect_count'], request.form['perfect_count'], request.form['near_count'],
|
x.set_score(f['score'], f['shiny_perfect_count'], f['perfect_count'], f['near_count'],
|
||||||
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
|
f['miss_count'], f['health'], f['modifier'], int(time() * 1000), f['clear_type'])
|
||||||
x.beyond_gauge = int(request.form['beyond_gauge'])
|
x.beyond_gauge = int(f['beyond_gauge'])
|
||||||
x.submission_hash = request.form['submission_hash']
|
x.submission_hash = f['submission_hash']
|
||||||
if 'combo_interval_bonus' in request.form:
|
x.combo_interval_bonus = f.get('combo_interval_bonus', type=int)
|
||||||
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
|
x.hp_interval_bonus = f.get('hp_interval_bonus', type=int)
|
||||||
|
# visible_map_count
|
||||||
|
x.fever_bonus = f.get('fever_bonus', type=int)
|
||||||
|
x.rank_bonus = f.get('rank_bonus', type=int)
|
||||||
|
x.maya_gauge = f.get('maya_gauge', type=int)
|
||||||
|
x.nextstage_bonus = f.get('nextstage_bonus', type=int)
|
||||||
|
x.highest_health = f.get("highest_health", type=int)
|
||||||
|
x.lowest_health = f.get("lowest_health", type=int)
|
||||||
|
x.room_code = f.get('room_code')
|
||||||
|
x.room_total_score = f.get('room_total_score', type=int)
|
||||||
|
x.room_total_players = f.get('room_total_players', type=int)
|
||||||
|
|
||||||
if not x.is_valid:
|
if not x.is_valid:
|
||||||
raise InputError('Invalid score.', 107)
|
raise InputError('Invalid score.', 107)
|
||||||
x.upload_score()
|
x.upload_score()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from flask import Blueprint, current_app, request
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.character import UserCharacter
|
from core.character import UserCharacter
|
||||||
|
from core.config_manager import Config
|
||||||
from core.error import ArcError
|
from core.error import ArcError
|
||||||
from core.item import ItemCore
|
from core.item import ItemCore
|
||||||
|
from core.operation import DeleteOneUser
|
||||||
from core.save import SaveData
|
from core.save import SaveData
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import User, UserLogin, UserOnline, UserRegister
|
from core.user import User, UserLogin, UserOnline, UserRegister
|
||||||
@@ -12,8 +14,11 @@ from .func import arc_try, header_check, success_return
|
|||||||
|
|
||||||
bp = Blueprint('user', __name__, url_prefix='/user')
|
bp = Blueprint('user', __name__, url_prefix='/user')
|
||||||
|
|
||||||
|
bp2 = Blueprint('account', __name__, url_prefix='/account')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('', methods=['POST']) # 注册接口
|
@bp.route('', methods=['POST']) # 注册接口
|
||||||
|
@bp2.route('', methods=['POST'])
|
||||||
@arc_try
|
@arc_try
|
||||||
def register():
|
def register():
|
||||||
error = header_check(request)
|
error = header_check(request)
|
||||||
@@ -29,13 +34,15 @@ def register():
|
|||||||
device_id = request.form['device_id']
|
device_id = request.form['device_id']
|
||||||
else:
|
else:
|
||||||
device_id = 'low_version'
|
device_id = 'low_version'
|
||||||
|
if 'is_allow_marketing_email' in request.form:
|
||||||
|
new_user.is_allow_marketing_email = request.form['is_allow_marketing_email'] == 'true'
|
||||||
|
|
||||||
new_user.register()
|
ip = request.remote_addr
|
||||||
|
new_user.register(device_id, ip)
|
||||||
|
|
||||||
# 注册后自动登录
|
# 注册后自动登录
|
||||||
user = UserLogin(c)
|
user = UserLogin(c)
|
||||||
user.login(new_user.name, new_user.password,
|
user.login(new_user.name, new_user.password, device_id, ip)
|
||||||
device_id, request.remote_addr)
|
|
||||||
current_app.logger.info(f'New user `{user.user_id}` registered')
|
current_app.logger.info(f'New user `{user.user_id}` registered')
|
||||||
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
||||||
|
|
||||||
@@ -48,6 +55,16 @@ def user_me(user_id):
|
|||||||
return success_return(UserOnline(c, user_id).to_dict())
|
return success_return(UserOnline(c, user_id).to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/toggle_invasion', methods=['POST']) # insight skill
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def toggle_invasion(user_id):
|
||||||
|
with Connect() as c:
|
||||||
|
user = UserOnline(c, user_id)
|
||||||
|
user.toggle_invasion()
|
||||||
|
return success_return({'user_id': user.user_id, 'insight_state': user.insight_state})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/me/character', methods=['POST']) # 角色切换
|
@bp.route('/me/character', methods=['POST']) # 角色切换
|
||||||
@auth_required(request)
|
@auth_required(request)
|
||||||
@arc_try
|
@arc_try
|
||||||
@@ -155,19 +172,31 @@ def sys_set(user_id, set_arg):
|
|||||||
user.change_favorite_character(int(value))
|
user.change_favorite_character(int(value))
|
||||||
else:
|
else:
|
||||||
value = 'true' == value
|
value = 'true' == value
|
||||||
if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'):
|
if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled', 'mp_notification_enabled', 'is_allow_marketing_email'):
|
||||||
user.update_user_one_column(set_arg, value)
|
user.update_user_one_column(set_arg, value)
|
||||||
return success_return(user.to_dict())
|
return success_return(user.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/me/request_delete', methods=['POST']) # 删除账号
|
@bp.route('/me/request_delete', methods=['POST']) # 删除账号
|
||||||
|
@bp2.route('/me/request_delete', methods=['POST'])
|
||||||
@auth_required(request)
|
@auth_required(request)
|
||||||
@arc_try
|
@arc_try
|
||||||
def user_delete(user_id):
|
def user_delete(user_id):
|
||||||
raise ArcError('Cannot delete the account.', 151, status=404)
|
if not Config.ALLOW_SELF_ACCOUNT_DELETE:
|
||||||
|
raise ArcError('Cannot delete the account.', 151, status=404)
|
||||||
|
DeleteOneUser().set_params(user_id).run()
|
||||||
|
return success_return({'user_id': user_id})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
||||||
|
@bp2.route('/email/resend_verify', methods=['POST'])
|
||||||
@arc_try
|
@arc_try
|
||||||
def email_resend_verify():
|
def email_resend_verify():
|
||||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/verify', methods=['POST']) # 邮箱验证状态查询
|
||||||
|
@bp2.route('/verify', methods=['POST'])
|
||||||
|
@arc_try
|
||||||
|
def email_verify():
|
||||||
|
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from flask import Blueprint, request
|
|||||||
|
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
from core.world import UserMap, get_world_all
|
from core.world import MapParser, UserMap
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
@@ -20,7 +20,7 @@ def world_all(user_id):
|
|||||||
return success_return({
|
return success_return({
|
||||||
"current_map": user.current_map.map_id,
|
"current_map": user.current_map.map_id,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in get_world_all(c, user)]
|
"maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in MapParser.get_world_all(c, user)]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ input[type=submit] {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.difficulty_etr {
|
||||||
|
font-size: 0.9em;
|
||||||
|
background-color: rgb(161, 132, 181);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.rank {
|
.rank {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
<span class="difficulty_byd">BYD</span>
|
<span class="difficulty_byd">BYD</span>
|
||||||
<span class="song-rating">{{song['rating_byn']}}</span>
|
<span class="song-rating">{{song['rating_byn']}}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<br />
|
||||||
|
{% if song['rating_etr'] %}
|
||||||
|
<span class="difficulty_etr">ETR</span>
|
||||||
|
<span class="song-rating">{{song['rating_etr']}}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<option value='fragment'>Fragment</option>
|
<option value='fragment'>Fragment</option>
|
||||||
<option value='memory'>Memory</option>
|
<option value='memory'>Memory</option>
|
||||||
<option value='anni5tix'>Anniversary 5 ticket</option>
|
<option value='anni5tix'>Anniversary 5 ticket</option>
|
||||||
|
<option value='pick_ticket'>Pick ticket</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="amount">Amount</label>
|
<label for="amount">Amount</label>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<option value='fragment'>Fragment</option>
|
<option value='fragment'>Fragment</option>
|
||||||
<option value='memory'>Memory</option>
|
<option value='memory'>Memory</option>
|
||||||
<option value='anni5tix'>Anniversary 5 ticket</option>
|
<option value='anni5tix'>Anniversary 5 ticket</option>
|
||||||
|
<option value='pick_ticket'>Pick ticket</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="amount">Amount</label>
|
<label for="amount">Amount</label>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<option value="1">Present</option>
|
<option value="1">Present</option>
|
||||||
<option value="2">Future</option>
|
<option value="2">Future</option>
|
||||||
<option value="3">Beyond</option>
|
<option value="3">Beyond</option>
|
||||||
|
<option value="4">Eternal</option>
|
||||||
</select>
|
</select>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<input name="rating_ftr" id="rating_ftr" required>
|
<input name="rating_ftr" id="rating_ftr" required>
|
||||||
<label for="rating_byd">Beyond chart const</label>
|
<label for="rating_byd">Beyond chart const</label>
|
||||||
<input name="rating_byd" id="rating_byd" required>
|
<input name="rating_byd" id="rating_byd" required>
|
||||||
|
<label for="rating_etr">Eternal chart const</label>
|
||||||
|
<input name="rating_etr" id="rating_etr" required>
|
||||||
<div class="content">如果没有某个谱面,应该填入-1。</div>
|
<div class="content">如果没有某个谱面,应该填入-1。</div>
|
||||||
<div class="content">If there is no some chart, fill in -1 please.</div>
|
<div class="content">If there is no some chart, fill in -1 please.</div>
|
||||||
<input type="submit" value="Add">
|
<input type="submit" value="Add">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<option value="1">Present</option>
|
<option value="1">Present</option>
|
||||||
<option value="2">Future</option>
|
<option value="2">Future</option>
|
||||||
<option value="3">Beyond</option>
|
<option value="3">Beyond</option>
|
||||||
|
<option value="4">Eternal</option>
|
||||||
</select>
|
</select>
|
||||||
<br />
|
<br />
|
||||||
<input type="submit" value="Find">
|
<input type="submit" value="Find">
|
||||||
@@ -33,6 +34,8 @@
|
|||||||
<span class="difficulty_prs">PRS</span>
|
<span class="difficulty_prs">PRS</span>
|
||||||
{% elif difficulty == 2 %}
|
{% elif difficulty == 2 %}
|
||||||
<span class="difficulty_ftr">FTR</span>
|
<span class="difficulty_ftr">FTR</span>
|
||||||
|
{% elif difficulty == 4 %}
|
||||||
|
<span class="difficulty_etr">ETR</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="difficulty_byd">BYD</span>
|
<span class="difficulty_byd">BYD</span>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
<span class="difficulty_prs">PRS</span>
|
<span class="difficulty_prs">PRS</span>
|
||||||
{% elif post['difficulty'] == 2 %}
|
{% elif post['difficulty'] == 2 %}
|
||||||
<span class="difficulty_ftr">FTR</span>
|
<span class="difficulty_ftr">FTR</span>
|
||||||
|
{% elif post['difficulty'] == 4 %}
|
||||||
|
<span class="difficulty_etr">ETR</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="difficulty_byd">BYD</span>
|
<span class="difficulty_byd">BYD</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
<span class="difficulty_prs">PRS</span>
|
<span class="difficulty_prs">PRS</span>
|
||||||
{% elif user['difficulty'] == 2 %}
|
{% elif user['difficulty'] == 2 %}
|
||||||
<span class="difficulty_ftr">FTR</span>
|
<span class="difficulty_ftr">FTR</span>
|
||||||
|
{% elif user['difficulty'] == 4 %}
|
||||||
|
<span class="difficulty_etr">ETR</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="difficulty_byd">BYD</span>
|
<span class="difficulty_byd">BYD</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -176,6 +178,8 @@
|
|||||||
<span class="difficulty_prs">PRS</span>
|
<span class="difficulty_prs">PRS</span>
|
||||||
{% elif i['difficulty'] == 2 %}
|
{% elif i['difficulty'] == 2 %}
|
||||||
<span class="difficulty_ftr">FTR</span>
|
<span class="difficulty_ftr">FTR</span>
|
||||||
|
{% elif i['difficulty'] == 4 %}
|
||||||
|
<span class="difficulty_etr">ETR</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="difficulty_byd">BYD</span>
|
<span class="difficulty_byd">BYD</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
这里可以将旧版本的数据库同步到新版本的数据库,并刷新用户拥有的全角色列表。<br />
|
这里可以将旧版本的数据库同步到新版本的数据库,并刷新用户拥有的全角色列表。<br />
|
||||||
可上传文件: arcaea_database.db<br />
|
可上传文件: arcaea_database.db<br />
|
||||||
新数据库不存在的数据会被添加,存在的重复数据也会被改变。<br /><br />
|
新数据库不存在的数据会被添加,存在的重复数据也会被改变。<br /><br />
|
||||||
Here you can synchronize the old version of the database to the new version of the database and refresh the list of full
|
Here you can synchronize the old version of the database to the new version of the database and refresh the list of
|
||||||
|
full
|
||||||
characters owned by players.<br />
|
characters owned by players.<br />
|
||||||
Uploadable files: arcaea_database.db<br />
|
Uploadable files: arcaea_database.db<br />
|
||||||
Data that does not exist in the new database will be added and the existing duplicate data will also be changed.
|
Data that does not exist in the new database will be added and the existing duplicate data will also be changed.
|
||||||
@@ -40,4 +41,16 @@
|
|||||||
<div class="content">Here you can refresh the ratings of all scores in the database. The purpose is to deal
|
<div class="content">Here you can refresh the ratings of all scores in the database. The purpose is to deal
|
||||||
with the updating of songs' chart consts.</div>
|
with the updating of songs' chart consts.</div>
|
||||||
</form>
|
</form>
|
||||||
|
<br />
|
||||||
|
<hr />
|
||||||
|
<form action="/web/updatedatabase/refreshsbundle" method="post">
|
||||||
|
<div class="title">Refresh content bundles</div>
|
||||||
|
<br />
|
||||||
|
<input type="submit" value="Refresh">
|
||||||
|
|
||||||
|
<div class="content">这里可以刷新储存在内存中的 bundle 文件夹下所有热更新文件的信息。目的是应对热更新文件的修改。</div>
|
||||||
|
<div class="content">Here you can refresh the information of all the files stored in the bundle folder in memory.
|
||||||
|
The purpose
|
||||||
|
is to deal with the updating of content bundles.</div>
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from core.init import FileChecker
|
|
||||||
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
|
|
||||||
from core.rank import RankList
|
|
||||||
from core.sql import Connect
|
|
||||||
from core.user import User
|
|
||||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
import web.system
|
import web.system
|
||||||
import web.webscore
|
import web.webscore
|
||||||
|
from core.init import FileChecker
|
||||||
|
from core.operation import (DeleteUserScore, RefreshAllScoreRating,
|
||||||
|
RefreshBundleCache, RefreshSongFileCache,
|
||||||
|
SaveUpdateScore, UnlockUserItem)
|
||||||
|
from core.rank import RankList
|
||||||
|
from core.score import Potential
|
||||||
|
from core.sql import Connect
|
||||||
|
from core.user import User
|
||||||
from web.login import login_required
|
from web.login import login_required
|
||||||
|
|
||||||
UPLOAD_FOLDER = 'database'
|
UPLOAD_FOLDER = 'database'
|
||||||
@@ -97,8 +100,11 @@ def single_player_ptt():
|
|||||||
user_id = user_id[0]
|
user_id = user_id[0]
|
||||||
user = web.webscore.get_user(c, user_id)
|
user = web.webscore.get_user(c, user_id)
|
||||||
posts = web.webscore.get_user_score(c, user_id, 30)
|
posts = web.webscore.get_user_score(c, user_id, 30)
|
||||||
recent, recentptt = web.webscore.get_user_recent30(
|
u = User()
|
||||||
c, user_id)
|
u.user_id = user_id
|
||||||
|
p = Potential(c, u)
|
||||||
|
recentptt = p.recent_10 / 10
|
||||||
|
recent = p.recent_30_to_dict_list()
|
||||||
if not posts:
|
if not posts:
|
||||||
error = '无成绩 No score.'
|
error = '无成绩 No score.'
|
||||||
else:
|
else:
|
||||||
@@ -191,13 +197,15 @@ def all_song():
|
|||||||
if x:
|
if x:
|
||||||
posts = []
|
posts = []
|
||||||
for i in x:
|
for i in x:
|
||||||
posts.append({'song_id': i[0],
|
posts.append({
|
||||||
'name_en': i[1],
|
'song_id': i[0],
|
||||||
'rating_pst': defnum(i[2]),
|
'name_en': i[1],
|
||||||
'rating_prs': defnum(i[3]),
|
'rating_pst': defnum(i[2]),
|
||||||
'rating_ftr': defnum(i[4]),
|
'rating_prs': defnum(i[3]),
|
||||||
'rating_byn': defnum(i[5])
|
'rating_ftr': defnum(i[4]),
|
||||||
})
|
'rating_byn': defnum(i[5]),
|
||||||
|
'rating_etr': defnum(i[6])
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
error = '没有谱面数据 No song data.'
|
error = '没有谱面数据 No song data.'
|
||||||
|
|
||||||
@@ -297,6 +305,18 @@ def update_song_hash():
|
|||||||
return render_template('web/updatedatabase.html')
|
return render_template('web/updatedatabase.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/updatedatabase/refreshsbundle', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_content_bundle():
|
||||||
|
# 更新 bundle
|
||||||
|
try:
|
||||||
|
RefreshBundleCache().run()
|
||||||
|
flash('数据刷新成功 Success refresh data.')
|
||||||
|
except:
|
||||||
|
flash('Something error!')
|
||||||
|
return render_template('web/updatedatabase.html')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/updatedatabase/refreshsongrating', methods=['POST'])
|
@bp.route('/updatedatabase/refreshsongrating', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_song_rating():
|
def update_song_rating():
|
||||||
@@ -335,6 +355,7 @@ def add_song():
|
|||||||
rating_prs = get_rating(request.form['rating_prs'])
|
rating_prs = get_rating(request.form['rating_prs'])
|
||||||
rating_ftr = get_rating(request.form['rating_ftr'])
|
rating_ftr = get_rating(request.form['rating_ftr'])
|
||||||
rating_byd = get_rating(request.form['rating_byd'])
|
rating_byd = get_rating(request.form['rating_byd'])
|
||||||
|
rating_etr = get_rating(request.form['rating_etr'])
|
||||||
if len(song_id) >= 256:
|
if len(song_id) >= 256:
|
||||||
song_id = song_id[:200]
|
song_id = song_id[:200]
|
||||||
if len(name_en) >= 256:
|
if len(name_en) >= 256:
|
||||||
@@ -344,8 +365,8 @@ def add_song():
|
|||||||
c.execute(
|
c.execute(
|
||||||
'''select exists(select * from chart where song_id=:a)''', {'a': song_id})
|
'''select exists(select * from chart where song_id=:a)''', {'a': song_id})
|
||||||
if c.fetchone() == (0,):
|
if c.fetchone() == (0,):
|
||||||
c.execute('''insert into chart values(:a,:b,:c,:d,:e,:f)''', {
|
c.execute('''insert into chart values(:a,:b,:c,:d,:e,:f,:g)''', {
|
||||||
'a': song_id, 'b': name_en, 'c': rating_pst, 'd': rating_prs, 'e': rating_ftr, 'f': rating_byd})
|
'a': song_id, 'b': name_en, 'c': rating_pst, 'd': rating_prs, 'e': rating_ftr, 'f': rating_byd, 'g': rating_etr})
|
||||||
flash('歌曲添加成功 Successfully add the song.')
|
flash('歌曲添加成功 Successfully add the song.')
|
||||||
else:
|
else:
|
||||||
error = '歌曲已存在 The song exists.'
|
error = '歌曲已存在 The song exists.'
|
||||||
@@ -422,7 +443,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', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
|
'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', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid', 'skill_hikari_selene', 'skill_hikari_clear', 'skill_tairitsu_fail', 'skill_nami_sui', 'skill_nonoka', 'skill_nonoka_uncap', 'skill_vita_arc', 'skill_maya_uncap', 'skill_hikari_tairitsu_debut', 'skill_hp_slow_drain', 'skill_lost_to_85', 'skill_hprate_based_on_hp', 'skill_frag_doubled_after_earning_X']
|
||||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||||
|
|
||||||
|
|
||||||
@@ -1366,7 +1387,10 @@ def delete_user_score():
|
|||||||
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.clear_user_score(c, user_id)
|
c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''', {
|
||||||
|
'a': user_id})
|
||||||
|
c.connection.commit()
|
||||||
|
DeleteUserScore().set_params(user_id=user_id).run()
|
||||||
flash("用户成绩删除成功 Successfully delete the user's scores.")
|
flash("用户成绩删除成功 Successfully delete the user's scores.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def random_str(randomlength=10):
|
|||||||
|
|
||||||
|
|
||||||
def update_user_char(c):
|
def update_user_char(c):
|
||||||
# 用character数据更新user_char
|
# 用character数据更新user_char_full
|
||||||
c.execute('''select character_id, max_level, is_uncapped from character''')
|
c.execute('''select character_id, max_level, is_uncapped from character''')
|
||||||
x = c.fetchall()
|
x = c.fetchall()
|
||||||
c.execute('''select user_id from user''')
|
c.execute('''select user_id from user''')
|
||||||
@@ -36,7 +36,7 @@ def update_user_char(c):
|
|||||||
c.execute('''delete from user_char_full where user_id=:a and character_id=:b''', {
|
c.execute('''delete from user_char_full where user_id=:a and character_id=:b''', {
|
||||||
'a': j[0], 'b': i[0]})
|
'a': j[0], 'b': i[0]})
|
||||||
exp = 25000 if i[1] == 30 else 10000
|
exp = 25000 if i[1] == 30 else 10000
|
||||||
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
|
c.execute('''insert into user_char_full values(?,?,?,?,?,?,?)''',
|
||||||
(j[0], i[0], i[1], exp, i[2], 0))
|
(j[0], i[0], i[1], exp, i[2], 0))
|
||||||
|
|
||||||
|
|
||||||
@@ -261,12 +261,3 @@ def ban_one_user(c, user_id):
|
|||||||
{'a': user_id})
|
{'a': user_id})
|
||||||
c.execute('''delete from login where user_id=:a''', {'a': user_id})
|
c.execute('''delete from login where user_id=:a''', {'a': user_id})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def clear_user_score(c, user_id):
|
|
||||||
# 清除用户所有成绩,包括best_score和recent30,以及recent数据,但不包括云端存档
|
|
||||||
c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''',
|
|
||||||
{'a': user_id})
|
|
||||||
c.execute('''delete from best_score where user_id=:a''', {'a': user_id})
|
|
||||||
c.execute('''delete from recent30 where user_id=:a''', {'a': user_id})
|
|
||||||
return
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
from core.score import Potential
|
||||||
|
|
||||||
|
|
||||||
def get_user_score(c, user_id, limit=-1, offset=0):
|
def get_user_score(c, user_id, limit=-1, offset=0):
|
||||||
# 返回用户的所有歌曲数据,带排名,返回字典列表
|
# 返回用户的所有歌曲数据,带排名,返回字典列表
|
||||||
@@ -74,37 +76,3 @@ def get_user(c, user_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def get_user_recent30(c, user_id):
|
|
||||||
# 获取玩家recent30信息并计算recent10的ptt,返回字典列表和一个值
|
|
||||||
c.execute('''select * from recent30 where user_id=:a''', {'a': user_id})
|
|
||||||
sumr = 0
|
|
||||||
x = c.fetchone()
|
|
||||||
r = []
|
|
||||||
if x is not None:
|
|
||||||
r30 = []
|
|
||||||
s30 = []
|
|
||||||
for i in range(1, 61, 2):
|
|
||||||
if x[i] is not None:
|
|
||||||
r30.append(float(x[i]))
|
|
||||||
s30.append(x[i+1])
|
|
||||||
else:
|
|
||||||
r30.append(0)
|
|
||||||
s30.append('')
|
|
||||||
r30, s30 = (list(t) for t in zip(*sorted(zip(r30, s30), reverse=True)))
|
|
||||||
songs = []
|
|
||||||
i = 0
|
|
||||||
while len(songs) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None:
|
|
||||||
if s30[i] not in songs:
|
|
||||||
sumr += r30[i]
|
|
||||||
songs.append(s30[i])
|
|
||||||
i += 1
|
|
||||||
for i in range(0, 30):
|
|
||||||
if s30[i]:
|
|
||||||
r.append({
|
|
||||||
'song_id': s30[i][:-1],
|
|
||||||
'difficulty': int(s30[i][-1]),
|
|
||||||
'rating': r30[i]
|
|
||||||
})
|
|
||||||
return r, sumr / 10
|
|
||||||
|
|||||||
19
tools/server_maintenance.py
Normal file
19
tools/server_maintenance.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from flask import Flask, jsonify
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
HOST = '0.0.0.0'
|
||||||
|
PORT = '80'
|
||||||
|
|
||||||
|
@app.route('/favicon.ico', methods=['GET'])
|
||||||
|
def favicon():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@app.route('/<path:p>')
|
||||||
|
def hello(p):
|
||||||
|
r = {"success": False, "error_code": 2}
|
||||||
|
return jsonify(r)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host=HOST, port=PORT)
|
||||||
@@ -34,7 +34,7 @@ class Connect():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def insert(cursor, song_id, name, a, b, c, d, update_type=0):
|
def insert(cursor, song_id, name, a, b, c, d, e, update_type=0):
|
||||||
'''Insert a new song into database.'''
|
'''Insert a new song into database.'''
|
||||||
if update_type == 0 or update_type == 1:
|
if update_type == 0 or update_type == 1:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -45,12 +45,12 @@ def insert(cursor, song_id, name, a, b, c, d, update_type=0):
|
|||||||
return
|
return
|
||||||
elif update_type == 1:
|
elif update_type == 1:
|
||||||
# 重复则更新,以`arcsong.db`数据为准
|
# 重复则更新,以`arcsong.db`数据为准
|
||||||
cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''',
|
cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=?, rating_etr=? where song_id=?''',
|
||||||
(name, a, b, c, d, song_id))
|
(name, a, b, c, d, e, song_id))
|
||||||
return
|
return
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
'''insert into chart values (?,?,?,?,?,?)''', (song_id, name, a, b, c, d))
|
'''insert into chart values (?,?,?,?,?,?,?)''', (song_id, name, a, b, c, d, e))
|
||||||
|
|
||||||
|
|
||||||
def from_song_datebase():
|
def from_song_datebase():
|
||||||
@@ -61,17 +61,19 @@ def from_song_datebase():
|
|||||||
if 'songs' in tables:
|
if 'songs' in tables:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''')
|
'''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''')
|
||||||
data = c.fetchall()
|
data = []
|
||||||
|
for x in c.fetchall():
|
||||||
|
data.append((x[0], x[1], x[2], x[3], x[4], x[5], -1))
|
||||||
elif 'charts' in tables:
|
elif 'charts' in tables:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''select song_id, rating_class, name_en, rating from charts''')
|
'''select song_id, rating_class, name_en, rating from charts''')
|
||||||
songs = {}
|
songs = {}
|
||||||
for song_id, rating_class, name_en, rating in c.fetchall():
|
for song_id, rating_class, name_en, rating in c.fetchall():
|
||||||
if song_id not in songs:
|
if song_id not in songs:
|
||||||
songs[song_id] = [-1, -1, -1, -1, name_en]
|
songs[song_id] = [-1, -1, -1, -1, -1, name_en]
|
||||||
songs[song_id][rating_class] = rating
|
songs[song_id][rating_class] = rating
|
||||||
|
|
||||||
data = [(x, y[-1], y[0], y[1], y[2], y[3])
|
data = [(x, y[-1], y[0], y[1], y[2], y[3], y[4])
|
||||||
for x, y in songs.items()]
|
for x, y in songs.items()]
|
||||||
else:
|
else:
|
||||||
print('Error: Cannot find table `songs` or `charts` in the database.')
|
print('Error: Cannot find table `songs` or `charts` in the database.')
|
||||||
@@ -91,7 +93,7 @@ def from_song_datebase():
|
|||||||
# 清空数据表后更新
|
# 清空数据表后更新
|
||||||
c.execute('''delete from chart''')
|
c.execute('''delete from chart''')
|
||||||
for x in data:
|
for x in data:
|
||||||
insert(c, x[0], x[1], x[2], x[3], x[4], x[5], update_type)
|
insert(c, x[0], x[1], x[2], x[3], x[4], x[5], x[6], update_type)
|
||||||
|
|
||||||
print('Seems to be done.')
|
print('Seems to be done.')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user