mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-09 09:17:26 +08:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6663ac0dc | ||
|
|
de1d46f96e | ||
|
|
f3c17cdde7 | ||
|
|
150686d9f8 | ||
|
|
3e93082a3c | ||
|
|
04010d9c33 | ||
|
|
1f389e4073 | ||
|
|
5788972692 | ||
|
|
f373c7b052 | ||
|
|
04c6d1a0e0 | ||
|
|
41409dee27 | ||
|
|
4527339d78 | ||
|
|
5a4ff11578 | ||
|
|
18e79a7799 | ||
|
|
e548d5639f | ||
|
|
bd74d96250 | ||
|
|
880b66a995 | ||
|
|
8efd11cd02 | ||
|
|
1c58aeb526 | ||
|
|
6f82e9b269 | ||
|
|
f92d8a9642 | ||
|
|
1672d337ff | ||
|
|
70d27be7c7 | ||
|
|
8d856696ca | ||
|
|
ed8d0aa73c | ||
|
|
a84ec560dd | ||
|
|
930faf508d | ||
|
|
7ece2598d1 | ||
|
|
6f39274b99 | ||
|
|
e21cf89b1b | ||
|
|
fbd5d83626 | ||
|
|
9fbdcd5edb | ||
|
|
88d949fc18 | ||
|
|
9636722709 | ||
|
|
9c90d6ef89 | ||
|
|
af03a48134 | ||
|
|
1fde2801ad | ||
|
|
f250701eca | ||
|
|
627985d1af | ||
|
|
7324c9bd76 | ||
|
|
426f65ea9e | ||
|
|
a7a9a4ba3d | ||
|
|
84b0e869a5 | ||
|
|
bd9eb28832 | ||
|
|
e3d5c19569 | ||
|
|
e1ce4d9ec4 | ||
|
|
b70bfd6081 | ||
|
|
7cc17181e1 | ||
|
|
e22485e4e0 | ||
|
|
65aed56c3e | ||
|
|
e6cb0f8981 | ||
|
|
4a5083c597 | ||
|
|
78d28f8886 | ||
|
|
3fde61c639 | ||
|
|
cf350842ac | ||
|
|
e04c8ecf2d | ||
|
|
4da3f38f6e | ||
|
|
40630fff4d | ||
|
|
ca03360f0c | ||
|
|
d150553e6b | ||
|
|
ba36190f30 | ||
|
|
68a83a29d2 |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,5 +1,23 @@
|
|||||||
|
# log files
|
||||||
*.log
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
|
# SSL cert
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# sqlite3 database
|
||||||
|
*.db
|
||||||
|
*.db.bak*
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
|
# setting/config files
|
||||||
|
latest version/config/
|
||||||
|
latest version/config.py
|
||||||
|
|
||||||
|
# song data
|
||||||
|
latest version/database/songs/
|
||||||
|
|||||||
151
README.md
151
README.md
@@ -1,7 +1,9 @@
|
|||||||
# Arcaea-server
|
# Arcaea-server
|
||||||
|
|
||||||
一个微型的Arcaea本地服务器 A small local server for Arcaea
|
一个微型的Arcaea本地服务器 A small local server for Arcaea
|
||||||
|
|
||||||
## 简介 Introduction
|
## 简介 Introduction
|
||||||
|
|
||||||
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||||
|
|
||||||
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
||||||
@@ -14,92 +16,110 @@ This procedure is mainly used for study and research, and shall not be used for
|
|||||||
> It looks stupid, but it works!
|
> It looks stupid, but it works!
|
||||||
|
|
||||||
## 特性 Features
|
## 特性 Features
|
||||||
有以下 We have:
|
|
||||||
|
:x: : 不支持 Not supported
|
||||||
|
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
|
||||||
|
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
|
||||||
|
:construction: : 建设中 In construction
|
||||||
|
|
||||||
- 登录、注册 Login and registration
|
- 登录、注册 Login and registration
|
||||||
- 多设备登录 Multi device login
|
- 多设备自动封号 Auto-ban of multiple devices
|
||||||
|
- :warning: 多设备登录 Multi device login
|
||||||
|
- 登录频次限制 Login rate limit
|
||||||
|
- :x: 销号 Destroy account
|
||||||
- 成绩上传 Score upload
|
- 成绩上传 Score upload
|
||||||
- PTT
|
- 成绩校验 Score check
|
||||||
- 世界排名 Global rank
|
- 成绩排名 Score rank
|
||||||
- 排名 Rank
|
- 潜力值机制 Potential
|
||||||
|
- Best 30
|
||||||
|
- :warning: Recent Top 10
|
||||||
|
- :warning: 世界排名 Global rank
|
||||||
- 段位系统 Course system
|
- 段位系统 Course system
|
||||||
- Link Play
|
- :warning: Link Play
|
||||||
- 好友系统 Friends
|
- 好友系统 Friends
|
||||||
- 数据同步 Data synchronization
|
- :x: 好友位提升 Max friend number increase
|
||||||
- 爬梯 Climbing steps
|
- 云端存档 Cloud save
|
||||||
- 自定义世界模式 Customizable World Mode
|
- 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
|
||||||
- 自定义歌曲下载 Customizable songs download
|
- 世界模式 World mode
|
||||||
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
|
- 体力系统 Stamina system
|
||||||
|
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
|
||||||
|
- :warning: 角色技能 Character skills
|
||||||
|
- 歌曲下载 Songs downloading
|
||||||
|
- :x: 加密下载 Encrypted downloading
|
||||||
|
- 下载校验 Download check
|
||||||
|
- 下载频次限制 Download rate limit
|
||||||
|
- 购买系统 Purchase system
|
||||||
|
- 单曲和曲包 Single & Pack
|
||||||
|
- :x: 捆绑包 Bundle
|
||||||
|
- 折扣 Discount
|
||||||
|
- 五周年兑换券 5-th anniversary ticket
|
||||||
|
- :x: Extend 包自动降价 Extend pack automatic price reduction
|
||||||
- 奖励系统 Present system
|
- 奖励系统 Present system
|
||||||
- 兑换码系统 Redeem code system
|
- 兑换码系统 Redeem code system
|
||||||
- 角色系统 Character system
|
- 角色系统 Character system
|
||||||
- 全剧情解锁 Unlock all the stories
|
- 数据记录 Data recording
|
||||||
- 后台查询 Background search
|
- 用户成绩 Users' scores
|
||||||
- 后台自定义信息 Customize some things in the background
|
- 用户每日潜力值 Users' daily potential
|
||||||
- 成绩校验 Score check
|
- :wastebasket: 简单的网页管理后台 Simple web admin backend
|
||||||
- 下载校验 Download check
|
- :construction: API
|
||||||
- 服务器日志 Server log
|
- 服务器日志 Server log
|
||||||
|
|
||||||
没有以下 We don't have:
|
|
||||||
- 服务器安全性保证 Server security assurance
|
|
||||||
|
|
||||||
可能有问题 There may be problems:
|
|
||||||
- Recent 30
|
|
||||||
- 一些歌曲的解锁 Some songs' unlocking
|
|
||||||
- 同设备多共存登录 Multiple app logins on the same device
|
|
||||||
|
|
||||||
## 说明 Statement
|
## 说明 Statement
|
||||||
|
|
||||||
只是很有趣,用处探索中。
|
只是很有趣,用处探索中。
|
||||||
It is just so interesting. What it can do is under exploration.
|
It is just so interesting. What it can do is under exploration.
|
||||||
|
|
||||||
|
|
||||||
## 下载 Download
|
## 下载 Download
|
||||||
|
|
||||||
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
|
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
|
||||||
|
|
||||||
[Arcaea-CN official](https://arcaea.lowiro.com/zh)
|
[Arcaea-CN official](https://arcaea.lowiro.com/zh)
|
||||||
[Arcaea-lowi.ro](https://lowi.ro)
|
[Arcaea-Konmai Academy](https://616.sb)
|
||||||
[Arcaea-RhyDown](https://rhydown.com)
|
|
||||||
|
|
||||||
## 更新日志 Update log
|
## 更新日志 Update log
|
||||||
|
|
||||||
只保留最新版本 Only keep the latest version.
|
只保留最新版本 Only keep the latest version.
|
||||||
|
|
||||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
||||||
>
|
|
||||||
> Tips: When updating, please keep the original database in case of data loss.
|
> Tips: When updating, please keep the original database in case of data loss.
|
||||||
|
>
|
||||||
|
> 其它小改动请参考各个 commit 信息
|
||||||
|
> Please refer to the commit messages for other minor changes.
|
||||||
|
|
||||||
|
### Version 2.11.3
|
||||||
|
|
||||||
### Version 2.10.0
|
> v2.11.2.1 ~ v2.11.2.7 for Arcaea 4.5.0 ~ 5.2.0
|
||||||
- 适用于Arcaea 4.1.0版本 For Arcaea 4.1.0
|
|
||||||
- 新搭档 **咲姬** 已解锁 Unlock the character **Saki**.
|
|
||||||
- 新搭档 **刹那** 已解锁 Unlock the character **Setsuna**.
|
|
||||||
- 完善了日志系统 Improve the log system.
|
|
||||||
- 现在可以利用`songlist`确保`3.aff`以外文件不被下载 Now you can use `songlist` to ensure that files other than `3.aff` should not be downloaded. [#60](https://github.com/Lost-MSth/Arcaea-server/issues/60)
|
|
||||||
- 适配v4.0.0以下版本的客户端云存档 Ensure that the clients under v4.0.0 can upload the cloud save.
|
|
||||||
- 优化数据库索引 Optimize database indices.
|
|
||||||
- 尝试确保LinkPlay服务器的线程安全,现在此功能将作为独立服务端 Try to ensure thread safety in LinkPlay server. Now this function will be served as an independent server.
|
|
||||||
- 对API接口的分数列表添加歌曲名 Add song names for getting the score list in API.
|
|
||||||
- 为下载错误增添HTTP状态码 Add HTTP status code when meeting download error.
|
|
||||||
|
|
||||||
- 修复iOS客户端因世界模式地图数据闪退的问题 Fix a bug when world maps' data don't have some unnecessary parts the client of iOS may break down.
|
|
||||||
- 修复API接口无数据`GET`请求导致报错的问题 Fix a bug that `GET` requests without data will report an error in API. [#50](https://github.com/Lost-MSth/Arcaea-server/issues/50)
|
|
||||||
- 修复`aggregate`请求无法捕获内部错误的问题 Fix a bug that `aggregate` requests will get an error when the inner function raises an error.
|
|
||||||
- 修复因错误设置主键导致课程模式谱面无法相同的问题 Fix a bug that the charts of a course cannot be the same because of the incorrect primary keys.
|
|
||||||
- 修复无谱面数据时世界排名分计算出错的问题 Fix a bug that global ranking scores cannot be calculated if there are no chart in the database. [#61](https://github.com/Lost-MSth/Arcaea-server/issues/61)
|
|
||||||
- 修复条件满足但隐藏段位依然无法解锁的问题 Fix a bug that the hidden courses cannot appear even if their requirements are satisfied.
|
|
||||||
- 修复Finale挑战中某些无法解锁的问题 Fix a bug that something of the finale challenge cannot be unlocked.
|
|
||||||
- 修复用户物品数量无法为0的问题,此问题导致了一些购买操作异常 Fix a bug that the users' items will not be zero, which will disturb some purchase operations.
|
|
||||||
- 修复角色等级能超过最大等级的问题 Fix a bug that the level of the character can exceed the max level.
|
|
||||||
- 修复使用`以太之滴`升级角色时应答不正确的问题 Fix a bug that the response is incorrect when upgrading the characters by `generic core`.
|
|
||||||
- 修复`源韵强化`数值显示不正确的问题 Fix a bug that the `prog boost` shows the incorrect value.
|
|
||||||
- 修复世界模式奖励可能被重复发放的问题 Fix a bug that the rewards can be get repeatedly in World Mode.
|
|
||||||
- 修复世界Boss的第二管血量无法削减的问题 Fix a bug that second tube of blood of the world boss won't change.
|
|
||||||
- 修复我的排名显示不正确的问题 Fix a bug that `my rank` doesn't work correctly.
|
|
||||||
- 修复在歌曲结束后无法及时轮换房主的问题 Fix a bug that the room host will be changed late when finishing a song.
|
|
||||||
|
|
||||||
|
- 适用于 Arcaea 5.2.0 版本
|
||||||
|
For Arcaea 5.2.0
|
||||||
|
- 新搭档 **Ilith & Ivy**、**Hikari & Vanessa**、**摩耶**、**露恩** 已解锁(注意“ 洞烛(至高:第八探索者)”因客户端因素不可选用)
|
||||||
|
Unlock the character **Ilith & Ivy**, **Hikari & Vanessa**, **Maya**, and **Luin**. (Note that "Insight(Ascendant - 8th Seeker)" is not available due to the client.)
|
||||||
|
- 为以上角色的技能提供服务端支持
|
||||||
|
Provide server-side support for the skills of the above characters.
|
||||||
|
- 设置中新增可选选项 `DOWNLOAD_FORBID_WHEN_NO_ITEM` 使得当 `songlist` 文件存在时,没有购买的用户无法下载曲目文件(实验性)
|
||||||
|
An option `DOWNLOAD_FORBID_WHEN_NO_ITEM` has been added to the config file to make that users cannot download the songs' files if they has not bought them when the `songlist` file exists. (Experimental)
|
||||||
|
- 支持文件 `video_720.mp4` & `video_1080.mp4` 的下载
|
||||||
|
Add support for downloading `video_720.mp4` & `video_1080.mp4`.
|
||||||
|
- 在存档全解锁和 `songlist` 解析器中支持更多东西,以适应游戏更新
|
||||||
|
Support more things in full cloud save unlocking and `songlist` parser, to adapt to game updates.
|
||||||
|
- Link Play 拥有更详细的控制台日志了
|
||||||
|
More detailed console log for Link Play.
|
||||||
|
- 修复一些搭档的技能在世界模式进度中显示不正确的问题
|
||||||
|
Fix a bug that some characters' skill cannot display proper values in world mode progression.
|
||||||
|
- 修复技能 "skill_mithra" 导致了 `prog` 值增加而不是世界模式进度增加的问题
|
||||||
|
Fix a bug that "skill_mithra" results in adding `prog` value instead of world mode progress.
|
||||||
|
- 重构 Link Play TCP 数据交换部分,以获得更好的安全性和扩展性
|
||||||
|
Code refactor of Link Play TCP data transmission for better security and scalability.
|
||||||
|
- 新增一个 HTTP API 用来获取 Link Play 中当前的房间和玩家信息
|
||||||
|
Add an HTTP API endpoint for getting the information of current rooms and players in Link Play.
|
||||||
|
|
||||||
## 运行环境与依赖 Running environment and requirements
|
## 运行环境与依赖 Running environment and requirements
|
||||||
- Windows/Linux/Mac OS/Android...
|
|
||||||
- Python 3
|
- Windows / Linux / Mac OS / Android...
|
||||||
- Flask module >= 2.0, Cryptography module >= 35.0.0
|
- Python >= 3.6
|
||||||
|
- Flask >= 2.0
|
||||||
|
- Cryptography >= 3.0.0
|
||||||
|
- limits >= 2.7.0
|
||||||
- Charles, IDA, proxy app... (optional)
|
- Charles, IDA, proxy app... (optional)
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -109,34 +129,41 @@ It is just so interesting. What it can do is under exploration.
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
## 使用说明 Instruction for use
|
## 使用说明 Instruction for use
|
||||||
|
|
||||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
|
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
|
||||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use)
|
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use)
|
||||||
|
|
||||||
## 注意 Attentions
|
## 注意 Attentions
|
||||||
|
|
||||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
|
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
|
||||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
|
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
|
||||||
|
|
||||||
|
|
||||||
## Q&A
|
## Q&A
|
||||||
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
|
|
||||||
|
|
||||||
|
[中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
|
||||||
|
|
||||||
## 鸣谢 Thanks
|
## 鸣谢 Thanks
|
||||||
~~歌曲数据库来自 Using song database from
|
|
||||||
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
歌曲数据库来自 Using song database from
|
||||||
|
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||||
|
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
|
||||||
|
|
||||||
> 从v2.9开始不再提供歌曲数据
|
> 从v2.9开始不再提供歌曲数据
|
||||||
> Since v2.9, song data will not be provided.
|
> Since v2.9, song data will not be provided.
|
||||||
|
|
||||||
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
|
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
|
||||||
|
|
||||||
## 联系方式 Contact
|
## 联系方式 Contact
|
||||||
|
|
||||||
如有必要,可以联系本人 Contact me if necessary
|
如有必要,可以联系本人 Contact me if necessary
|
||||||
邮箱 Email:th84292@foxmail.com
|
邮箱 Email:th84292@foxmail.com
|
||||||
|
|
||||||
## 支持一下 Support me
|
## 支持一下 Support me
|
||||||
|
|
||||||
生活不易。 Life is not easy.
|
生活不易。 Life is not easy.
|
||||||
[支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg)
|
[支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg)
|
||||||
[微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png)
|
[微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png)
|
||||||
|
|
||||||
## 使用许可 Use license
|
## 使用许可 Use license
|
||||||
|
|
||||||
[MIT](LICENSE) © Lost
|
[MIT](LICENSE) © Lost
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
from . import users
|
from . import (users, songs, token, system, items,
|
||||||
from . import songs
|
purchases, presents, redeems, characters, multiplay)
|
||||||
from . import token
|
|
||||||
|
|
||||||
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(i.bp)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
from base64 import b64decode
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from json import loads
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from core.api_user import APIUser
|
|
||||||
from core.error import ArcError, NoAccess, PostError
|
|
||||||
from core.sql import Connect
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from setting import Config
|
|
||||||
|
from core.api_user import APIUser
|
||||||
|
from core.config_manager import Config
|
||||||
|
from core.error import ArcError, InputError, NoAccess, PostError
|
||||||
|
from core.sql import Connect
|
||||||
|
|
||||||
from .api_code import error_return
|
from .api_code import error_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
|
||||||
def role_required(request, powers=[]):
|
def role_required(request, powers=[]):
|
||||||
@@ -25,20 +29,17 @@ def role_required(request, powers=[]):
|
|||||||
return error_return(PostError('No token', api_error_code=-1), 401)
|
return error_return(PostError('No token', api_error_code=-1), 401)
|
||||||
|
|
||||||
user = APIUser()
|
user = APIUser()
|
||||||
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
|
|
||||||
user.user_id = 0
|
|
||||||
elif powers == []:
|
|
||||||
# 无powers则非本地权限(API_TOKEN规定的)无法访问
|
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
|
||||||
else:
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
try:
|
|
||||||
user.c = c
|
user.c = c
|
||||||
|
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
|
||||||
|
user.set_role_system()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
user.select_user_id_from_api_token(
|
user.select_user_id_from_api_token(
|
||||||
request.headers['Token'])
|
request.headers['Token'])
|
||||||
user.select_role_and_powers()
|
user.select_role_and_powers()
|
||||||
|
|
||||||
if not any([y in [x.power_name for x in user.role.powers] for y in powers]):
|
if not any(user.role.has_power(y) for y in powers):
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
return error_return(e, 401)
|
return error_return(e, 401)
|
||||||
@@ -49,13 +50,15 @@ def role_required(request, powers=[]):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def request_json_handle(request, required_keys=[], optional_keys=[]):
|
def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False):
|
||||||
'''
|
'''
|
||||||
提取post参数,返回dict,写成了修饰器\
|
提取post参数,返回dict,写成了修饰器
|
||||||
parameters: \
|
|
||||||
`request`: `Request` - 当前请求\
|
parameters:
|
||||||
`required_keys`: `list` - 必须的参数\
|
`request`: `Request` - 当前请求
|
||||||
|
`required_keys`: `list` - 必须的参数
|
||||||
`optional_keys`: `list` - 可选的参数
|
`optional_keys`: `list` - 可选的参数
|
||||||
|
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def decorator(view):
|
def decorator(view):
|
||||||
@@ -63,17 +66,39 @@ def request_json_handle(request, required_keys=[], optional_keys=[]):
|
|||||||
def wrapped_view(*args, **kwargs):
|
def wrapped_view(*args, **kwargs):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
if not request.data:
|
if request.data:
|
||||||
return view(data, *args, **kwargs)
|
json_data = request.get_json()
|
||||||
|
else:
|
||||||
|
if request.method == 'GET' and 'query' in request.args:
|
||||||
|
# 处理axios没法GET传data的问题
|
||||||
|
try:
|
||||||
|
json_data = loads(
|
||||||
|
b64decode(request.args['query']).decode())
|
||||||
|
except:
|
||||||
|
raise PostError(api_error_code=-105)
|
||||||
|
else:
|
||||||
|
json_data = {}
|
||||||
|
|
||||||
for key in required_keys:
|
for key in required_keys:
|
||||||
if key not in request.json:
|
if key not in json_data:
|
||||||
return error_return(PostError('Missing parameter: ' + key, api_error_code=-100))
|
return error_return(InputError(f'Missing parameter: {key}', api_error_code=-100))
|
||||||
data[key] = request.json[key]
|
data[key] = json_data[key]
|
||||||
|
|
||||||
|
if is_batch:
|
||||||
|
for key in Constant.PATCH_KEYS:
|
||||||
|
if key in json_data:
|
||||||
|
data[key] = json_data[key]
|
||||||
|
if not isinstance(data[key], list):
|
||||||
|
return error_return(InputError(f'Parameter {key} must be a list', api_error_code=-100))
|
||||||
|
if not data:
|
||||||
|
return error_return(InputError('No change', api_error_code=-100))
|
||||||
|
else:
|
||||||
for key in optional_keys:
|
for key in optional_keys:
|
||||||
if key in request.json:
|
if key in json_data:
|
||||||
data[key] = request.json[key]
|
data[key] = json_data[key]
|
||||||
|
|
||||||
|
if must_change and not data:
|
||||||
|
return error_return(InputError('No change', api_error_code=-100))
|
||||||
|
|
||||||
return view(data, *args, **kwargs)
|
return view(data, *args, **kwargs)
|
||||||
|
|
||||||
@@ -89,7 +114,6 @@ def api_try(view):
|
|||||||
data = view(*args, **kwargs)
|
data = view(*args, **kwargs)
|
||||||
if data is None:
|
if data is None:
|
||||||
return error_return()
|
return error_return()
|
||||||
else:
|
|
||||||
return data
|
return data
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
if Config.ALLOW_WARNING_LOG:
|
if Config.ALLOW_WARNING_LOG:
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
from core.error import ArcError
|
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
|
from core.error import ArcError
|
||||||
|
|
||||||
default_error = ArcError('Unknown Error')
|
default_error = ArcError('Unknown Error')
|
||||||
|
|
||||||
|
|
||||||
CODE_MSG = {
|
CODE_MSG = {
|
||||||
0: '',
|
0: '',
|
||||||
-1: 'See status code',
|
-1: 'See status code', # 基础错误
|
||||||
-2: 'No data',
|
-2: 'No data',
|
||||||
-3: 'No data or user',
|
-3: 'No data or user', # 不确定是无数据还是无用户
|
||||||
-4: 'No user_id',
|
-4: 'Data exist',
|
||||||
-100: 'Wrong post data',
|
-100: 'Invalid post data', # 1xx数据错误
|
||||||
-101: 'Wrong data type',
|
-101: 'Invalid data type',
|
||||||
-102: 'Wrong query parameter',
|
-102: 'Invalid query parameter',
|
||||||
-103: 'Wrong sort parameter',
|
-103: 'Invalid sort parameter',
|
||||||
-104: 'Wrong sort order parameter',
|
-104: 'Invalid sort order parameter',
|
||||||
-200: 'No permission',
|
-105: 'Invalid URL parameter',
|
||||||
|
-110: 'Invalid user_id',
|
||||||
|
-120: 'Invalid item type',
|
||||||
|
-121: 'No such item',
|
||||||
|
-122: 'Item already exists',
|
||||||
|
-123: 'The collection already has this item',
|
||||||
|
-124: 'The collection does not have this item',
|
||||||
|
-130: 'No such character',
|
||||||
|
-131: 'Invalid skill ID',
|
||||||
|
-200: 'No permission', # 2xx用户相关错误
|
||||||
-201: 'Wrong username or password',
|
-201: 'Wrong username or password',
|
||||||
-202: 'User is banned',
|
-202: 'User is banned',
|
||||||
-203: 'Username exists',
|
-203: 'Too many login attempts',
|
||||||
-204: 'Email address exists',
|
-210: 'Username exists',
|
||||||
|
-211: 'Email address exists',
|
||||||
|
-212: 'User code exists',
|
||||||
-999: 'Unknown error'
|
-999: 'Unknown error'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
134
latest version/api/characters.py
Normal file
134
latest version/api/characters.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import InputError, NoData
|
||||||
|
from core.item import ItemFactory
|
||||||
|
from core.character import Character
|
||||||
|
from core.sql import Connect, Query, Sql
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('characters', __name__, url_prefix='/characters')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
|
@api_try
|
||||||
|
def characters_get(data, user):
|
||||||
|
'''查询全角色信息'''
|
||||||
|
A = ['character_id', 'name', 'skill_id',
|
||||||
|
'skill_id_uncap', 'char_type', 'is_uncapped']
|
||||||
|
B = ['name', 'skill_id', 'skill_id_uncap']
|
||||||
|
C = ['name', 'frag1', 'prog1', 'overdrive1', 'frag20',
|
||||||
|
'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30']
|
||||||
|
with Connect() as c:
|
||||||
|
query = Query(A, B, C).from_dict(data)
|
||||||
|
x = Sql(c).select('character', query=query)
|
||||||
|
r = [Character().from_list(i) for i in x]
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
|
return success_return([x.to_dict() for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:character_id>', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def characters_character_get(user, character_id: int):
|
||||||
|
# 包含core
|
||||||
|
with Connect() as c:
|
||||||
|
c = Character(c).select(character_id)
|
||||||
|
c.select_character_core()
|
||||||
|
return success_return(c.to_dict(has_cores=True))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:character_id>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['max_level', 'skill_id', 'skill_id_uncap', 'skill_unlock_level', 'skill_requires_uncap', 'char_type', 'is_uncapped', 'frag1', 'prog1', 'overdrive1', 'frag20', 'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def characters_character_put(data, user, character_id: int):
|
||||||
|
'''修改角色信息'''
|
||||||
|
if ('skill_id' in data and data['skill_id'] != '' and data['skill_id'] not in Constant.SKILL_IDS) or ('skill_id_uncap' in data and data['skill_id_uncap'] != '' and data['skill_id_uncap'] not in Constant.SKILL_IDS):
|
||||||
|
raise InputError('Invalid skill_id', api_error_code=-131)
|
||||||
|
with Connect() as c:
|
||||||
|
c = Character(c).select(character_id)
|
||||||
|
try:
|
||||||
|
if 'max_level' in data:
|
||||||
|
c.max_level = int(data['max_level'])
|
||||||
|
if 'skill_id' in data:
|
||||||
|
c.skill_id = data['skill_id']
|
||||||
|
if 'skill_id_uncap' in data:
|
||||||
|
c.skill_id_uncap = data['skill_id_uncap']
|
||||||
|
if 'skill_unlock_level' in data:
|
||||||
|
c.skill_unlock_level = int(data['skill_unlock_level'])
|
||||||
|
if 'skill_requires_uncap' in data:
|
||||||
|
c.skill_requires_uncap = data['skill_requires_uncap'] == 1
|
||||||
|
if 'char_type' in data:
|
||||||
|
c.char_type = int(data['char_type'])
|
||||||
|
if 'is_uncapped' in data:
|
||||||
|
c.is_uncapped = data['is_uncapped'] == 1
|
||||||
|
t = ['frag1', 'prog1', 'overdrive1', 'frag20', 'prog20',
|
||||||
|
'overdrive20', 'frag30', 'prog30', 'overdrive30']
|
||||||
|
for i in t:
|
||||||
|
if i not in data:
|
||||||
|
continue
|
||||||
|
if i.endswith('1'):
|
||||||
|
x = getattr(c, i[:-1])
|
||||||
|
x.start = float(data[i])
|
||||||
|
elif i.endswith('20'):
|
||||||
|
x = getattr(c, i[:-2])
|
||||||
|
x.mid = float(data[i])
|
||||||
|
else:
|
||||||
|
x = getattr(c, i[:-2])
|
||||||
|
x.end = float(data[i])
|
||||||
|
except ValueError as e:
|
||||||
|
raise InputError('Invalid input', api_error_code=-101) from e
|
||||||
|
c.update()
|
||||||
|
return success_return(c.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:character_id>/cores', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def characters_character_cores_get(user, character_id: int):
|
||||||
|
with Connect() as c:
|
||||||
|
c = Character(c)
|
||||||
|
c.character_id = character_id
|
||||||
|
c.select_character_core()
|
||||||
|
return success_return(c.uncap_cores_to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:character_id>/cores', methods=['PATCH'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, is_batch=True)
|
||||||
|
@api_try
|
||||||
|
def characters_character_cores_patch(data, user, character_id: int):
|
||||||
|
'''修改角色觉醒cores'''
|
||||||
|
def force_type_core(x: dict) -> dict:
|
||||||
|
x['item_type'] = 'core'
|
||||||
|
x['type'] = 'core'
|
||||||
|
return x
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
ch = Character(c)
|
||||||
|
ch.character_id = character_id
|
||||||
|
ch.select_character_core()
|
||||||
|
ch.remove_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in map(force_type_core, data.get('remove', []))])
|
||||||
|
ch.add_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in map(force_type_core, data.get('create', []))])
|
||||||
|
updates = list(map(force_type_core, data.get('update', [])))
|
||||||
|
for x in updates:
|
||||||
|
if 'amount' not in x:
|
||||||
|
raise InputError('`amount` is required in `update`')
|
||||||
|
if not isinstance(x['amount'], int) or x['amount'] <= 0:
|
||||||
|
raise InputError(
|
||||||
|
'`amount` must be a positive integer', api_error_code=-101)
|
||||||
|
|
||||||
|
ch.update_items(
|
||||||
|
[ItemFactory.from_dict(x, c=c) for x in updates])
|
||||||
|
return success_return(ch.uncap_cores_to_dict())
|
||||||
@@ -1,2 +1,7 @@
|
|||||||
class Constant:
|
class Constant:
|
||||||
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
|
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
|
||||||
|
|
||||||
|
PATCH_KEYS = ['create', 'update', 'remove']
|
||||||
|
|
||||||
|
SKILL_IDS = ['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']
|
||||||
|
|||||||
115
latest version/api/items.py
Normal file
115
latest version/api/items.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import DataExist, InputError, NoData
|
||||||
|
from core.item import Item, ItemFactory
|
||||||
|
from core.sql import Connect, Query, Sql
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
bp = Blueprint('items', __name__, url_prefix='/items')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
|
@api_try
|
||||||
|
def items_get(data, user):
|
||||||
|
'''查询全物品信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
query = Query(['item_id', 'type'], ['item_id'],
|
||||||
|
['item_id']).from_dict(data)
|
||||||
|
x = Sql(c).select('item', query=query)
|
||||||
|
r: 'list[Item]' = []
|
||||||
|
for i in x:
|
||||||
|
r.append(ItemFactory.from_dict({
|
||||||
|
'item_id': i[0],
|
||||||
|
'type': i[1],
|
||||||
|
'is_available': i[2] == 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
|
return success_return([x.to_dict(has_is_available=True, has_amount=False) for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
ALLOW_ITEM_TYPE = ['pack', 'single', 'world_song', 'character']
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['POST'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, required_keys=['item_id', 'type'], optional_keys=['is_available'])
|
||||||
|
@api_try
|
||||||
|
def items_post(data, user):
|
||||||
|
'''新增物品'''
|
||||||
|
if data['type'] not in ALLOW_ITEM_TYPE:
|
||||||
|
raise InputError(
|
||||||
|
f'Invalid item type: `{data["type"]}`', api_error_code=-120)
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemFactory.from_dict(data, c=c)
|
||||||
|
if item.select_exists():
|
||||||
|
raise DataExist(
|
||||||
|
f'Item `{item.item_type}`: `{item.item_id}` already exists', api_error_code=-122)
|
||||||
|
item.insert()
|
||||||
|
return success_return(item.to_dict(has_is_available=True, has_amount=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:item_type>/<string:item_id>', methods=['DELETE'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@api_try
|
||||||
|
def items_item_delete(user, item_type, item_id):
|
||||||
|
'''删除物品'''
|
||||||
|
if item_type not in ALLOW_ITEM_TYPE:
|
||||||
|
raise InputError(
|
||||||
|
f'Invalid item type: `{item_type}`', api_error_code=-120)
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemFactory.from_dict({
|
||||||
|
'item_id': item_id,
|
||||||
|
'type': item_type
|
||||||
|
}, c=c)
|
||||||
|
if not item.select_exists():
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{item_type}`: `{item_id}`', api_error_code=-121)
|
||||||
|
item.delete()
|
||||||
|
return success_return()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:item_type>/<string:item_id>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['is_available'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def items_item_put(data, user, item_type, item_id):
|
||||||
|
'''修改物品'''
|
||||||
|
if item_type not in ALLOW_ITEM_TYPE:
|
||||||
|
raise InputError(
|
||||||
|
f'Invalid item type: `{item_type}`', api_error_code=-120)
|
||||||
|
if not isinstance(data['is_available'], bool):
|
||||||
|
raise InputError('`is_available` must be a boolean',
|
||||||
|
api_error_code=-101)
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemFactory.from_dict({
|
||||||
|
'item_id': item_id,
|
||||||
|
'type': item_type,
|
||||||
|
'is_available': data['is_available']
|
||||||
|
}, c=c)
|
||||||
|
if not item.select_exists():
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{item_type}`: `{item_id}`', api_error_code=-121)
|
||||||
|
item.update()
|
||||||
|
return success_return(item.to_dict(has_is_available=True, has_amount=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:item_type>/<string:item_id>', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def items_item_get(user, item_type, item_id):
|
||||||
|
'''查询单个物品信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
item = ItemFactory.from_dict({
|
||||||
|
'item_id': item_id,
|
||||||
|
'type': item_type
|
||||||
|
}, c=c)
|
||||||
|
item.select()
|
||||||
|
return success_return(item.to_dict(has_is_available=True, has_amount=False))
|
||||||
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)
|
||||||
122
latest version/api/presents.py
Normal file
122
latest version/api/presents.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import DataExist, InputError, NoData
|
||||||
|
from core.item import ItemFactory
|
||||||
|
from core.present import Present
|
||||||
|
from core.sql import Connect, Query, Sql
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('presents', __name__, url_prefix='/presents')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
|
@api_try
|
||||||
|
def presents_get(data, user):
|
||||||
|
'''查询全present信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
query = Query(['present_id'], ['present_id', 'description'], [
|
||||||
|
'present_id', 'expire_ts']).from_dict(data)
|
||||||
|
x = Sql(c).select('present', query=query)
|
||||||
|
r = [Present().from_list(i) for i in x]
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
|
return success_return([x.to_dict(has_items=False) for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['POST'])
|
||||||
|
@role_required(request, ['insert'])
|
||||||
|
@request_json_handle(request, required_keys=['present_id', 'description', 'expire_ts'], optional_keys=['items'])
|
||||||
|
@api_try
|
||||||
|
def presents_post(data, user):
|
||||||
|
'''添加present,注意可以有items,不存在的item会自动创建'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Present(c).from_dict(data)
|
||||||
|
if p.select_exists():
|
||||||
|
raise DataExist(
|
||||||
|
f'Present `{p.present_id}` already exists')
|
||||||
|
p.insert_all()
|
||||||
|
return success_return(p.to_dict(has_items='items' in data))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:present_id>', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def presents_present_get(user, present_id: str):
|
||||||
|
'''查询单个present信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Present(c).select(present_id)
|
||||||
|
p.select_items()
|
||||||
|
return success_return(p.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:present_id>', methods=['DELETE'])
|
||||||
|
@role_required(request, ['delete'])
|
||||||
|
@api_try
|
||||||
|
def presents_present_delete(user, present_id: str):
|
||||||
|
'''删除present,会连带删除present_item'''
|
||||||
|
with Connect() as c:
|
||||||
|
Present(c).select(present_id).delete_all()
|
||||||
|
return success_return()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:present_id>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['description', 'expire_ts'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def presents_present_put(data, user, present_id: str):
|
||||||
|
'''更新present信息,注意不能有items'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Present(c).select(present_id)
|
||||||
|
if 'description' in data:
|
||||||
|
p.description = str(data['description'])
|
||||||
|
if 'expire_ts' in data:
|
||||||
|
p.expire_ts = int(data['expire_ts'])
|
||||||
|
p.update()
|
||||||
|
return success_return(p.to_dict(has_items=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:present_id>/items', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def presents_present_items_get(user, present_id: str):
|
||||||
|
'''查询present的items'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Present(c)
|
||||||
|
p.present_id = present_id
|
||||||
|
p.select_items()
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in p.items])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:present_id>/items', methods=['PATCH'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, is_batch=True)
|
||||||
|
@api_try
|
||||||
|
def presents_present_items_patch(data, user, present_id: str):
|
||||||
|
'''增删改单个present的items'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Present(c)
|
||||||
|
p.present_id = present_id
|
||||||
|
p.select_items()
|
||||||
|
p.remove_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('remove', [])])
|
||||||
|
p.add_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('create', [])])
|
||||||
|
|
||||||
|
updates = data.get('update', [])
|
||||||
|
for x in updates:
|
||||||
|
if 'amount' not in x:
|
||||||
|
raise InputError('`amount` is required in `update`')
|
||||||
|
if not isinstance(x['amount'], int) or x['amount'] <= 0:
|
||||||
|
raise InputError(
|
||||||
|
'`amount` must be a positive integer', api_error_code=-101)
|
||||||
|
|
||||||
|
p.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in p.items])
|
||||||
163
latest version/api/purchases.py
Normal file
163
latest version/api/purchases.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import DataExist, InputError, NoData
|
||||||
|
from core.item import ItemFactory
|
||||||
|
from core.purchase import Purchase
|
||||||
|
from core.sql import Connect, Query, Sql
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
bp = Blueprint('purchases', __name__, url_prefix='/purchases')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
|
@api_try
|
||||||
|
def purchases_get(data, user):
|
||||||
|
'''查询全购买信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
query = Query(['purchase_name', 'discount_reason'], ['purchase_name'], [
|
||||||
|
'purchase_name', 'price', 'orig_price', 'discount_from', 'discount_to']).from_dict(data)
|
||||||
|
x = Sql(c).select('purchase', query=query)
|
||||||
|
r = [Purchase().from_list(i) for i in x]
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
|
return success_return([x.to_dict(has_items=False, show_real_price=False) for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['POST'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, required_keys=['purchase_name', 'orig_price'], optional_keys=['price', 'discount_from', 'discount_to', 'discount_reason', 'items'])
|
||||||
|
@api_try
|
||||||
|
def purchases_post(data, user):
|
||||||
|
'''新增购买,注意可以有items,不存在的item会自动创建'''
|
||||||
|
with Connect() as c:
|
||||||
|
purchase = Purchase(c).from_dict(data)
|
||||||
|
if purchase.select_exists():
|
||||||
|
raise DataExist(
|
||||||
|
f'Purchase `{purchase.purchase_name}` already exists')
|
||||||
|
purchase.insert_all()
|
||||||
|
return success_return(purchase.to_dict(has_items='items' in data, show_real_price=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:purchase_name>', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def purchases_purchase_get(user, purchase_name: str):
|
||||||
|
'''查询单个购买信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
return success_return(Purchase(c).select(purchase_name).to_dict(show_real_price=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:purchase_name>', methods=['DELETE'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@api_try
|
||||||
|
def purchases_purchase_delete(user, purchase_name: str):
|
||||||
|
'''删除单个购买信息,会连带删除purchase_item'''
|
||||||
|
with Connect() as c:
|
||||||
|
Purchase(c).select(purchase_name).delete_all()
|
||||||
|
return success_return()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:purchase_name>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['price', 'orig_price', 'discount_from', 'discount_to', 'discount_reason'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def purchases_purchase_put(data, user, purchase_name: str):
|
||||||
|
'''修改单个购买信息,注意不能有items'''
|
||||||
|
with Connect() as c:
|
||||||
|
purchase = Purchase(c).select(purchase_name)
|
||||||
|
t = ['price', 'orig_price', 'discount_from', 'discount_to']
|
||||||
|
for i in t:
|
||||||
|
if i in data:
|
||||||
|
setattr(purchase, i, int(data[i]))
|
||||||
|
if 'discount_reason' in data:
|
||||||
|
purchase.discount_reason = str(data['discount_reason'])
|
||||||
|
|
||||||
|
purchase.update()
|
||||||
|
return success_return(purchase.to_dict(has_items=False, show_real_price=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:purchase_name>/items', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def purchases_purchase_items_get(user, purchase_name: str):
|
||||||
|
'''查询单个购买的所有items'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Purchase(c)
|
||||||
|
p.purchase_name = purchase_name
|
||||||
|
p.select_items()
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in p.items])
|
||||||
|
|
||||||
|
|
||||||
|
# @bp.route('/<string:purchase_name>/items', methods=['POST'])
|
||||||
|
# @role_required(request, ['change'])
|
||||||
|
# @request_json_handle(request, required_keys=['item_id', 'type'], optional_keys=['amount'])
|
||||||
|
# @api_try
|
||||||
|
# def purchases_purchase_items_post(data, user, purchase_name: str):
|
||||||
|
# '''新增单个购买的批量items'''
|
||||||
|
# with Connect() as c:
|
||||||
|
# p = Purchase(c)
|
||||||
|
# p.purchase_name = purchase_name
|
||||||
|
# p.select_items()
|
||||||
|
# p.add_items([ItemFactory().from_dict(data)])
|
||||||
|
# return success_return([x.to_dict(has_is_available=True) for x in p.items])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:purchase_name>/items', methods=['PATCH'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, is_batch=True)
|
||||||
|
@api_try
|
||||||
|
def purchases_purchase_items_patch(data, user, purchase_name: str):
|
||||||
|
'''增删改单个购买的批量items'''
|
||||||
|
with Connect() as c:
|
||||||
|
p = Purchase(c)
|
||||||
|
p.purchase_name = purchase_name
|
||||||
|
p.select_items()
|
||||||
|
p.remove_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('remove', [])])
|
||||||
|
p.add_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('create', [])])
|
||||||
|
|
||||||
|
updates = data.get('update', [])
|
||||||
|
for x in updates:
|
||||||
|
if 'amount' not in x:
|
||||||
|
raise InputError('`amount` is required in `update`')
|
||||||
|
if not isinstance(x['amount'], int) or x['amount'] <= 0:
|
||||||
|
raise InputError(
|
||||||
|
'`amount` must be a positive integer', api_error_code=-101)
|
||||||
|
|
||||||
|
p.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in p.items])
|
||||||
|
|
||||||
|
|
||||||
|
# @bp.route('/<string:purchase_name>/items/<string:item_type>/<string:item_id>', methods=['DELETE'])
|
||||||
|
# @role_required(request, ['change'])
|
||||||
|
# @api_try
|
||||||
|
# def purchases_purchase_items_item_delete(user, purchase_name: str, item_type: str, item_id: str):
|
||||||
|
# '''删除单个购买的单个item'''
|
||||||
|
# with Connect() as c:
|
||||||
|
# p = Purchase(c)
|
||||||
|
# p.purchase_name = purchase_name
|
||||||
|
# p.select_items()
|
||||||
|
# p.delete_items([ItemFactory().from_dict(
|
||||||
|
# {'item_type': item_type, 'item_id': item_id})])
|
||||||
|
# return success_return()
|
||||||
|
|
||||||
|
|
||||||
|
# @bp.route('/<string:purchase_name>/items/<string:item_type>/<string:item_id>', methods=['PUT'])
|
||||||
|
# @role_required(request, ['change'])
|
||||||
|
# @request_json_handle(request, optional_keys=['amount', 'is_available'], must_change=True)
|
||||||
|
# @api_try
|
||||||
|
# def purchases_purchase_items_item_put(data, user, purchase_name: str, item_type: str, item_id: str):
|
||||||
|
# '''修改单个购买的单个item'''
|
||||||
|
# with Connect() as c:
|
||||||
|
# p = Purchase(c)
|
||||||
|
# p.purchase_name = purchase_name
|
||||||
|
# pass
|
||||||
|
# return success_return()
|
||||||
119
latest version/api/redeems.py
Normal file
119
latest version/api/redeems.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import DataExist, InputError, NoData
|
||||||
|
from core.item import ItemFactory
|
||||||
|
from core.redeem import Redeem
|
||||||
|
from core.sql import Connect, Query, Sql
|
||||||
|
|
||||||
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
from .constant import Constant
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('redeems', __name__, url_prefix='/redeems')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
|
@api_try
|
||||||
|
def redeems_get(data, user):
|
||||||
|
'''查询全redeem信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
query = Query(['code', 'type'], ['code'], ['code']).from_dict(data)
|
||||||
|
x = Sql(c).select('redeem', query=query)
|
||||||
|
r = [Redeem().from_list(i) for i in x]
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
|
return success_return([x.to_dict(has_items=False) for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['POST'])
|
||||||
|
@role_required(request, ['insert'])
|
||||||
|
@request_json_handle(request, required_keys=['code', 'type'], optional_keys=['items'])
|
||||||
|
@api_try
|
||||||
|
def redeems_post(data, user):
|
||||||
|
'''添加redeem,注意可以有items,不存在的item会自动创建'''
|
||||||
|
with Connect() as c:
|
||||||
|
r = Redeem(c).from_dict(data)
|
||||||
|
if r.select_exists():
|
||||||
|
raise DataExist(
|
||||||
|
f'redeem `{r.code}` already exists')
|
||||||
|
r.insert_all()
|
||||||
|
return success_return(r.to_dict(has_items='items' in data))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:code>', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def redeems_redeem_get(user, code: str):
|
||||||
|
'''查询单个redeem信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
r = Redeem(c).select(code)
|
||||||
|
r.select_items()
|
||||||
|
return success_return(r.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:code>', methods=['DELETE'])
|
||||||
|
@role_required(request, ['delete'])
|
||||||
|
@api_try
|
||||||
|
def redeems_redeem_delete(user, code: str):
|
||||||
|
'''删除redeem,会连带删除redeem_item'''
|
||||||
|
with Connect() as c:
|
||||||
|
Redeem(c).select(code).delete_all()
|
||||||
|
return success_return()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:code>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['type'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def redeems_redeem_put(data, user, code: str):
|
||||||
|
'''更新redeem信息,注意不能有items'''
|
||||||
|
with Connect() as c:
|
||||||
|
r = Redeem(c).select(code)
|
||||||
|
if 'type' in data:
|
||||||
|
r.redeem_type = int(data['type'])
|
||||||
|
r.update()
|
||||||
|
return success_return(r.to_dict(has_items=False))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:code>/items', methods=['GET'])
|
||||||
|
@role_required(request, ['select'])
|
||||||
|
@api_try
|
||||||
|
def redeems_redeem_items_get(user, code: str):
|
||||||
|
'''查询redeem的items'''
|
||||||
|
with Connect() as c:
|
||||||
|
r = Redeem(c)
|
||||||
|
r.code = code
|
||||||
|
r.select_items()
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in r.items])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:code>/items', methods=['PATCH'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, is_batch=True)
|
||||||
|
@api_try
|
||||||
|
def redeems_redeem_items_patch(data, user, code: str):
|
||||||
|
'''增删改单个redeem的items'''
|
||||||
|
with Connect() as c:
|
||||||
|
r = Redeem(c)
|
||||||
|
r.code = code
|
||||||
|
r.select_items()
|
||||||
|
r.remove_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('remove', [])])
|
||||||
|
r.add_items([ItemFactory.from_dict(x, c=c)
|
||||||
|
for x in data.get('create', [])])
|
||||||
|
|
||||||
|
updates = data.get('update', [])
|
||||||
|
for x in updates:
|
||||||
|
if 'amount' not in x:
|
||||||
|
raise InputError('`amount` is required in `update`')
|
||||||
|
if not isinstance(x['amount'], int) or x['amount'] <= 0:
|
||||||
|
raise InputError(
|
||||||
|
'`amount` must be a positive integer', api_error_code=-101)
|
||||||
|
|
||||||
|
r.update_items([ItemFactory.from_dict(x, c=c) for x in updates])
|
||||||
|
return success_return([x.to_dict(has_is_available=True) for x in r.items])
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from core.error import NoData
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import DataExist, InputError, NoData
|
||||||
|
from core.rank import RankList
|
||||||
from core.song import Song
|
from core.song import Song
|
||||||
from core.sql import Connect, Query, Sql
|
from core.sql import Connect, Query, Sql
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .api_auth import api_try, request_json_handle, role_required
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
from .api_code import success_return
|
from .api_code import success_return
|
||||||
@@ -20,6 +22,39 @@ def songs_song_get(user, song_id):
|
|||||||
return success_return(s.to_dict())
|
return success_return(s.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:song_id>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['name', 'charts'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def songs_song_put(data, user, song_id):
|
||||||
|
'''修改歌曲信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
s = Song(c, song_id).select()
|
||||||
|
if 'name' in data:
|
||||||
|
s.name = str(data['name'])
|
||||||
|
if 'charts' in data:
|
||||||
|
for i in data['charts']:
|
||||||
|
if 'difficulty' in i and 'chart_const' in i:
|
||||||
|
s.charts[i['difficulty']].defnum = round(
|
||||||
|
i['chart_const'] * 10)
|
||||||
|
|
||||||
|
s.update()
|
||||||
|
return success_return(s.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:song_id>', methods=['DELETE'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@api_try
|
||||||
|
def songs_song_delete(user, song_id):
|
||||||
|
'''删除歌曲信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
s = Song(c, song_id)
|
||||||
|
if not s.select_exists():
|
||||||
|
raise NoData(f'No such song: `{song_id}`')
|
||||||
|
s.delete()
|
||||||
|
return success_return()
|
||||||
|
|
||||||
|
|
||||||
@bp.route('', methods=['GET'])
|
@bp.route('', methods=['GET'])
|
||||||
@role_required(request, ['select', 'select_song_info'])
|
@role_required(request, ['select', 'select_song_info'])
|
||||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||||
@@ -30,7 +65,7 @@ def songs_get(data, user):
|
|||||||
B = ['song_id', 'name', 'rating_pst',
|
B = ['song_id', 'name', 'rating_pst',
|
||||||
'rating_prs', 'rating_ftr', 'rating_byn']
|
'rating_prs', 'rating_ftr', 'rating_byn']
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
query = Query(A, A, B).from_data(data)
|
query = Query(A, A, B).from_dict(data)
|
||||||
x = Sql(c).select('chart', query=query)
|
x = Sql(c).select('chart', query=query)
|
||||||
r = []
|
r = []
|
||||||
for i in x:
|
for i in x:
|
||||||
@@ -40,3 +75,41 @@ def songs_get(data, user):
|
|||||||
raise NoData(api_error_code=-2)
|
raise NoData(api_error_code=-2)
|
||||||
|
|
||||||
return success_return([x.to_dict() for x in r])
|
return success_return([x.to_dict() for x in r])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['POST'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, ['song_id', 'charts'], ['name'])
|
||||||
|
@api_try
|
||||||
|
def songs_post(data, user):
|
||||||
|
'''添加歌曲信息'''
|
||||||
|
with Connect() as c:
|
||||||
|
s = Song(c).from_dict(data)
|
||||||
|
if s.select_exists():
|
||||||
|
raise DataExist(f'Song `{s.song_id}` already exists')
|
||||||
|
s.insert()
|
||||||
|
return success_return(s.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<string:song_id>/<int:difficulty>/rank', methods=['GET'])
|
||||||
|
@role_required(request, ['select', 'select_song_rank', 'select_song_rank_top'])
|
||||||
|
@request_json_handle(request, optional_keys=['limit'])
|
||||||
|
@api_try
|
||||||
|
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
|
||||||
|
'''查询歌曲某个难度的成绩排行榜,和游戏内接口相似,只允许limit'''
|
||||||
|
if difficulty not in [0, 1, 2, 3]:
|
||||||
|
raise InputError('Difficulty must be 0, 1, 2 or 3')
|
||||||
|
limit = data.get('limit', 20)
|
||||||
|
if not isinstance(limit, int):
|
||||||
|
raise InputError('Limit must be int')
|
||||||
|
if user.role.only_has_powers(['select_song_rank_top'], ['select', 'select_song_rank']):
|
||||||
|
# 限制低权限只能查询前20名
|
||||||
|
if limit > 20 or limit < 0:
|
||||||
|
limit = 20
|
||||||
|
with Connect() as c:
|
||||||
|
rank_list = RankList(c)
|
||||||
|
rank_list.song.set_chart(song_id, difficulty)
|
||||||
|
rank_list.limit = limit
|
||||||
|
# 不检查歌曲是否存在,不存在返回的是空列表
|
||||||
|
rank_list.select_top()
|
||||||
|
return success_return(rank_list.to_dict_list())
|
||||||
|
|||||||
32
latest version/api/system.py
Normal file
32
latest version/api/system.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.error import ArcError
|
||||||
|
from core.operation import BaseOperation
|
||||||
|
|
||||||
|
from .api_auth import api_try, role_required
|
||||||
|
from .api_code import success_return
|
||||||
|
|
||||||
|
bp = Blueprint('system', __name__, url_prefix='/system')
|
||||||
|
|
||||||
|
|
||||||
|
operation_dict = {i._name: i for i in BaseOperation.__subclasses__()}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/operations', methods=['GET'])
|
||||||
|
@role_required(request, ['system'])
|
||||||
|
@api_try
|
||||||
|
def operations_get(user):
|
||||||
|
return success_return(list(operation_dict.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/operations/<string:operation_name>', methods=['POST'])
|
||||||
|
@role_required(request, ['system'])
|
||||||
|
@api_try
|
||||||
|
def operations_operation_post(user, operation_name: str):
|
||||||
|
if operation_name not in operation_dict:
|
||||||
|
raise ArcError(
|
||||||
|
f'No such operation: `{operation_name}`', api_error_code=-1, status=404)
|
||||||
|
x = operation_dict[operation_name]()
|
||||||
|
x.set_params(**request.get_json())
|
||||||
|
x.run()
|
||||||
|
return success_return()
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.api_user import APIUser
|
from core.api_user import APIUser
|
||||||
from core.error import PostError
|
from core.error import PostError
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .api_auth import api_try, request_json_handle, role_required
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
from .api_code import success_return
|
from .api_code import success_return
|
||||||
@@ -16,21 +17,23 @@ bp = Blueprint('token', __name__, url_prefix='/token')
|
|||||||
@api_try
|
@api_try
|
||||||
def token_post(data):
|
def token_post(data):
|
||||||
'''
|
'''
|
||||||
登录,获取token\
|
登录,获取token
|
||||||
|
|
||||||
{'auth': base64('<user_id>:<password>')}
|
{'auth': base64('<user_id>:<password>')}
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
auth_decode = bytes.decode(b64decode(data['auth']))
|
auth_decode = bytes.decode(b64decode(data['auth']))
|
||||||
except:
|
except Exception as e:
|
||||||
raise PostError(api_error_code=-100)
|
raise PostError(api_error_code=-100) from e
|
||||||
if not ':' in auth_decode:
|
if ':' not in auth_decode:
|
||||||
raise PostError(api_error_code=-100)
|
raise PostError(api_error_code=-100)
|
||||||
name, password = auth_decode.split(':', 1)
|
name, password = auth_decode.split(':', 1)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
user = APIUser(c)
|
user = APIUser(c)
|
||||||
user.login(name, password, request.remote_addr)
|
user.login(name, password, request.remote_addr)
|
||||||
return success_return({'token': user.token, 'user_id': user.user_id})
|
current_app.logger.info(f'API user `{user.user_id}` log in')
|
||||||
|
return success_return({'token': user.api_token, 'user_id': user.user_id})
|
||||||
|
|
||||||
|
|
||||||
@bp.route('', methods=['GET'])
|
@bp.route('', methods=['GET'])
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.api_user import APIUser
|
||||||
|
from core.config_manager import Config
|
||||||
from core.error import InputError, NoAccess, NoData
|
from core.error import InputError, NoAccess, NoData
|
||||||
from core.score import Potential, UserScoreList
|
from core.score import Potential, UserScoreList
|
||||||
from core.sql import Connect, Query, Sql
|
from core.sql import Connect, Query, Sql
|
||||||
from core.user import UserInfo, UserRegister
|
from core.user import UserChanger, UserInfo, UserRegister
|
||||||
from flask import Blueprint, request
|
from core.util import get_today_timestamp
|
||||||
|
|
||||||
from .api_auth import api_try, request_json_handle, role_required
|
from .api_auth import api_try, request_json_handle, role_required
|
||||||
from .api_code import error_return, success_return
|
from .api_code import error_return, success_return
|
||||||
@@ -15,7 +19,7 @@ bp = Blueprint('users', __name__, url_prefix='/users')
|
|||||||
@role_required(request, ['change'])
|
@role_required(request, ['change'])
|
||||||
@request_json_handle(request, ['name', 'password', 'email'])
|
@request_json_handle(request, ['name', 'password', 'email'])
|
||||||
@api_try
|
@api_try
|
||||||
def users_post(data, _):
|
def users_post(data, user):
|
||||||
'''注册一个用户'''
|
'''注册一个用户'''
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
new_user = UserRegister(c)
|
new_user = UserRegister(c)
|
||||||
@@ -36,7 +40,7 @@ def users_get(data, user):
|
|||||||
B = ['user_id', 'name', 'user_code', 'join_date',
|
B = ['user_id', 'name', 'user_code', 'join_date',
|
||||||
'rating_ptt', 'time_played', 'ticket', 'world_rank_score']
|
'rating_ptt', 'time_played', 'ticket', 'world_rank_score']
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
query = Query(A, A, B).from_data(data)
|
query = Query(A, A, B).from_dict(data)
|
||||||
x = Sql(c).select('user', query=query)
|
x = Sql(c).select('user', query=query)
|
||||||
r = []
|
r = []
|
||||||
for i in x:
|
for i in x:
|
||||||
@@ -50,7 +54,7 @@ def users_get(data, user):
|
|||||||
'name': x.name,
|
'name': x.name,
|
||||||
'join_date': x.join_date,
|
'join_date': x.join_date,
|
||||||
'user_code': x.user_code,
|
'user_code': x.user_code,
|
||||||
'rating_ptt': x.rating_ptt/100,
|
'rating_ptt': x.rating_ptt,
|
||||||
'character_id': x.character.character_id,
|
'character_id': x.character.character_id,
|
||||||
'is_char_uncapped': x.character.is_uncapped,
|
'is_char_uncapped': x.character.is_uncapped,
|
||||||
'is_char_uncapped_override': x.character.is_uncapped_override,
|
'is_char_uncapped_override': x.character.is_uncapped_override,
|
||||||
@@ -65,9 +69,9 @@ def users_get(data, user):
|
|||||||
def users_user_get(user, user_id):
|
def users_user_get(user, user_id):
|
||||||
'''查询用户信息'''
|
'''查询用户信息'''
|
||||||
if user_id <= 0:
|
if user_id <= 0:
|
||||||
return error_return(InputError(api_error_code=-4))
|
return error_return(InputError(api_error_code=-110))
|
||||||
# 查别人需要select权限
|
# 查别人需要select权限
|
||||||
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'):
|
if user_id != user.user_id and not user.role.has_power('select'):
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
@@ -75,24 +79,62 @@ def users_user_get(user, user_id):
|
|||||||
return success_return(u.to_dict())
|
return success_return(u.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>', methods=['PUT'])
|
||||||
|
@role_required(request, ['change'])
|
||||||
|
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'], must_change=True)
|
||||||
|
@api_try
|
||||||
|
def users_user_put(data, user, user_id):
|
||||||
|
'''修改一个用户'''
|
||||||
|
with Connect() as c:
|
||||||
|
u = UserChanger(c, user_id)
|
||||||
|
r = {}
|
||||||
|
r['user_id'] = user_id
|
||||||
|
if 'name' in data:
|
||||||
|
u.set_name(data['name'])
|
||||||
|
r['name'] = u.name
|
||||||
|
if 'password' in data:
|
||||||
|
if data['password'] == '':
|
||||||
|
u.password = ''
|
||||||
|
r['password'] = ''
|
||||||
|
else:
|
||||||
|
u.set_password(data['password'])
|
||||||
|
r['password'] = u.hash_pwd
|
||||||
|
if 'email' in data:
|
||||||
|
u.set_email(data['email'])
|
||||||
|
r['email'] = u.email
|
||||||
|
if 'user_code' in data:
|
||||||
|
u.set_user_code(data['user_code'])
|
||||||
|
r['user_code'] = u.user_code
|
||||||
|
if 'ticket' in data:
|
||||||
|
if not isinstance(data['ticket'], int):
|
||||||
|
raise InputError('Ticket must be int')
|
||||||
|
u.ticket = data['ticket']
|
||||||
|
r['ticket'] = u.ticket
|
||||||
|
u.update_columns(d=r)
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<int:user_id>/b30', methods=['GET'])
|
@bp.route('/<int:user_id>/b30', methods=['GET'])
|
||||||
@role_required(request, ['select', 'select_me'])
|
@role_required(request, ['select', 'select_me'])
|
||||||
@api_try
|
@api_try
|
||||||
def users_user_b30_get(user, user_id):
|
def users_user_b30_get(user, user_id):
|
||||||
'''查询用户b30'''
|
'''查询用户b30'''
|
||||||
if user_id <= 0:
|
if user_id <= 0:
|
||||||
return error_return(InputError(api_error_code=-4))
|
return error_return(InputError(api_error_code=-110))
|
||||||
# 查别人需要select权限
|
# 查别人需要select权限
|
||||||
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'):
|
if user_id != user.user_id and not user.role.has_power('select'):
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
x = UserScoreList(c, UserInfo(c, user_id))
|
x = UserScoreList(c, UserInfo(c, user_id))
|
||||||
x.query.limit = 30
|
x.query.limit = 30
|
||||||
x.select_from_user()
|
x.select_from_user()
|
||||||
|
if not x.scores:
|
||||||
|
raise NoData(
|
||||||
|
f'No best30 data of user `{user_id}`', api_error_code=-3)
|
||||||
x.select_song_name()
|
x.select_song_name()
|
||||||
r = x.to_dict_list()
|
r = x.to_dict_list()
|
||||||
rating_sum = sum([i.rating for i in x.scores])
|
rating_sum = sum(i.rating for i in x.scores)
|
||||||
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
|
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
|
||||||
|
|
||||||
|
|
||||||
@@ -103,15 +145,18 @@ def users_user_b30_get(user, user_id):
|
|||||||
def users_user_best_get(data, user, user_id):
|
def users_user_best_get(data, user, user_id):
|
||||||
'''查询用户所有best成绩'''
|
'''查询用户所有best成绩'''
|
||||||
if user_id <= 0:
|
if user_id <= 0:
|
||||||
return error_return(InputError(api_error_code=-4))
|
return error_return(InputError(api_error_code=-110))
|
||||||
# 查别人需要select权限
|
# 查别人需要select权限
|
||||||
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'):
|
if user_id != user.user_id and not user.role.has_power('select'):
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
x = UserScoreList(c, UserInfo(c, user_id))
|
x = UserScoreList(c, UserInfo(c, user_id))
|
||||||
x.query.from_data(data)
|
x.query.from_dict(data)
|
||||||
x.select_from_user()
|
x.select_from_user()
|
||||||
|
if not x.scores:
|
||||||
|
raise NoData(
|
||||||
|
f'No best score data of user `{user_id}`', api_error_code=-3)
|
||||||
r = x.to_dict_list()
|
r = x.to_dict_list()
|
||||||
return success_return({'user_id': user_id, 'data': r})
|
return success_return({'user_id': user_id, 'data': r})
|
||||||
|
|
||||||
@@ -123,11 +168,61 @@ def users_user_r30_get(user, user_id):
|
|||||||
'''查询用户r30'''
|
'''查询用户r30'''
|
||||||
|
|
||||||
if user_id <= 0:
|
if user_id <= 0:
|
||||||
return error_return(InputError(api_error_code=-4))
|
return error_return(InputError(api_error_code=-110))
|
||||||
# 查别人需要select权限
|
# 查别人需要select权限
|
||||||
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'):
|
if user_id != user.user_id and not user.role.has_power('select'):
|
||||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
p = Potential(c, UserInfo(c, user_id))
|
p = Potential(c, UserInfo(c, user_id))
|
||||||
return success_return({'user_id': user_id, 'r10_ptt': p.recent_10 / 10, 'data': p.recent_30_to_dict_list()})
|
return success_return({'user_id': user_id, 'r10_ptt': p.recent_10 / 10, 'data': p.recent_30_to_dict_list()})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>/role', methods=['GET'])
|
||||||
|
@role_required(request, ['select', 'select_me'])
|
||||||
|
@api_try
|
||||||
|
def users_user_role_get(user, user_id):
|
||||||
|
'''查询用户role和powers'''
|
||||||
|
|
||||||
|
if user_id <= 0:
|
||||||
|
return error_return(InputError(api_error_code=-110))
|
||||||
|
|
||||||
|
if user_id == user.user_id:
|
||||||
|
return success_return({'user_id': user.user_id, 'role': user.role.role_id, 'powers': [i.power_id for i in user.role.powers]})
|
||||||
|
# 查别人需要select权限
|
||||||
|
if not user.role.has_power('select'):
|
||||||
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
x = APIUser(c, user_id)
|
||||||
|
x.select_role_and_powers()
|
||||||
|
return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>/rating', methods=['GET'])
|
||||||
|
@role_required(request, ['select', 'select_me'])
|
||||||
|
@request_json_handle(request, optional_keys=['start_timestamp', 'end_timestamp', 'duration'])
|
||||||
|
@api_try
|
||||||
|
def users_user_rating_get(data, user, user_id):
|
||||||
|
'''查询用户历史rating,`duration`是相对于今天的天数'''
|
||||||
|
# 查别人需要select权限
|
||||||
|
if user_id != user.user_id and not user.role.has_power('select'):
|
||||||
|
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||||
|
|
||||||
|
start_timestamp = data.get('start_timestamp', None)
|
||||||
|
end_timestamp = data.get('end_timestamp', None)
|
||||||
|
duration = data.get('duration', None)
|
||||||
|
sql = '''select time, rating_ptt from user_rating where user_id = ?'''
|
||||||
|
sql_data = [user_id]
|
||||||
|
if start_timestamp is not None and end_timestamp is not None:
|
||||||
|
sql += ''' and time between ? and ?'''
|
||||||
|
sql_data += [start_timestamp, end_timestamp]
|
||||||
|
elif duration is not None:
|
||||||
|
sql += ''' and time between ? and ?'''
|
||||||
|
t = get_today_timestamp()
|
||||||
|
sql_data += [t - duration * 24 * 3600, t]
|
||||||
|
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.execute(sql, sql_data)
|
||||||
|
r = c.fetchall()
|
||||||
|
return success_return({'user_id': user_id, 'data': [{'time': i[0], 'rating_ptt': i[1]} for i in r]})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Config():
|
class Config():
|
||||||
'''
|
'''
|
||||||
This is the setting file. You can change some parameters here.
|
This is the example setting file.
|
||||||
|
The user's setting file's name is `config.py`.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@@ -9,7 +10,7 @@ class Config():
|
|||||||
Host and port of your server
|
Host and port of your server
|
||||||
'''
|
'''
|
||||||
HOST = '0.0.0.0'
|
HOST = '0.0.0.0'
|
||||||
PORT = '80'
|
PORT = 80
|
||||||
'''
|
'''
|
||||||
--------------------
|
--------------------
|
||||||
'''
|
'''
|
||||||
@@ -2,57 +2,62 @@ from hashlib import sha256
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .error import NoAccess, NoData, UserBan
|
from .config_manager import Config
|
||||||
|
from .error import NoAccess, NoData, RateLimit, UserBan
|
||||||
|
from .limiter import ArcLimiter
|
||||||
from .user import UserOnline
|
from .user import UserOnline
|
||||||
|
|
||||||
|
|
||||||
class Power:
|
class Power:
|
||||||
def __init__(self, c=None):
|
def __init__(self, c=None):
|
||||||
self.c = c
|
self.c = c
|
||||||
self.power_id: int = None
|
self.power_id: str = None
|
||||||
self.power_name: str = None
|
|
||||||
self.caption: str = None
|
self.caption: str = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d: dict, c=None) -> 'Power':
|
def from_dict(cls, d: dict, c=None) -> 'Power':
|
||||||
p = cls(c)
|
p = cls(c)
|
||||||
p.power_id = d['power_id']
|
p.power_id = d['power_id']
|
||||||
p.power_name = d['power_name']
|
|
||||||
p.caption = d['caption']
|
p.caption = d['caption']
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def select_from_name(self, power_name: str) -> 'Power':
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Role:
|
class Role:
|
||||||
def __init__(self, c=None):
|
def __init__(self, c=None):
|
||||||
self.c = c
|
self.c = c
|
||||||
self.role_id: int = None
|
self.role_id: str = None
|
||||||
self.role_name: str = None
|
|
||||||
self.caption: str = None
|
self.caption: str = None
|
||||||
|
|
||||||
self.powers: list = None
|
self.powers: list = None
|
||||||
|
|
||||||
def has_power(self, power_name: str) -> bool:
|
def has_power(self, power_id: str) -> bool:
|
||||||
'''判断role是否有power'''
|
'''判断role是否有power'''
|
||||||
|
return any(power_id == i.power_id for i in self.powers)
|
||||||
|
|
||||||
|
def only_has_powers(self, power_ids: list, anti_power_ids: list = None) -> bool:
|
||||||
|
'''判断role是否全有power_ids里的power,且没有anti_power_ids里的任何一个power'''
|
||||||
|
flags = [False] * len(power_ids)
|
||||||
|
if anti_power_ids is None:
|
||||||
|
anti_power_ids = []
|
||||||
for i in self.powers:
|
for i in self.powers:
|
||||||
if i.power_name == power_name:
|
if i.power_id in anti_power_ids:
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
for j, k in enumerate(power_ids):
|
||||||
|
if i.power_id == k:
|
||||||
|
flags[j] = True
|
||||||
|
return all(flags)
|
||||||
|
|
||||||
def select_from_id(self, role_id: int = None) -> 'Role':
|
def select_from_id(self, role_id: int = None) -> 'Role':
|
||||||
'''用role_id查询role'''
|
'''用role_id查询role'''
|
||||||
if role_id is not None:
|
if role_id is not None:
|
||||||
self.role_id = role_id
|
self.role_id = role_id
|
||||||
self.c.execute('''select role_name, caption from role where role_id = :a''',
|
self.c.execute('''select caption from role where role_id = :a''',
|
||||||
{'a': self.role_id})
|
{'a': self.role_id})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The role `%s` does not exist.' %
|
raise NoData(
|
||||||
self.role_id, api_error_code=-200)
|
f'The role `{self.role_id}` does not exist.', api_error_code=-200)
|
||||||
self.role_name = x[0]
|
self.caption = x[0]
|
||||||
self.caption = x[1]
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def select_powers(self) -> None:
|
def select_powers(self) -> None:
|
||||||
@@ -63,10 +68,12 @@ class Role:
|
|||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
for i in x:
|
for i in x:
|
||||||
self.powers.append(Power.from_dict(
|
self.powers.append(Power.from_dict(
|
||||||
{'power_id': i[0], 'power_name': i[1], 'caption': i[2]}, self.c))
|
{'power_id': i[0], 'caption': i[1]}, self.c))
|
||||||
|
|
||||||
|
|
||||||
class APIUser(UserOnline):
|
class APIUser(UserOnline):
|
||||||
|
limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login')
|
||||||
|
|
||||||
def __init__(self, c=None, user_id=None) -> None:
|
def __init__(self, c=None, user_id=None) -> None:
|
||||||
super().__init__(c, user_id)
|
super().__init__(c, user_id)
|
||||||
self.api_token: str = None
|
self.api_token: str = None
|
||||||
@@ -74,6 +81,13 @@ class APIUser(UserOnline):
|
|||||||
|
|
||||||
self.ip: str = None
|
self.ip: str = None
|
||||||
|
|
||||||
|
def set_role_system(self) -> None:
|
||||||
|
'''设置为最高权限用户,API接口'''
|
||||||
|
self.user_id = 0
|
||||||
|
self.role = Role(self.c)
|
||||||
|
self.role.role_id = 'system'
|
||||||
|
self.role.select_powers()
|
||||||
|
|
||||||
def select_role(self) -> None:
|
def select_role(self) -> None:
|
||||||
'''查询user的role'''
|
'''查询user的role'''
|
||||||
self.c.execute('''select role_id from user_role where user_id = :a''',
|
self.c.execute('''select role_id from user_role where user_id = :a''',
|
||||||
@@ -82,10 +96,9 @@ class APIUser(UserOnline):
|
|||||||
self.role = Role(self.c)
|
self.role = Role(self.c)
|
||||||
if x is None:
|
if x is None:
|
||||||
# 默认role为user
|
# 默认role为user
|
||||||
self.role.role_id = 1
|
self.role.role_id = 'user'
|
||||||
else:
|
else:
|
||||||
self.role.role_id = int(x[0])
|
self.role.role_id = x[0]
|
||||||
self.role.select_from_id()
|
|
||||||
|
|
||||||
def select_role_and_powers(self) -> None:
|
def select_role_and_powers(self) -> None:
|
||||||
'''查询user的role,以及role的powers'''
|
'''查询user的role,以及role的powers'''
|
||||||
@@ -113,22 +126,27 @@ class APIUser(UserOnline):
|
|||||||
self.password = password
|
self.password = password
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
if not self.limiter.hit(name):
|
||||||
|
raise RateLimit(
|
||||||
|
f'Too many login attempts of username {name}', api_error_code=-205)
|
||||||
|
|
||||||
self.c.execute('''select user_id, password from user where name = :a''', {
|
self.c.execute('''select user_id, password from user where name = :a''', {
|
||||||
'a': self.name})
|
'a': self.name})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The user `%s` does not exist.' %
|
raise NoData(
|
||||||
self.name, api_error_code=-201)
|
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
|
||||||
if x[1] == '':
|
if x[1] == '':
|
||||||
raise UserBan('The user `%s` is banned.' % self.name)
|
raise UserBan(f'The user `{x[0]}` is banned.')
|
||||||
if self.hash_pwd != x[1]:
|
if self.hash_pwd != x[1]:
|
||||||
raise NoAccess('The password is incorrect.', api_error_code=-201)
|
raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
|
||||||
|
api_error_code=-201, status=401)
|
||||||
|
|
||||||
self.user_id = x[0]
|
self.user_id = x[0]
|
||||||
now = int(time() * 1000)
|
now = int(time() * 1000)
|
||||||
self.token = sha256(
|
self.api_token = sha256(
|
||||||
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
|
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
self.c.execute('''insert into api_login values(?,?,?,?)''',
|
self.c.execute('''insert into api_login values(?,?,?,?)''',
|
||||||
(self.user_id, self.token, now, self.ip))
|
(self.user_id, self.api_token, now, self.ip))
|
||||||
|
|||||||
50
latest version/core/bgtask.py
Normal file
50
latest version/core/bgtask.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from atexit import register
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from .constant import Constant
|
||||||
|
from .sql import Connect
|
||||||
|
|
||||||
|
|
||||||
|
class BGTask:
|
||||||
|
executor = ThreadPoolExecutor(max_workers=1)
|
||||||
|
|
||||||
|
def __init__(self, func, *args, **kwargs):
|
||||||
|
self.future = self.executor.submit(func, *args, **kwargs)
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
return self.future.result()
|
||||||
|
|
||||||
|
def cancel(self) -> bool:
|
||||||
|
return self.future.cancel()
|
||||||
|
|
||||||
|
def done(self) -> bool:
|
||||||
|
return self.future.done()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shutdown(wait: bool = True):
|
||||||
|
BGTask.executor.shutdown(wait)
|
||||||
|
|
||||||
|
|
||||||
|
@register
|
||||||
|
def atexit():
|
||||||
|
BGTask.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_func(sql, *args, **kwargs):
|
||||||
|
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.execute(sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_many_func(sql, *args, **kwargs):
|
||||||
|
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
c.executemany(sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute(sql: str, *args, **kwargs):
|
||||||
|
'''异步执行SQL,日志库写入,注意不会直接返回结果'''
|
||||||
|
return BGTask(logdb_execute_func, sql, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def logdb_execute_many(sql: str, *args, **kwargs):
|
||||||
|
'''异步批量执行SQL,日志库写入,注意不会直接返回结果'''
|
||||||
|
return BGTask(logdb_execute_many_func, sql, *args, **kwargs)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from setting import Config
|
from .config_manager import Config
|
||||||
from .error import ArcError, InputError, NoData, ItemNotEnough
|
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .item import Item, ItemCore
|
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||||
|
from .item import CollectionItemMixin, ItemCore
|
||||||
|
|
||||||
|
|
||||||
class Level:
|
class Level:
|
||||||
@@ -9,9 +9,9 @@ class Level:
|
|||||||
min_level = 1
|
min_level = 1
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.max_level = None
|
self.max_level: int = None
|
||||||
self.level = None
|
self.level: int = None
|
||||||
self.exp = None
|
self.exp: float = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level_exp(self):
|
def level_exp(self):
|
||||||
@@ -29,9 +29,9 @@ class Level:
|
|||||||
|
|
||||||
a = []
|
a = []
|
||||||
b = []
|
b = []
|
||||||
for i in Constant.LEVEL_STEPS:
|
for k, v in Constant.LEVEL_STEPS.items():
|
||||||
a.append(i)
|
a.append(k)
|
||||||
b.append(Constant.LEVEL_STEPS[i])
|
b.append(v)
|
||||||
|
|
||||||
if exp < b[0]: # 向下溢出,是异常状态,不该被try捕获,不然数据库无法回滚
|
if exp < b[0]: # 向下溢出,是异常状态,不该被try捕获,不然数据库无法回滚
|
||||||
raise ValueError('EXP value error.')
|
raise ValueError('EXP value error.')
|
||||||
@@ -46,23 +46,10 @@ class Level:
|
|||||||
|
|
||||||
class Skill:
|
class Skill:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.skill_id = None
|
self.skill_id: str = None
|
||||||
self.skill_id_uncap = None
|
self.skill_id_uncap: str = None
|
||||||
self.skill_unlock_level = None
|
self.skill_unlock_level: int = None
|
||||||
self.skill_requires_uncap = None
|
self.skill_requires_uncap: bool = None
|
||||||
|
|
||||||
|
|
||||||
class Core(Item):
|
|
||||||
item_type = 'core'
|
|
||||||
|
|
||||||
def __init__(self, core_type: str = '', amount: int = 0) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.item_id = core_type
|
|
||||||
self.amount = amount
|
|
||||||
self.is_available = True
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {'core_type': self.item_id, 'amount': self.amount}
|
|
||||||
|
|
||||||
|
|
||||||
class CharacterValue:
|
class CharacterValue:
|
||||||
@@ -70,16 +57,23 @@ class CharacterValue:
|
|||||||
self.set_parameter(start, mid, end)
|
self.set_parameter(start, mid, end)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _calc_char_value_20(level, stata, statb, lva=1, lvb=20):
|
def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
|
||||||
# 计算1~20级搭档数值的核心函数,返回浮点数,来自https://redive.estertion.win/arcaea/calc/
|
# by Lost-MSth
|
||||||
n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
|
# 4/6859 = 0.00058317539
|
||||||
0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
|
if level <= 10:
|
||||||
e = n[lva] - n[lvb]
|
return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1
|
||||||
a = stata - statb
|
return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
|
||||||
r = a / e
|
|
||||||
d = stata - n[lva] * r
|
|
||||||
|
|
||||||
return d + r * n[level]
|
# @staticmethod
|
||||||
|
# def _calc_char_value_20(level: int, stata, statb, lva=1, lvb=20) -> float:
|
||||||
|
# # 计算1~20级搭档数值的核心函数,返回浮点数,来自https://redive.estertion.win/arcaea/calc/
|
||||||
|
# n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
|
||||||
|
# 0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
|
||||||
|
# e = n[lva] - n[lvb]
|
||||||
|
# a = stata - statb
|
||||||
|
# r = a / e
|
||||||
|
# d = stata - n[lva] * r
|
||||||
|
# return d + r * n[level]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
|
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
|
||||||
@@ -87,42 +81,51 @@ class CharacterValue:
|
|||||||
return (level - lva) * (statb - stata) / (lvb - lva) + stata
|
return (level - lva) * (statb - stata) / (lvb - lva) + stata
|
||||||
|
|
||||||
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
|
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
|
||||||
self.start = start
|
self.start: float = start
|
||||||
self.mid = mid
|
self.mid: float = mid
|
||||||
self.end = end
|
self.end: float = end
|
||||||
|
|
||||||
def get_value(self, level: Level):
|
def get_value(self, level: Level):
|
||||||
if level.min_level <= level.level <= level.mid_level:
|
if level.min_level <= level.level <= level.mid_level:
|
||||||
return self._calc_char_value_20(level.level, self.start, self.mid)
|
return self._calc_char_value_20_math(level.level, self.start, self.mid)
|
||||||
elif level.mid_level < level.level <= level.max_level:
|
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)
|
||||||
else:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class Character:
|
class Character(CollectionItemMixin):
|
||||||
database_table_name = None
|
database_table_name = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
collection_item_const = {
|
||||||
self.character_id = None
|
'name': 'character',
|
||||||
self.name = None
|
'table_name': 'char_item',
|
||||||
self.char_type = None
|
'table_primary_key': 'character_id',
|
||||||
self.is_uncapped = None
|
'id_name': 'character_id',
|
||||||
self.is_uncapped_override = None
|
'items_name': 'uncap_cores'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, c=None) -> None:
|
||||||
|
self.c = c
|
||||||
|
|
||||||
|
self.character_id: int = None
|
||||||
|
self.name: str = None
|
||||||
|
self.char_type: int = None
|
||||||
|
self.is_uncapped: bool = None
|
||||||
|
self.is_uncapped_override: bool = None
|
||||||
self.skill = Skill()
|
self.skill = Skill()
|
||||||
self.level = Level()
|
self.level = Level()
|
||||||
self.frag = CharacterValue()
|
self.frag = CharacterValue()
|
||||||
self.prog = CharacterValue()
|
self.prog = CharacterValue()
|
||||||
self.overdrive = CharacterValue()
|
self.overdrive = CharacterValue()
|
||||||
self.uncap_cores = []
|
self.uncap_cores: list = []
|
||||||
self.voice = None
|
self.voice: list = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skill_id_displayed(self) -> str:
|
def skill_id_displayed(self) -> str:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def uncap_cores_to_dict(self):
|
def uncap_cores_to_dict(self):
|
||||||
return [x.to_dict() for x in self.uncap_cores]
|
return [x.to_dict(character_format=True) for x in self.uncap_cores]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_uncapped_displayed(self) -> bool:
|
def is_uncapped_displayed(self) -> bool:
|
||||||
@@ -134,10 +137,76 @@ class Character:
|
|||||||
# 应该是只有对立这样
|
# 应该是只有对立这样
|
||||||
return self.character_id == 1
|
return self.character_id == 1
|
||||||
|
|
||||||
|
def to_dict(self, has_cores=False) -> dict:
|
||||||
|
# 用于API里,游戏内的数据不太一样,故不能super
|
||||||
|
r = {
|
||||||
|
'character_id': self.character_id,
|
||||||
|
'name': self.name,
|
||||||
|
'char_type': self.char_type,
|
||||||
|
'is_uncapped': self.is_uncapped,
|
||||||
|
'max_level': self.level.max_level,
|
||||||
|
'skill_id': self.skill.skill_id,
|
||||||
|
'skill_unlock_level': self.skill.skill_unlock_level,
|
||||||
|
'skill_requires_uncap': self.skill.skill_requires_uncap,
|
||||||
|
'skill_id_uncap': self.skill.skill_id_uncap,
|
||||||
|
'frag1': self.frag.start,
|
||||||
|
'frag20': self.frag.mid,
|
||||||
|
'frag30': self.frag.end,
|
||||||
|
'prog1': self.prog.start,
|
||||||
|
'prog20': self.prog.mid,
|
||||||
|
'prog30': self.prog.end,
|
||||||
|
'overdrive1': self.overdrive.start,
|
||||||
|
'overdrive20': self.overdrive.mid,
|
||||||
|
'overdrive30': self.overdrive.end,
|
||||||
|
}
|
||||||
|
if has_cores:
|
||||||
|
r['uncap_cores'] = self.uncap_cores_to_dict()
|
||||||
|
return r
|
||||||
|
|
||||||
|
def from_list(self, l: tuple) -> 'Character':
|
||||||
|
self.character_id = l[0]
|
||||||
|
self.name = l[1]
|
||||||
|
self.level.max_level = l[2]
|
||||||
|
self.frag.set_parameter(l[3], l[6], l[9])
|
||||||
|
self.prog.set_parameter(l[4], l[7], l[10])
|
||||||
|
self.overdrive.set_parameter(l[5], l[8], l[11])
|
||||||
|
self.skill.skill_id = l[12]
|
||||||
|
self.skill.skill_unlock_level = l[13]
|
||||||
|
self.skill.skill_requires_uncap = l[14] == 1
|
||||||
|
self.skill.skill_id_uncap = l[15]
|
||||||
|
self.char_type = l[16]
|
||||||
|
self.is_uncapped = l[17] == 1
|
||||||
|
return self
|
||||||
|
|
||||||
|
def select(self, character_id: int = None) -> 'Character':
|
||||||
|
if character_id is not None:
|
||||||
|
self.character_id = character_id
|
||||||
|
self.c.execute(
|
||||||
|
'select * from character where character_id = ?', (self.character_id,))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData(
|
||||||
|
f'No such character: {self.character_id}', api_error_code=-130)
|
||||||
|
return self.from_list(x)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self.c.execute('''update character set name = ?, max_level = ?, frag1 = ?, frag20 = ?, frag30 = ?, prog1 = ?, prog20 = ?, prog30 = ?, overdrive1 = ?, overdrive20 = ?, overdrive30 = ?, skill_id = ?, skill_unlock_level = ?, skill_requires_uncap = ?, skill_id_uncap = ?, char_type = ?, is_uncapped = ? where character_id = ?''', (
|
||||||
|
self.name, self.level.max_level, self.frag.start, self.frag.mid, self.frag.end, self.prog.start, self.prog.mid, self.prog.end, self.overdrive.start, self.overdrive.mid, self.overdrive.end, self.skill.skill_id, self.skill.skill_unlock_level, self.skill.skill_requires_uncap, self.skill.skill_id_uncap, self.char_type, self.is_uncapped, self.character_id))
|
||||||
|
|
||||||
|
def select_character_core(self):
|
||||||
|
# 获取此角色所需核心
|
||||||
|
self.c.execute(
|
||||||
|
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
|
||||||
|
x = self.c.fetchall()
|
||||||
|
if x:
|
||||||
|
self.uncap_cores = []
|
||||||
|
for i in x:
|
||||||
|
self.uncap_cores.append(ItemCore(self.c, i[0], i[1]))
|
||||||
|
|
||||||
|
|
||||||
class UserCharacter(Character):
|
class UserCharacter(Character):
|
||||||
'''
|
'''
|
||||||
用户角色类\
|
用户角色类
|
||||||
property: `user` - `User`类或子类的实例
|
property: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||||
@@ -148,32 +217,29 @@ class UserCharacter(Character):
|
|||||||
self.character_id = character_id
|
self.character_id = character_id
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
|
self.skill_flag: bool = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def skill_id_displayed(self) -> str:
|
def skill_id_displayed(self) -> str:
|
||||||
'''对外显示的技能id'''
|
'''对外显示的技能id'''
|
||||||
if self.is_uncapped_displayed and self.skill.skill_id_uncap:
|
if self.is_uncapped_displayed and self.skill.skill_id_uncap:
|
||||||
return self.skill.skill_id_uncap
|
return self.skill.skill_id_uncap
|
||||||
elif self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
|
if self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
|
||||||
return self.skill.skill_id
|
return self.skill.skill_id
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def select_character_core(self):
|
@property
|
||||||
# 获取此角色所需核心
|
def skill_state(self) -> str:
|
||||||
self.c.execute(
|
if self.skill_id_displayed == 'skill_maya':
|
||||||
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
|
return 'add_random' if self.skill_flag else 'remove_random'
|
||||||
x = self.c.fetchall()
|
return None
|
||||||
if x:
|
|
||||||
self.uncap_cores = []
|
|
||||||
for i in x:
|
|
||||||
self.uncap_cores.append(Core(i[0], i[1]))
|
|
||||||
|
|
||||||
def select_character_uncap_condition(self, user=None):
|
def select_character_uncap_condition(self, user=None):
|
||||||
# parameter: user - User类或子类的实例
|
# parameter: user - User类或子类的实例
|
||||||
# 获取此角色的觉醒信息
|
# 获取此角色的觉醒信息
|
||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = user
|
||||||
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name,
|
self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
|
||||||
{'a': self.user.user_id, 'b': self.character_id})
|
{'a': self.user.user_id, 'b': self.character_id})
|
||||||
|
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
@@ -190,29 +256,31 @@ class UserCharacter(Character):
|
|||||||
# 获取所给用户此角色信息
|
# 获取所给用户此角色信息
|
||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = user
|
||||||
self.c.execute('''select * from %s a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''' % self.database_table_name,
|
self.c.execute(f'''select * from {self.database_table_name} a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
|
||||||
(self.user.user_id, self.character_id))
|
(self.user.user_id, self.character_id))
|
||||||
|
|
||||||
y = self.c.fetchone()
|
y = self.c.fetchone()
|
||||||
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
|
||||||
|
|
||||||
if self.character_id == 21 or self.character_id == 46:
|
self.skill_flag = y[6] == 1
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
self.select_character_core()
|
self.select_character_core()
|
||||||
@@ -244,6 +312,11 @@ class UserCharacter(Character):
|
|||||||
r['fatalis_is_limited'] = False # emmmmmmm
|
r['fatalis_is_limited'] = False # emmmmmmm
|
||||||
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):
|
||||||
@@ -251,7 +324,7 @@ class UserCharacter(Character):
|
|||||||
# 切换觉醒状态
|
# 切换觉醒状态
|
||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = user
|
||||||
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name,
|
self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
|
||||||
{'a': self.user.user_id, 'b': self.character_id})
|
{'a': self.user.user_id, 'b': self.character_id})
|
||||||
|
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
@@ -261,7 +334,7 @@ class UserCharacter(Character):
|
|||||||
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
|
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
|
||||||
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
|
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
|
||||||
|
|
||||||
self.c.execute('''update %s set is_uncapped_override = :a where user_id = :b and character_id = :c''' % self.database_table_name, {
|
self.c.execute(f'''update {self.database_table_name} set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||||
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id})
|
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id})
|
||||||
|
|
||||||
self.is_uncapped_override = x[1] == 0
|
self.is_uncapped_override = x[1] == 0
|
||||||
@@ -291,11 +364,11 @@ class UserCharacter(Character):
|
|||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
|
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
|
||||||
y = self.c.fetchone()
|
y = self.c.fetchone()
|
||||||
if not y or i.amount > y[0]:
|
if i.amount > 0 and (not y or i.amount > y[0]):
|
||||||
raise ItemNotEnough('The cores are not enough.')
|
raise ItemNotEnough('The cores are not enough.')
|
||||||
|
|
||||||
for i in self.uncap_cores:
|
for i in self.uncap_cores:
|
||||||
ItemCore(self.c, i, reverse=True).user_claim_item(self.user)
|
i.user_claim_item(self.user, reverse=True)
|
||||||
|
|
||||||
self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
|
self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
|
||||||
(self.user.user_id, self.character_id))
|
(self.user.user_id, self.character_id))
|
||||||
@@ -332,8 +405,9 @@ class UserCharacter(Character):
|
|||||||
|
|
||||||
def upgrade_by_core(self, user=None, core=None):
|
def upgrade_by_core(self, user=None, core=None):
|
||||||
'''
|
'''
|
||||||
以太之滴升级,注意这里core.amount应该是负数\
|
以太之滴升级,注意这里core.amount应该是负数
|
||||||
parameter: `user` - `User`类或子类的实例\
|
|
||||||
|
parameter: `user` - `User`类或子类的实例
|
||||||
`core` - `ItemCore`类或子类的实例
|
`core` - `ItemCore`类或子类的实例
|
||||||
'''
|
'''
|
||||||
if user:
|
if user:
|
||||||
@@ -350,10 +424,16 @@ 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:
|
||||||
'''
|
'''
|
||||||
用户拥有角色列表类\
|
用户拥有角色列表类
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||||
@@ -365,7 +445,7 @@ class UserCharacterList:
|
|||||||
|
|
||||||
def select_user_characters(self):
|
def select_user_characters(self):
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select character_id from %s where user_id=?''' % self.database_table_name, (self.user.user_id,))
|
f'''select character_id from {self.database_table_name} where user_id=?''', (self.user.user_id,))
|
||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
self.characters: list = []
|
self.characters: list = []
|
||||||
if x:
|
if x:
|
||||||
|
|||||||
101
latest version/core/config_manager.py
Normal file
101
latest version/core/config_manager.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
class Config:
|
||||||
|
'''
|
||||||
|
Default config
|
||||||
|
'''
|
||||||
|
|
||||||
|
HOST = '0.0.0.0'
|
||||||
|
PORT = 80
|
||||||
|
|
||||||
|
DEPLOY_MODE = 'flask_multithread'
|
||||||
|
USE_PROXY_FIX = False
|
||||||
|
USE_CORS = False
|
||||||
|
|
||||||
|
SONG_FILE_HASH_PRE_CALCULATE = True
|
||||||
|
|
||||||
|
GAME_API_PREFIX = '/evolution/23'
|
||||||
|
|
||||||
|
ALLOW_APPVERSION = [] # list[str]
|
||||||
|
|
||||||
|
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
|
||||||
|
|
||||||
|
LINKPLAY_HOST = '0.0.0.0'
|
||||||
|
LINKPLAY_UDP_PORT = 10900
|
||||||
|
LINKPLAY_TCP_PORT = 10901
|
||||||
|
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
|
||||||
|
LINKPLAY_DISPLAY_HOST = ''
|
||||||
|
LINKPLAY_TCP_SECRET_KEY = '1145141919810'
|
||||||
|
|
||||||
|
SSL_CERT = ''
|
||||||
|
SSL_KEY = ''
|
||||||
|
|
||||||
|
IS_APRILFOOLS = True
|
||||||
|
|
||||||
|
WORLD_RANK_MAX = 200
|
||||||
|
|
||||||
|
AVAILABLE_MAP = [] # list[str]
|
||||||
|
|
||||||
|
USERNAME = 'admin'
|
||||||
|
PASSWORD = 'admin'
|
||||||
|
|
||||||
|
SECRET_KEY = '1145141919810'
|
||||||
|
|
||||||
|
API_TOKEN = ''
|
||||||
|
|
||||||
|
DOWNLOAD_LINK_PREFIX = ''
|
||||||
|
|
||||||
|
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
|
||||||
|
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
|
||||||
|
|
||||||
|
DOWNLOAD_TIMES_LIMIT = 3000
|
||||||
|
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||||
|
|
||||||
|
DOWNLOAD_FORBID_WHEN_NO_ITEM = False
|
||||||
|
|
||||||
|
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||||
|
ALLOW_LOGIN_SAME_DEVICE = False
|
||||||
|
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
||||||
|
|
||||||
|
ALLOW_SCORE_WITH_NO_SONG = True
|
||||||
|
|
||||||
|
ALLOW_INFO_LOG = False
|
||||||
|
ALLOW_WARNING_LOG = False
|
||||||
|
|
||||||
|
DEFAULT_MEMORIES = 0
|
||||||
|
|
||||||
|
UPDATE_WITH_NEW_CHARACTER_DATA = True
|
||||||
|
|
||||||
|
CHARACTER_FULL_UNLOCK = True
|
||||||
|
WORLD_SONG_FULL_UNLOCK = True
|
||||||
|
WORLD_SCENERY_FULL_UNLOCK = True
|
||||||
|
|
||||||
|
SAVE_FULL_UNLOCK = False
|
||||||
|
|
||||||
|
# ------------------------------------------
|
||||||
|
|
||||||
|
# You can change this to make another PTT mechanism.
|
||||||
|
BEST30_WEIGHT = 1 / 40
|
||||||
|
RECENT10_WEIGHT = 1 / 40
|
||||||
|
|
||||||
|
MAX_FRIEND_COUNT = 50
|
||||||
|
|
||||||
|
WORLD_MAP_FOLDER_PATH = './database/map/'
|
||||||
|
SONG_FILE_FOLDER_PATH = './database/songs/'
|
||||||
|
SONGLIST_FILE_PATH = './database/songs/songlist'
|
||||||
|
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
|
||||||
|
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
|
||||||
|
DATABASE_INIT_PATH = './database/init/'
|
||||||
|
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
|
||||||
|
|
||||||
|
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
||||||
|
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(config) -> None:
|
||||||
|
for k, v in config.__dict__.items():
|
||||||
|
if k.startswith('__') or k.endswith('__'):
|
||||||
|
continue
|
||||||
|
if hasattr(Config, k):
|
||||||
|
setattr(Config, k, v)
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
from setting import Config
|
from .config_manager import Config
|
||||||
|
|
||||||
|
ARCAEA_SERVER_VERSION = 'v2.11.2.7'
|
||||||
|
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||||
|
|
||||||
|
|
||||||
class Constant:
|
class Constant:
|
||||||
@@ -9,29 +12,38 @@ class Constant:
|
|||||||
|
|
||||||
STAMINA_RECOVER_TICK = 1800000
|
STAMINA_RECOVER_TICK = 1800000
|
||||||
|
|
||||||
|
COURSE_STAMINA_COST = 4
|
||||||
|
|
||||||
CORE_EXP = 250
|
CORE_EXP = 250
|
||||||
|
|
||||||
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',
|
||||||
|
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
|
||||||
|
|
||||||
MAX_FRIEND_COUNT = 50
|
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
||||||
|
|
||||||
MY_RANK_MAX_LOCAL_POSITION = 5
|
MY_RANK_MAX_LOCAL_POSITION = 5
|
||||||
MY_RANK_MAX_GLOBAL_POSITION = 9999
|
MY_RANK_MAX_GLOBAL_POSITION = 9999
|
||||||
|
|
||||||
# You can change this to make another PTT mechanism.
|
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
||||||
BEST30_WEIGHT = 1 / 40
|
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
||||||
RECENT10_WEIGHT = 1 / 40
|
|
||||||
|
|
||||||
WORLD_MAP_FOLDER_PATH = './database/map/'
|
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
|
||||||
SONG_FILE_FOLDER_PATH = './database/songs/'
|
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
|
||||||
SONGLIST_FILE_PATH = './database/songs/songlist'
|
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
|
||||||
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
|
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
||||||
|
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
||||||
|
|
||||||
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
||||||
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
||||||
@@ -44,8 +56,8 @@ class Constant:
|
|||||||
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
|
||||||
COURSE_STAMINA_COST = 4
|
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||||
|
|
||||||
# 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 = [
|
||||||
@@ -93,3 +105,10 @@ class Constant:
|
|||||||
(0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE,
|
(0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE,
|
||||||
0x6CB300), (0XA35687B, 0xE456CDEA)
|
0x6CB300), (0XA35687B, 0xE456CDEA)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
|
||||||
|
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
|
||||||
|
|
||||||
|
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
|
||||||
|
|
||||||
|
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class Course:
|
|||||||
'''select * from course where course_id = ?''', (self.course_id,))
|
'''select * from course where course_id = ?''', (self.course_id,))
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The course `%s` is not found.' % self.course_id)
|
raise NoData(f'The course `{self.course_id}` is not found.')
|
||||||
return self.from_list(x)
|
return self.from_list(x)
|
||||||
|
|
||||||
def select_course_chart(self) -> None:
|
def select_course_chart(self) -> None:
|
||||||
@@ -151,7 +151,8 @@ class Course:
|
|||||||
|
|
||||||
class UserCourse(Course):
|
class UserCourse(Course):
|
||||||
'''
|
'''
|
||||||
用户课题类\
|
用户课题类
|
||||||
|
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -200,7 +201,8 @@ class UserCourse(Course):
|
|||||||
|
|
||||||
class UserCourseList:
|
class UserCourseList:
|
||||||
'''
|
'''
|
||||||
用户课题列表类\
|
用户课题列表类
|
||||||
|
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -237,8 +239,9 @@ class UserCourseList:
|
|||||||
|
|
||||||
class CoursePlay(UserCourse):
|
class CoursePlay(UserCourse):
|
||||||
'''
|
'''
|
||||||
课题模式打歌类,联动UserPlay\
|
课题模式打歌类,联动UserPlay
|
||||||
parameter: `user` - `UserOnline`类或子类的实例\
|
|
||||||
|
parameter: `user` - `UserOnline`类或子类的实例
|
||||||
'user_play` - `UserPlay`类的实例
|
'user_play` - `UserPlay`类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ from time import time
|
|||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
|
from .config_manager import Config
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .error import NoAccess
|
from .error import NoAccess
|
||||||
|
from .limiter import ArcLimiter
|
||||||
from .user import User
|
from .user import User
|
||||||
from .util import get_file_md5, md5
|
from .util import get_file_md5, md5
|
||||||
|
|
||||||
@@ -19,39 +21,138 @@ def get_song_file_md5(song_id: str, file_name: str) -> str:
|
|||||||
return get_file_md5(path)
|
return get_file_md5(path)
|
||||||
|
|
||||||
|
|
||||||
def initialize_songfile():
|
class SonglistParser:
|
||||||
'''初始化歌曲数据的md5信息'''
|
'''songlist文件解析器'''
|
||||||
get_song_file_md5.cache_clear()
|
|
||||||
x = DownloadList()
|
|
||||||
x.url_flag = False
|
|
||||||
x.add_songs()
|
|
||||||
del x
|
|
||||||
|
|
||||||
|
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
|
||||||
|
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4']
|
||||||
|
|
||||||
@lru_cache()
|
has_songlist = False
|
||||||
def get_only_3_song_ids():
|
songs: dict = {} # {song_id: value, ...}
|
||||||
'''初始化只能下载byd相关的歌曲id'''
|
# value: bit 76543210
|
||||||
if not os.path.isfile(Constant.SONGLIST_FILE_PATH):
|
# 7: video_audio.ogg
|
||||||
return []
|
# 6: video.mp4
|
||||||
only_3_song_ids = []
|
# 5: 3.ogg
|
||||||
data = []
|
# 4: base.ogg
|
||||||
with open(Constant.SONGLIST_FILE_PATH, 'r', encoding='utf-8') as f:
|
# 3: 3.aff
|
||||||
data = loads(f.read())['songs']
|
# 2: 2.aff
|
||||||
for x in data:
|
# 1: 1.aff
|
||||||
if 'remote_dl' not in x or 'remote_dl' in x and not x['remote_dl']:
|
# 0: 0.aff
|
||||||
if any(i['ratingClass'] == 3 for i in x['difficulties']):
|
|
||||||
only_3_song_ids.append(x['id'])
|
pack_info: 'dict[str, set]' = {} # {pack_id: {song_id, ...}, ...}
|
||||||
return only_3_song_ids
|
free_songs: set = set() # {song_id, ...}
|
||||||
|
world_songs: set = set() # {world_song_id, ...}
|
||||||
|
|
||||||
|
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
|
||||||
|
self.path = path
|
||||||
|
self.data: list = []
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_available_file(song_id: str, file_name: str) -> bool:
|
||||||
|
'''判断文件是否允许被下载'''
|
||||||
|
if song_id not in SonglistParser.songs:
|
||||||
|
# songlist没有,则只限制文件名
|
||||||
|
return file_name in SonglistParser.FILE_NAMES
|
||||||
|
rule = SonglistParser.songs[song_id]
|
||||||
|
for i in range(10):
|
||||||
|
if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0:
|
||||||
|
return True
|
||||||
|
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:
|
||||||
|
'''解析单个歌曲'''
|
||||||
|
# TODO: byd_local_unlock ???
|
||||||
|
if not 'id' in song:
|
||||||
|
return {}
|
||||||
|
r = 0
|
||||||
|
if 'remote_dl' in song and song['remote_dl']:
|
||||||
|
r |= 16
|
||||||
|
for i in song.get('difficulties', []):
|
||||||
|
if i['ratingClass'] == 3 and i.get('audioOverride', False):
|
||||||
|
r |= 32
|
||||||
|
r |= 1 << i['ratingClass']
|
||||||
|
else:
|
||||||
|
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
|
||||||
|
r |= 8
|
||||||
|
|
||||||
|
for extra_file in song.get('additional_files', []):
|
||||||
|
x = extra_file['file_name']
|
||||||
|
if x == SonglistParser.FILE_NAMES[6]:
|
||||||
|
r |= 64
|
||||||
|
elif x == SonglistParser.FILE_NAMES[7]:
|
||||||
|
r |= 128
|
||||||
|
elif x == SonglistParser.FILE_NAMES[8]:
|
||||||
|
r |= 256
|
||||||
|
elif x == SonglistParser.FILE_NAMES[9]:
|
||||||
|
r |= 512
|
||||||
|
|
||||||
|
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:
|
||||||
|
'''解析songlist文件'''
|
||||||
|
if not os.path.isfile(self.path):
|
||||||
|
return
|
||||||
|
with open(self.path, 'rb') as f:
|
||||||
|
self.data = loads(f.read()).get('songs', [])
|
||||||
|
self.has_songlist = True
|
||||||
|
for x in self.data:
|
||||||
|
self.songs.update(self.parse_one(x))
|
||||||
|
self.parse_one_unlock(x)
|
||||||
|
|
||||||
|
|
||||||
class UserDownload:
|
class UserDownload:
|
||||||
'''
|
'''
|
||||||
用户下载类\
|
用户下载类
|
||||||
properties: `user` - `User`类或子类的实例
|
|
||||||
|
properties: `user` - `UserInfo`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, c=None, user=None) -> None:
|
limiter = ArcLimiter(
|
||||||
self.c = c
|
str(Constant.DOWNLOAD_TIMES_LIMIT) + '/day', 'download')
|
||||||
|
|
||||||
|
def __init__(self, c_m=None, user=None) -> None:
|
||||||
|
self.c_m = c_m
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.song_id: str = None
|
self.song_id: str = None
|
||||||
@@ -60,19 +161,13 @@ class UserDownload:
|
|||||||
self.token: str = None
|
self.token: str = None
|
||||||
self.token_time: int = None
|
self.token_time: int = None
|
||||||
|
|
||||||
def clear_user_download(self) -> None:
|
|
||||||
self.c.execute(
|
|
||||||
'''delete from user_download where user_id = :a and time <= :b''', {'a': self.user.user_id, 'b': int(time()) - 24*3600})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_limited(self) -> bool:
|
def is_limited(self) -> bool:
|
||||||
'''是否达到用户最大下载量'''
|
'''是否达到用户最大下载量'''
|
||||||
if self.user is None:
|
if self.user is None:
|
||||||
self.select_for_check()
|
self.select_for_check()
|
||||||
self.c.execute(
|
|
||||||
'''select count(*) from user_download where user_id = :a''', {'a': self.user.user_id})
|
return not self.limiter.test(str(self.user.user_id))
|
||||||
y = self.c.fetchone()
|
|
||||||
return y is not None and y[0] > Constant.DOWNLOAD_TIMES_LIMIT
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
@@ -81,19 +176,19 @@ class UserDownload:
|
|||||||
self.select_for_check()
|
self.select_for_check()
|
||||||
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT
|
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT
|
||||||
|
|
||||||
def insert_user_download(self) -> None:
|
def download_hit(self) -> bool:
|
||||||
'''记录下载信息'''
|
'''下载次数+1,返回成功与否bool值'''
|
||||||
self.c.execute('''insert into user_download values(:a,:b,:c)''', {
|
return self.limiter.hit(str(self.user.user_id))
|
||||||
'a': self.user.user_id, 'c': self.token, 'b': int(time())})
|
|
||||||
|
|
||||||
def select_for_check(self) -> None:
|
def select_for_check(self) -> None:
|
||||||
'''利用token、song_id、file_name查询其它信息'''
|
'''利用token、song_id、file_name查询其它信息'''
|
||||||
self.c.execute('''select user_id, time from download_token where song_id=? and file_name=? and token = ? limit 1;''',
|
self.c_m.execute('''select user_id, time from download_token where song_id=? and file_name=? and token = ? limit 1;''',
|
||||||
(self.song_id, self.file_name, self.token))
|
(self.song_id, self.file_name, self.token))
|
||||||
|
|
||||||
x = self.c.fetchone()
|
x = self.c_m.fetchone()
|
||||||
if not x:
|
if not x:
|
||||||
raise NoAccess('The token `%s` is not valid.' % self.token, status=403)
|
raise NoAccess('The token `%s` is not valid.' %
|
||||||
|
self.token, status=403)
|
||||||
self.user = User()
|
self.user = User()
|
||||||
self.user.user_id = x[0]
|
self.user.user_id = x[0]
|
||||||
self.token_time = x[1]
|
self.token_time = x[1]
|
||||||
@@ -101,11 +196,11 @@ class UserDownload:
|
|||||||
def generate_token(self) -> None:
|
def generate_token(self) -> None:
|
||||||
self.token_time = int(time())
|
self.token_time = int(time())
|
||||||
self.token = md5(str(self.user.user_id) + self.song_id +
|
self.token = md5(str(self.user.user_id) + self.song_id +
|
||||||
self.file_name + str(self.token_time))
|
self.file_name + str(self.token_time) + str(os.urandom(8)))
|
||||||
|
|
||||||
def insert_download_token(self) -> None:
|
def insert_download_token(self) -> None:
|
||||||
'''将数据插入数据库,让这个下载链接可用'''
|
'''将数据插入数据库,让这个下载链接可用'''
|
||||||
self.c.execute('''insert into download_token values(:a,:b,:c,:d,:e)''', {
|
self.c_m.execute('''insert or replace into download_token values(:a,:b,:c,:d,:e)''', {
|
||||||
'a': self.user.user_id, 'b': self.song_id, 'c': self.file_name, 'd': self.token, 'e': self.token_time})
|
'a': self.user.user_id, 'b': self.song_id, 'c': self.file_name, 'd': self.token, 'e': self.token_time})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -113,14 +208,13 @@ class UserDownload:
|
|||||||
'''生成下载链接'''
|
'''生成下载链接'''
|
||||||
if self.token is None:
|
if self.token is None:
|
||||||
self.generate_token()
|
self.generate_token()
|
||||||
self.insert_download_token()
|
# self.insert_download_token() # 循环插入速度慢,改为executemany
|
||||||
if Constant.DOWNLOAD_LINK_PREFIX:
|
if Constant.DOWNLOAD_LINK_PREFIX:
|
||||||
prefix = Constant.DOWNLOAD_LINK_PREFIX
|
prefix = Constant.DOWNLOAD_LINK_PREFIX
|
||||||
if prefix[-1] != '/':
|
if prefix[-1] != '/':
|
||||||
prefix += '/'
|
prefix += '/'
|
||||||
return prefix + self.song_id + '/' + self.file_name + '?t=' + self.token
|
return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
|
||||||
else:
|
return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
|
||||||
return url_for('download', file_path=self.song_id + '/' + self.file_name, t=self.token, _external=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash(self) -> str:
|
def hash(self) -> str:
|
||||||
@@ -129,12 +223,12 @@ class UserDownload:
|
|||||||
|
|
||||||
class DownloadList(UserDownload):
|
class DownloadList(UserDownload):
|
||||||
'''
|
'''
|
||||||
下载列表类\
|
下载列表类
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, c=None, user=None) -> None:
|
def __init__(self, c_m=None, user=None) -> None:
|
||||||
super().__init__(c, user)
|
super().__init__(c_m, user)
|
||||||
|
|
||||||
self.song_ids: list = None
|
self.song_ids: list = None
|
||||||
self.url_flag: bool = None
|
self.url_flag: bool = None
|
||||||
@@ -142,23 +236,55 @@ class DownloadList(UserDownload):
|
|||||||
self.downloads: list = []
|
self.downloads: list = []
|
||||||
self.urls: dict = {}
|
self.urls: dict = {}
|
||||||
|
|
||||||
def clear_download_token_from_song(self, song_id: str) -> None:
|
@classmethod
|
||||||
self.c.execute('''delete from download_token where user_id=:a and song_id=:b''', {
|
def initialize_cache(cls) -> None:
|
||||||
'a': self.user.user_id, 'b': song_id})
|
'''初始化歌曲数据缓存,包括md5、文件目录遍历、解析songlist'''
|
||||||
|
SonglistParser()
|
||||||
|
if Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||||
|
x = cls()
|
||||||
|
x.url_flag = False
|
||||||
|
x.add_songs()
|
||||||
|
del x
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear_all_cache() -> None:
|
||||||
|
'''清除所有歌曲文件有关缓存'''
|
||||||
|
get_song_file_md5.cache_clear()
|
||||||
|
DownloadList.get_one_song_file_names.cache_clear()
|
||||||
|
DownloadList.get_all_song_ids.cache_clear()
|
||||||
|
SonglistParser.songs = {}
|
||||||
|
SonglistParser.pack_info = {}
|
||||||
|
SonglistParser.free_songs = set()
|
||||||
|
SonglistParser.world_songs = set()
|
||||||
|
SonglistParser.has_songlist = False
|
||||||
|
|
||||||
|
def clear_download_token(self) -> None:
|
||||||
|
'''清除过期下载链接'''
|
||||||
|
self.c_m.execute('''delete from download_token where time<?''',
|
||||||
|
(int(time()) - Constant.DOWNLOAD_TIME_GAP_LIMIT,))
|
||||||
|
|
||||||
|
def insert_download_tokens(self) -> None:
|
||||||
|
'''插入所有下载链接'''
|
||||||
|
self.c_m.executemany('''insert or replace into download_token values(?,?,?,?,?)''', [(
|
||||||
|
self.user.user_id, x.song_id, x.file_name, x.token, x.token_time) for x in self.downloads])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@lru_cache(maxsize=2048)
|
||||||
|
def get_one_song_file_names(song_id: str) -> list:
|
||||||
|
'''获取一个歌曲文件夹下的所有合法文件名,有lru缓存'''
|
||||||
|
r = []
|
||||||
|
for i in os.scandir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)):
|
||||||
|
file_name = i.name
|
||||||
|
if i.is_file() and SonglistParser.is_available_file(song_id, file_name):
|
||||||
|
r.append(file_name)
|
||||||
|
return r
|
||||||
|
|
||||||
def add_one_song(self, song_id: str) -> None:
|
def add_one_song(self, song_id: str) -> None:
|
||||||
if self.url_flag:
|
|
||||||
self.clear_download_token_from_song(song_id)
|
|
||||||
dir_list = os.listdir(os.path.join(
|
|
||||||
Constant.SONG_FILE_FOLDER_PATH, song_id))
|
|
||||||
|
|
||||||
re = {}
|
re = {}
|
||||||
for i in dir_list:
|
for i in self.get_one_song_file_names(song_id):
|
||||||
if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']:
|
x = UserDownload(self.c_m, self.user)
|
||||||
if song_id in get_only_3_song_ids() and i not in ['3.aff', '3.ogg']:
|
self.downloads.append(x)
|
||||||
continue
|
|
||||||
x = UserDownload(self.c, self.user)
|
|
||||||
# self.downloads.append(x) # 这实际上没有用
|
|
||||||
x.song_id = song_id
|
x.song_id = song_id
|
||||||
x.file_name = i
|
x.file_name = i
|
||||||
if i == 'base.ogg':
|
if i == 'base.ogg':
|
||||||
@@ -176,7 +302,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 == 'video.mp4' or i == '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'] = []
|
||||||
|
|
||||||
@@ -186,6 +312,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'] = {}
|
||||||
@@ -197,13 +324,36 @@ class DownloadList(UserDownload):
|
|||||||
|
|
||||||
self.urls.update({song_id: re})
|
self.urls.update({song_id: re})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@lru_cache()
|
||||||
|
def get_all_song_ids() -> list:
|
||||||
|
'''获取全歌曲文件夹列表,有lru缓存'''
|
||||||
|
return [i.name for i in os.scandir(Constant.SONG_FILE_FOLDER_PATH) if i.is_dir()]
|
||||||
|
|
||||||
def add_songs(self, song_ids: list = None) -> None:
|
def add_songs(self, song_ids: list = None) -> None:
|
||||||
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
|
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
|
||||||
if song_ids is not None:
|
if song_ids is not None:
|
||||||
self.song_ids = song_ids
|
self.song_ids = song_ids
|
||||||
|
|
||||||
x = self.song_ids if self.song_ids else os.listdir(
|
if not self.song_ids:
|
||||||
Constant.SONG_FILE_FOLDER_PATH)
|
self.song_ids = self.get_all_song_ids()
|
||||||
for i in x:
|
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:
|
||||||
|
self.add_one_song(i)
|
||||||
|
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:
|
||||||
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)
|
||||||
|
|
||||||
|
if self.url_flag:
|
||||||
|
self.clear_download_token()
|
||||||
|
self.insert_download_tokens()
|
||||||
|
|||||||
@@ -1,91 +1,113 @@
|
|||||||
class ArcError(Exception):
|
class ArcError(Exception):
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
self.message = message
|
self.message: str = message
|
||||||
self.error_code = error_code
|
self.error_code: int = error_code
|
||||||
self.api_error_code = api_error_code
|
self.api_error_code: int = api_error_code
|
||||||
self.extra_data = extra_data
|
self.extra_data = extra_data
|
||||||
self.status = status
|
self.status: int = status
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return repr(self.message)
|
return repr(self.message)
|
||||||
|
|
||||||
|
|
||||||
class InputError(ArcError):
|
class InputError(ArcError):
|
||||||
# 输入类型错误
|
'''输入类型错误'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class DataExist(ArcError):
|
class DataExist(ArcError):
|
||||||
# 数据存在
|
'''数据存在'''
|
||||||
pass
|
|
||||||
|
def __init__(self, message=None, error_code=108, api_error_code=-4, extra_data=None, status=200) -> None:
|
||||||
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class NoData(ArcError):
|
class NoData(ArcError):
|
||||||
# 数据不存在
|
'''数据不存在'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class PostError(ArcError):
|
class PostError(ArcError):
|
||||||
# 缺少输入
|
'''缺少输入'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class UserBan(ArcError):
|
class UserBan(ArcError):
|
||||||
# 用户封禁
|
'''用户封禁'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class ItemNotEnough(ArcError):
|
class ItemNotEnough(ArcError):
|
||||||
# 物品数量不足
|
'''物品数量不足'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class ItemUnavailable(ArcError):
|
class ItemUnavailable(ArcError):
|
||||||
# 物品不可用
|
'''物品不可用'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class RedeemUnavailable(ArcError):
|
class RedeemUnavailable(ArcError):
|
||||||
# 兑换码不可用
|
'''兑换码不可用'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=505, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=505, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class MapLocked(ArcError):
|
class MapLocked(ArcError):
|
||||||
# 地图锁定
|
'''地图锁定'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class StaminaNotEnough(ArcError):
|
class StaminaNotEnough(ArcError):
|
||||||
# 体力不足
|
'''体力不足'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=107, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=107, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class TicketNotEnough(ArcError):
|
class TicketNotEnough(ArcError):
|
||||||
# 记忆源点不足
|
'''记忆源点不足'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class FriendError(ArcError):
|
class FriendError(ArcError):
|
||||||
# 好友系统出错
|
'''好友系统出错'''
|
||||||
|
|
||||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|
||||||
|
|
||||||
class NoAccess(ArcError):
|
class NoAccess(ArcError):
|
||||||
# 无权限
|
'''无权限'''
|
||||||
pass
|
|
||||||
|
def __init__(self, message=None, error_code=108, 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
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimit(ArcError):
|
||||||
|
'''频率达到限制'''
|
||||||
|
|
||||||
|
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=429) -> None:
|
||||||
|
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||||
|
|||||||
326
latest version/core/init.py
Normal file
326
latest version/core/init.py
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from importlib import import_module
|
||||||
|
from json import load
|
||||||
|
from shutil import copy, copy2
|
||||||
|
from time import time
|
||||||
|
from traceback import format_exc
|
||||||
|
|
||||||
|
from core.config_manager import Config
|
||||||
|
from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
|
||||||
|
from core.course import Course
|
||||||
|
from core.download import DownloadList
|
||||||
|
from core.purchase import Purchase
|
||||||
|
from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
|
||||||
|
MemoryDatabase)
|
||||||
|
from core.user import UserRegister
|
||||||
|
from core.util import try_rename
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseInit:
|
||||||
|
def __init__(self, db_path: str = Config.SQLITE_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
|
||||||
|
self.db_path = db_path
|
||||||
|
self.init_folder_path = init_folder_path
|
||||||
|
self.c = None
|
||||||
|
self.init_data = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sql_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'tables.sql')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def course_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'courses.json')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pack_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'packs.json')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def single_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'singles.json')
|
||||||
|
|
||||||
|
def table_init(self) -> None:
|
||||||
|
'''初始化数据库结构'''
|
||||||
|
with open(self.sql_path, 'r', encoding='utf-8') as f:
|
||||||
|
self.c.executescript(f.read())
|
||||||
|
self.c.execute('''insert into config values("version", :a);''', {
|
||||||
|
'a': ARCAEA_SERVER_VERSION})
|
||||||
|
|
||||||
|
def character_init(self) -> None:
|
||||||
|
'''初始化搭档信息'''
|
||||||
|
uncapped_characters = self.init_data.char_core.keys()
|
||||||
|
for i in range(0, len(self.init_data.char)):
|
||||||
|
skill_requires_uncap = 1 if i == 2 else 0
|
||||||
|
if i in uncapped_characters:
|
||||||
|
max_level = 30
|
||||||
|
uncapable = 1
|
||||||
|
else:
|
||||||
|
max_level = 20
|
||||||
|
uncapable = 0
|
||||||
|
sql = '''insert into character values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'''
|
||||||
|
self.c.execute(sql, (i, self.init_data.char[i], max_level, self.init_data.frag1[i], self.init_data.prog1[i], self.init_data.overdrive1[i], self.init_data.frag20[i], self.init_data.prog20[i], self.init_data.overdrive20[i],
|
||||||
|
self.init_data.frag30[i], self.init_data.prog30[i], self.init_data.overdrive30[i], self.init_data.skill_id[i], self.init_data.skill_unlock_level[i], skill_requires_uncap, self.init_data.skill_id_uncap[i], self.init_data.char_type[i], uncapable))
|
||||||
|
|
||||||
|
self.c.execute('''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)''', (
|
||||||
|
99, 'shirahime', 38, 33, 28, 66, 58, 50, 66, 58, 50, 'frags_preferred_song', 0, 0, '', 0))
|
||||||
|
|
||||||
|
for i in self.init_data.char_core:
|
||||||
|
self.c.executemany('''insert into char_item values(?,?,'core',?)''', [
|
||||||
|
(i, j['core_id'], j['amount']) for j in self.init_data.char_core[i]])
|
||||||
|
|
||||||
|
def insert_purchase_item(self, purchases: list):
|
||||||
|
'''处理singles和packs'''
|
||||||
|
for i in purchases:
|
||||||
|
x = Purchase(self.c).from_dict(i)
|
||||||
|
x.insert_all()
|
||||||
|
|
||||||
|
def item_init(self) -> None:
|
||||||
|
'''初始化物品信息'''
|
||||||
|
self.c.executemany('''insert into item values(?,"core",1)''',
|
||||||
|
[(i,) for i in self.init_data.cores])
|
||||||
|
self.c.executemany('''insert into item values(?,"world_song",1)''', [
|
||||||
|
(i,) for i in self.init_data.world_songs])
|
||||||
|
self.c.executemany('''insert into item values(?,"world_unlock",1)''', [
|
||||||
|
(i,) for i in self.init_data.world_unlocks])
|
||||||
|
self.c.executemany('''insert into item values(?,"course_banner",1)''', [
|
||||||
|
(i,) for i in self.init_data.course_banners])
|
||||||
|
|
||||||
|
self.c.execute('''insert into item values(?,?,?)''',
|
||||||
|
('fragment', 'fragment', 1))
|
||||||
|
self.c.execute('''insert into item values(?,?,?)''',
|
||||||
|
('memory', 'memory', 1))
|
||||||
|
self.c.execute('''insert into item values(?,?,?)''',
|
||||||
|
('anni5tix', 'anni5tix', 1))
|
||||||
|
|
||||||
|
with open(self.pack_path, 'rb') as f:
|
||||||
|
self.insert_purchase_item(load(f))
|
||||||
|
|
||||||
|
with open(self.single_path, 'rb') as f:
|
||||||
|
self.insert_purchase_item(load(f))
|
||||||
|
|
||||||
|
def course_init(self) -> None:
|
||||||
|
'''初始化课题信息'''
|
||||||
|
courses = []
|
||||||
|
with open(self.course_path, 'rb') as f:
|
||||||
|
courses = load(f)
|
||||||
|
for i in courses:
|
||||||
|
x = Course(self.c).from_dict(i)
|
||||||
|
x.insert_all()
|
||||||
|
|
||||||
|
def role_power_init(self) -> None:
|
||||||
|
'''初始化power和role'''
|
||||||
|
self.c.executemany('''insert into role values(?,?)''', [(
|
||||||
|
self.init_data.role[i], self.init_data.role_caption[i]) for i in range(len(self.init_data.role))])
|
||||||
|
|
||||||
|
self.c.executemany('''insert into power values(?,?)''', [(
|
||||||
|
self.init_data.power[i], self.init_data.power_caption[i]) for i in range(len(self.init_data.power))])
|
||||||
|
|
||||||
|
for i in self.init_data.role_power:
|
||||||
|
self.c.executemany('''insert into role_power values(?,?)''', [
|
||||||
|
(i, j) for j in self.init_data.role_power[i]])
|
||||||
|
|
||||||
|
def admin_init(self) -> None:
|
||||||
|
'''初始化测试账号'''
|
||||||
|
x = UserRegister(self.c)
|
||||||
|
x.user_code = '123456789'
|
||||||
|
x.user_id = 2000000
|
||||||
|
x.name = 'admin'
|
||||||
|
x.email = 'admin@admin.com'
|
||||||
|
now = int(time() * 1000)
|
||||||
|
|
||||||
|
x._insert_user_char()
|
||||||
|
|
||||||
|
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)
|
||||||
|
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
|
||||||
|
''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email})
|
||||||
|
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
||||||
|
'user_id': x.user_id})
|
||||||
|
self.c.execute(
|
||||||
|
'''insert into user_role values(?, "admin")''', (x.user_id,))
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
sys.path.append(os.path.join(sys.path[0], self.init_folder_path))
|
||||||
|
self.init_data = import_module('arc_data').InitData
|
||||||
|
|
||||||
|
with Connect(self.db_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_init()
|
||||||
|
self.character_init()
|
||||||
|
self.item_init()
|
||||||
|
self.course_init()
|
||||||
|
self.role_power_init()
|
||||||
|
self.admin_init()
|
||||||
|
|
||||||
|
|
||||||
|
class LogDatabaseInit:
|
||||||
|
def __init__(self, db_path: str = Config.SQLITE_LOG_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
|
||||||
|
self.db_path = db_path
|
||||||
|
self.init_folder_path = init_folder_path
|
||||||
|
self.c = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sql_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'log_tables.sql')
|
||||||
|
|
||||||
|
def table_init(self) -> None:
|
||||||
|
'''初始化数据库结构'''
|
||||||
|
with open(self.sql_path, 'r') as f:
|
||||||
|
self.c.executescript(f.read())
|
||||||
|
self.c.execute(
|
||||||
|
'''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION})
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
with Connect(self.db_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_init()
|
||||||
|
|
||||||
|
|
||||||
|
class FileChecker:
|
||||||
|
'''文件检查及初始化类'''
|
||||||
|
|
||||||
|
def __init__(self, logger=None):
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def check_file(self, file_path: str) -> bool:
|
||||||
|
f = os.path.isfile(file_path)
|
||||||
|
if not f:
|
||||||
|
self.logger.warning('File `%s` is missing.' % file_path)
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_folder(self, folder_path: str) -> bool:
|
||||||
|
f = os.path.isdir(folder_path)
|
||||||
|
if not f:
|
||||||
|
self.logger.warning('Folder `%s` is missing.' % folder_path)
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_update_database(self) -> bool:
|
||||||
|
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
|
||||||
|
# 新建日志数据库
|
||||||
|
try:
|
||||||
|
self.logger.info(
|
||||||
|
f'Try to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||||
|
LogDatabaseInit().init()
|
||||||
|
self.logger.info(
|
||||||
|
f'Success to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.error(
|
||||||
|
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# 检查更新
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
|
||||||
|
try:
|
||||||
|
x = c.execute(
|
||||||
|
'''select value from cache where key="version"''').fetchone()
|
||||||
|
except:
|
||||||
|
x = None
|
||||||
|
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
|
||||||
|
self.logger.warning(
|
||||||
|
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
|
||||||
|
try:
|
||||||
|
self.logger.info(
|
||||||
|
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
self.update_log_database()
|
||||||
|
self.logger.info(
|
||||||
|
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.error(
|
||||||
|
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
||||||
|
# 新建数据库
|
||||||
|
try:
|
||||||
|
self.logger.info(
|
||||||
|
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
DatabaseInit().init()
|
||||||
|
self.logger.info(
|
||||||
|
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# 检查更新
|
||||||
|
with Connect() as c:
|
||||||
|
try:
|
||||||
|
c.execute('''select value from config where id="version"''')
|
||||||
|
x = c.fetchone()
|
||||||
|
except:
|
||||||
|
x = None
|
||||||
|
# 数据库自动更新,不强求
|
||||||
|
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
||||||
|
self.logger.warning(
|
||||||
|
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
try:
|
||||||
|
self.logger.info(
|
||||||
|
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
|
||||||
|
if not os.path.isdir(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(
|
||||||
|
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(Config.SQLITE_DATABASE_PATH)[-1] + '.bak'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
copy2(backup_path, Config.SQLITE_DATABASE_PATH)
|
||||||
|
except:
|
||||||
|
copy(backup_path, Config.SQLITE_DATABASE_PATH)
|
||||||
|
|
||||||
|
temp_path = os.path.join(
|
||||||
|
*os.path.split(Config.SQLITE_DATABASE_PATH)[:-1], 'old_arcaea_database.db')
|
||||||
|
if os.path.isfile(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
|
||||||
|
try_rename(Config.SQLITE_DATABASE_PATH, temp_path)
|
||||||
|
|
||||||
|
DatabaseInit().init()
|
||||||
|
self.update_database(temp_path)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.warning(
|
||||||
|
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
|
||||||
|
'''更新数据库,并删除旧文件'''
|
||||||
|
if os.path.isfile(old_path) and os.path.isfile(new_path):
|
||||||
|
DatabaseMigrator(old_path, new_path).update_database()
|
||||||
|
os.remove(old_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
|
||||||
|
'''直接更新日志数据库'''
|
||||||
|
if os.path.isfile(old_path):
|
||||||
|
LogDatabaseMigrator(old_path).update_database()
|
||||||
|
|
||||||
|
def check_song_file(self) -> bool:
|
||||||
|
'''检查song有关文件并初始化缓存'''
|
||||||
|
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
|
||||||
|
self.logger.info("Start to initialize song data...")
|
||||||
|
try:
|
||||||
|
DownloadList.initialize_cache()
|
||||||
|
if not Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||||
|
self.logger.info('Song file hash pre-calculate is disabled.')
|
||||||
|
self.logger.info('Complete!')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(format_exc())
|
||||||
|
self.logger.warning('Initialization error!')
|
||||||
|
f = False
|
||||||
|
return f
|
||||||
|
|
||||||
|
def check_before_run(self) -> bool:
|
||||||
|
'''运行前检查,返回布尔值'''
|
||||||
|
MemoryDatabase() # 初始化内存数据库
|
||||||
|
return self.check_song_file() & self.check_update_database()
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData
|
from .config_manager import Config
|
||||||
from setting import Config
|
from .error import DataExist, InputError, ItemNotEnough, ItemUnavailable, NoData
|
||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
item_type = None
|
item_type = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.item_id = None
|
self.item_id = None
|
||||||
self.__amount = None
|
self.__amount = None
|
||||||
self.is_available = None
|
self.is_available = None
|
||||||
|
self.c = c
|
||||||
|
|
||||||
|
def __eq__(self, other: 'Item') -> bool:
|
||||||
|
return self.item_id == other.item_id and self.item_type == other.item_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
@@ -18,12 +22,13 @@ class Item:
|
|||||||
def amount(self, value: int):
|
def amount(self, value: int):
|
||||||
self.__amount = int(value)
|
self.__amount = int(value)
|
||||||
|
|
||||||
def to_dict(self, has_is_available: bool = False) -> dict:
|
def to_dict(self, has_is_available: bool = False, has_amount: bool = True) -> dict:
|
||||||
r = {
|
r = {
|
||||||
'id': self.item_id,
|
'id': self.item_id,
|
||||||
'amount': self.amount,
|
|
||||||
'type': self.item_type
|
'type': self.item_type
|
||||||
}
|
}
|
||||||
|
if has_amount:
|
||||||
|
r['amount'] = self.amount
|
||||||
if has_is_available:
|
if has_is_available:
|
||||||
r['is_available'] = self.is_available
|
r['is_available'] = self.is_available
|
||||||
return r
|
return r
|
||||||
@@ -32,6 +37,32 @@ class Item:
|
|||||||
# parameter: user - User类或子类的实例
|
# parameter: user - User类或子类的实例
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def select_exists(self):
|
||||||
|
self.c.execute('''select exists(select * from item where item_id=? and type=?)''',
|
||||||
|
(self.item_id, self.item_type))
|
||||||
|
return bool(self.c.fetchone()[0])
|
||||||
|
|
||||||
|
def insert(self, ignore: bool = False):
|
||||||
|
sql = '''insert into item values(?,?,?)''' if not ignore else '''insert or ignore into item values(?,?,?)'''
|
||||||
|
self.c.execute(sql, (self.item_id, self.item_type, self.is_available))
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
self.c.execute('''delete from item where item_id=? and type=?''',
|
||||||
|
(self.item_id, self.item_type))
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.c.execute('''update item set is_available=? where item_id=? and type=?''',
|
||||||
|
(self.is_available, self.item_id, self.item_type))
|
||||||
|
|
||||||
|
def select(self):
|
||||||
|
self.c.execute('''select is_available from item where item_id=? and type=?''',
|
||||||
|
(self.item_id, self.item_type))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{self.item_type}`: `{self.item_id}`', api_error_code=-121)
|
||||||
|
self.is_available = x[0]
|
||||||
|
|
||||||
|
|
||||||
class UserItem(Item):
|
class UserItem(Item):
|
||||||
|
|
||||||
@@ -40,9 +71,10 @@ class UserItem(Item):
|
|||||||
self.c = c
|
self.c = c
|
||||||
self.user = None
|
self.user = None
|
||||||
|
|
||||||
def select(self, user=None):
|
def select_user_item(self, user=None):
|
||||||
'''
|
'''
|
||||||
查询用户item\
|
查询用户item
|
||||||
|
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
if user is not None:
|
if user is not None:
|
||||||
@@ -71,7 +103,6 @@ class NormalItem(UserItem):
|
|||||||
if x[0] == 0:
|
if x[0] == 0:
|
||||||
self.is_available = False
|
self.is_available = False
|
||||||
raise ItemUnavailable('The item is unavailable.')
|
raise ItemUnavailable('The item is unavailable.')
|
||||||
else:
|
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
else:
|
else:
|
||||||
raise NoData('No item data.')
|
raise NoData('No item data.')
|
||||||
@@ -111,16 +142,29 @@ class PositiveItem(UserItem):
|
|||||||
class ItemCore(PositiveItem):
|
class ItemCore(PositiveItem):
|
||||||
item_type = 'core'
|
item_type = 'core'
|
||||||
|
|
||||||
def __init__(self, c, core=None, reverse=False) -> None:
|
def __init__(self, c, core_type: str = '', amount: int = 0) -> None:
|
||||||
super().__init__(c)
|
super().__init__(c)
|
||||||
self.is_available = True
|
self.is_available = True
|
||||||
if core:
|
self.item_id = core_type
|
||||||
self.item_id = core.item_id
|
self.amount = amount
|
||||||
self.amount = - core.amount if reverse else core.amount
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.item_id + '_' + str(self.amount)
|
return self.item_id + '_' + str(self.amount)
|
||||||
|
|
||||||
|
def to_dict(self, has_is_available: bool = False, has_amount: bool = True, character_format: bool = False) -> dict:
|
||||||
|
if character_format:
|
||||||
|
# 搭档的core是特殊格式的
|
||||||
|
return {'core_type': self.item_id, 'amount': self.amount}
|
||||||
|
return super().to_dict(has_is_available=has_is_available, has_amount=has_amount)
|
||||||
|
|
||||||
|
def user_claim_item(self, user, reverse: bool = False) -> None:
|
||||||
|
# 骚操作,将amount变为负数后使用再变回来
|
||||||
|
if reverse:
|
||||||
|
self.amount = -self.amount
|
||||||
|
super().user_claim_item(user)
|
||||||
|
if reverse:
|
||||||
|
self.amount = -self.amount
|
||||||
|
|
||||||
|
|
||||||
class ItemCharacter(UserItem):
|
class ItemCharacter(UserItem):
|
||||||
item_type = 'character'
|
item_type = 'character'
|
||||||
@@ -151,7 +195,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):
|
||||||
@@ -226,16 +270,10 @@ class CourseBanner(NormalItem):
|
|||||||
class Single(NormalItem):
|
class Single(NormalItem):
|
||||||
item_type = 'single'
|
item_type = 'single'
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
|
||||||
super().__init__(c)
|
|
||||||
|
|
||||||
|
|
||||||
class Pack(NormalItem):
|
class Pack(NormalItem):
|
||||||
item_type = 'pack'
|
item_type = 'pack'
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
|
||||||
super().__init__(c)
|
|
||||||
|
|
||||||
|
|
||||||
class ProgBoost(UserItem):
|
class ProgBoost(UserItem):
|
||||||
item_type = 'prog_boost_300'
|
item_type = 'prog_boost_300'
|
||||||
@@ -245,7 +283,8 @@ class ProgBoost(UserItem):
|
|||||||
|
|
||||||
def user_claim_item(self, user):
|
def user_claim_item(self, user):
|
||||||
'''
|
'''
|
||||||
世界模式prog_boost\
|
世界模式prog_boost
|
||||||
|
|
||||||
parameters: `user` - `UserOnline`类或子类的实例
|
parameters: `user` - `UserOnline`类或子类的实例
|
||||||
'''
|
'''
|
||||||
user.update_user_one_column('prog_boost', 300)
|
user.update_user_one_column('prog_boost', 300)
|
||||||
@@ -259,7 +298,7 @@ class Stamina6(UserItem):
|
|||||||
|
|
||||||
def user_claim_item(self, user):
|
def user_claim_item(self, user):
|
||||||
'''
|
'''
|
||||||
世界模式记忆源点或残片买体力+6\
|
世界模式记忆源点或残片买体力+6
|
||||||
顺手清一下世界模式过载状态
|
顺手清一下世界模式过载状态
|
||||||
'''
|
'''
|
||||||
user.select_user_about_stamina()
|
user.select_user_about_stamina()
|
||||||
@@ -302,7 +341,8 @@ class ItemFactory:
|
|||||||
elif item_type == 'course_banner':
|
elif item_type == 'course_banner':
|
||||||
return CourseBanner(self.c)
|
return CourseBanner(self.c)
|
||||||
else:
|
else:
|
||||||
raise InputError('The item type `%s` is wrong.' % item_type)
|
raise InputError(
|
||||||
|
f'The item type `{item_type}` is invalid.', api_error_code=-120)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d: dict, c=None):
|
def from_dict(cls, d: dict, c=None):
|
||||||
@@ -354,8 +394,8 @@ class ItemFactory:
|
|||||||
|
|
||||||
class UserItemList:
|
class UserItemList:
|
||||||
'''
|
'''
|
||||||
用户的item列表\
|
用户的item列表
|
||||||
注意只能查在user_item里面的,character不行\
|
注意只能查在user_item里面的,character不行
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -388,3 +428,63 @@ class UserItemList:
|
|||||||
self.items.append(ItemFactory.from_dict(
|
self.items.append(ItemFactory.from_dict(
|
||||||
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
|
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionItemMixin:
|
||||||
|
'''
|
||||||
|
批量修改一些集合中的items
|
||||||
|
'''
|
||||||
|
collection_item_const = {
|
||||||
|
'name': 'collection',
|
||||||
|
'table_name': 'collection_item',
|
||||||
|
'table_primary_key': 'collection_id',
|
||||||
|
'id_name': 'collection_id',
|
||||||
|
'items_name': 'items'
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_items(self, items: 'list[Item]') -> None:
|
||||||
|
collection_id: 'str' = getattr(
|
||||||
|
self, self.collection_item_const['id_name'])
|
||||||
|
collection_items: 'list[Item]' = getattr(
|
||||||
|
self, self.collection_item_const['items_name'])
|
||||||
|
|
||||||
|
for i in items:
|
||||||
|
if not i.select_exists():
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
|
||||||
|
if i in collection_items:
|
||||||
|
raise DataExist(
|
||||||
|
f'Item `{i.item_type}`: `{i.item_id}` already exists in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-123)
|
||||||
|
self.c.executemany(f'''insert into {self.collection_item_const["table_name"]} values (?, ?, ?, ?)''', [
|
||||||
|
(collection_id, i.item_id, i.item_type, i.amount) for i in items])
|
||||||
|
collection_items.extend(items)
|
||||||
|
|
||||||
|
def remove_items(self, items: 'list[Item]') -> None:
|
||||||
|
collection_id: 'str' = getattr(
|
||||||
|
self, self.collection_item_const['id_name'])
|
||||||
|
collection_items: 'list[Item]' = getattr(
|
||||||
|
self, self.collection_item_const['items_name'])
|
||||||
|
|
||||||
|
for i in items:
|
||||||
|
if i not in collection_items:
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
|
||||||
|
self.c.executemany(f'''delete from {self.collection_item_const["table_name"]} where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
|
||||||
|
(collection_id, i.item_id, i.item_type) for i in items])
|
||||||
|
for i in items:
|
||||||
|
collection_items.remove(i)
|
||||||
|
|
||||||
|
def update_items(self, items: 'list[Item]') -> None:
|
||||||
|
collection_id: 'str' = getattr(
|
||||||
|
self, self.collection_item_const['id_name'])
|
||||||
|
collection_items: 'list[Item]' = getattr(
|
||||||
|
self, self.collection_item_const['items_name'])
|
||||||
|
|
||||||
|
for i in items:
|
||||||
|
if i not in collection_items:
|
||||||
|
raise NoData(
|
||||||
|
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
|
||||||
|
self.c.executemany(f'''update {self.collection_item_const["table_name"]} set amount=? where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
|
||||||
|
(i.amount, collection_id, i.item_id, i.item_type) for i in items])
|
||||||
|
for i in items:
|
||||||
|
collection_items[collection_items.index(i)].amount = i.amount
|
||||||
|
|||||||
31
latest version/core/limiter.py
Normal file
31
latest version/core/limiter.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from limits import parse_many, strategies
|
||||||
|
from limits.storage import storage_from_string
|
||||||
|
|
||||||
|
|
||||||
|
class ArcLimiter:
|
||||||
|
storage = storage_from_string("memory://")
|
||||||
|
strategy = strategies.FixedWindowRateLimiter(storage)
|
||||||
|
|
||||||
|
def __init__(self, limit_str: str = None, namespace: str = None):
|
||||||
|
self._limits: list = None
|
||||||
|
self.limits = limit_str
|
||||||
|
self.namespace = namespace
|
||||||
|
|
||||||
|
@property
|
||||||
|
def limits(self) -> list:
|
||||||
|
return self._limits
|
||||||
|
|
||||||
|
@limits.setter
|
||||||
|
def limits(self, value: str):
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
self._limits = parse_many(value)
|
||||||
|
|
||||||
|
def hit(self, key: str, cost: int = 1) -> bool:
|
||||||
|
flag = True
|
||||||
|
for limit in self.limits:
|
||||||
|
flag &= self.strategy.hit(limit, self.namespace, key, cost)
|
||||||
|
return flag
|
||||||
|
|
||||||
|
def test(self, key: str) -> bool:
|
||||||
|
return all(self.strategy.test(limit, self.namespace, key) for limit in self.limits)
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import socket
|
import socket
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
from json import dumps, loads
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -86,53 +88,77 @@ class Room:
|
|||||||
|
|
||||||
|
|
||||||
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')
|
||||||
except socket.timeout:
|
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(12)
|
||||||
|
ciphertext = sock.recv(cipher_len)
|
||||||
|
received = aes_gcm_128_decrypt(
|
||||||
|
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||||
|
except socket.timeout as e:
|
||||||
raise Timeout(
|
raise Timeout(
|
||||||
'Timeout when waiting for data from link play server.', status=400)
|
'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[:12] + 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:
|
||||||
raise ArcError('Link Play error.',
|
raise ArcError(f'Link Play error code: {code}', code, status=400)
|
||||||
int(self.data_recv[0]), 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_one_column('name')
|
||||||
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
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:
|
||||||
'''加入房间'''
|
'''加入房间'''
|
||||||
@@ -142,23 +168,49 @@ class RemoteMultiPlayer:
|
|||||||
self.room = room
|
self.room = room
|
||||||
|
|
||||||
self.user.select_user_one_column('name')
|
self.user.select_user_one_column('name')
|
||||||
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])
|
}
|
||||||
self.user.player_id = int(self.data_recv[5])
|
})
|
||||||
|
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.data_swap({
|
||||||
|
'endpoint': 'update_room',
|
||||||
|
'data': {
|
||||||
|
'token': self.user.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
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']
|
||||||
|
|||||||
247
latest version/core/operation.py
Normal file
247
latest version/core/operation.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
from .download import DownloadList
|
||||||
|
from .error import NoData
|
||||||
|
from .save import SaveData
|
||||||
|
from .score import Score
|
||||||
|
from .sql import Connect, Sql
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOperation:
|
||||||
|
_name: str = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs) -> None:
|
||||||
|
return self.run(*args, **kwargs)
|
||||||
|
|
||||||
|
def set_params(self, *args, **kwargs) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshAllScoreRating(BaseOperation):
|
||||||
|
'''
|
||||||
|
刷新所有成绩的评分
|
||||||
|
'''
|
||||||
|
_name = 'refresh_all_score_rating'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# 追求效率,不用Song类,尽量不用对象
|
||||||
|
# 但其实还是很慢
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute(
|
||||||
|
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||||
|
x = c.fetchall()
|
||||||
|
|
||||||
|
songs = [i[0] for i in x]
|
||||||
|
c.execute(
|
||||||
|
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
||||||
|
|
||||||
|
for i in x:
|
||||||
|
for j in range(0, 4):
|
||||||
|
defnum = -10 # 没在库里的全部当做定数-10
|
||||||
|
if i[j+1] is not None and i[j+1] > 0:
|
||||||
|
defnum = float(i[j+1]) / 10
|
||||||
|
|
||||||
|
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
|
||||||
|
'a': i[0], 'b': j})
|
||||||
|
y = c.fetchall()
|
||||||
|
values = []
|
||||||
|
where_values = []
|
||||||
|
for k in y:
|
||||||
|
ptt = Score.calculate_rating(defnum, k[1])
|
||||||
|
ptt = max(ptt, 0)
|
||||||
|
values.append((ptt,))
|
||||||
|
where_values.append((k[0], i[0], j))
|
||||||
|
if values:
|
||||||
|
Sql(c).update_many('best_score', ['rating'], values, [
|
||||||
|
'user_id', 'song_id', 'difficulty'], where_values)
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshSongFileCache(BaseOperation):
|
||||||
|
'''
|
||||||
|
刷新歌曲文件缓存,包括文件hash缓存重建、文件目录重遍历、songlist重解析
|
||||||
|
注意在设置里预先计算关闭的情况下,文件hash不会计算
|
||||||
|
'''
|
||||||
|
_name = 'refresh_song_file_cache'
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
DownloadList.clear_all_cache()
|
||||||
|
DownloadList.initialize_cache()
|
||||||
|
|
||||||
|
|
||||||
|
class SaveUpdateScore(BaseOperation):
|
||||||
|
'''
|
||||||
|
云存档更新成绩,是覆盖式更新
|
||||||
|
提供user参数时,只更新该用户的成绩,否则更新所有用户的成绩
|
||||||
|
'''
|
||||||
|
_name = 'save_update_score'
|
||||||
|
|
||||||
|
def __init__(self, user=None):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||||
|
if user_id is not None:
|
||||||
|
self.user = User()
|
||||||
|
self.user.user_id = int(user_id)
|
||||||
|
|
||||||
|
def run(self, user=None):
|
||||||
|
'''
|
||||||
|
parameter:
|
||||||
|
`user` - `User`类或子类的实例
|
||||||
|
'''
|
||||||
|
if user is not None:
|
||||||
|
self.user = user
|
||||||
|
if self.user is not None and self.user.user_id is not None:
|
||||||
|
self._one_user_update()
|
||||||
|
else:
|
||||||
|
self._all_update()
|
||||||
|
|
||||||
|
def _one_user_update(self):
|
||||||
|
with Connect() as c:
|
||||||
|
save = SaveData(c)
|
||||||
|
save.select_scores(self.user)
|
||||||
|
|
||||||
|
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
|
||||||
|
for i in save.clearlamps_data}
|
||||||
|
|
||||||
|
song_id_1 = [i['song_id'] for i in save.scores_data]
|
||||||
|
song_id_2 = [i['song_id'] for i in save.clearlamps_data]
|
||||||
|
song_id = list(set(song_id_1 + song_id_2))
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
|
||||||
|
x = c.fetchall()
|
||||||
|
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||||
|
for i in x} # chart const * 10
|
||||||
|
|
||||||
|
new_scores = []
|
||||||
|
for i in save.scores_data:
|
||||||
|
rating = 0
|
||||||
|
if i['song_id'] in song_chart_const:
|
||||||
|
rating = Score.calculate_rating(
|
||||||
|
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
||||||
|
rating = max(rating, 0)
|
||||||
|
|
||||||
|
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||||
|
if y in clear_state:
|
||||||
|
clear_type = clear_state[y]
|
||||||
|
else:
|
||||||
|
clear_type = 0
|
||||||
|
|
||||||
|
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||||
|
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
||||||
|
|
||||||
|
c.executemany(
|
||||||
|
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||||
|
|
||||||
|
def _all_update(self):
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute(
|
||||||
|
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||||
|
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
|
||||||
|
for i in c.fetchall()} # chart const * 10
|
||||||
|
c.execute('''select user_id from user_save''')
|
||||||
|
for y in c.fetchall():
|
||||||
|
user = User()
|
||||||
|
user.user_id = y[0]
|
||||||
|
save = SaveData(c)
|
||||||
|
save.select_scores(user)
|
||||||
|
|
||||||
|
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
|
||||||
|
for i in save.clearlamps_data}
|
||||||
|
|
||||||
|
new_scores = []
|
||||||
|
for i in save.scores_data:
|
||||||
|
rating = 0
|
||||||
|
if i['song_id'] in song_chart_const:
|
||||||
|
rating = Score.calculate_rating(
|
||||||
|
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
|
||||||
|
rating = max(rating, 0)
|
||||||
|
|
||||||
|
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||||
|
if y in clear_state:
|
||||||
|
clear_type = clear_state[y]
|
||||||
|
else:
|
||||||
|
clear_type = 0
|
||||||
|
|
||||||
|
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
|
||||||
|
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
|
||||||
|
|
||||||
|
c.executemany(
|
||||||
|
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockUserItem(BaseOperation):
|
||||||
|
'''
|
||||||
|
全解锁/锁定用户物品
|
||||||
|
提供user参数时,只更新该用户的,否则更新所有用户的
|
||||||
|
'''
|
||||||
|
_name = 'unlock_user_item'
|
||||||
|
|
||||||
|
ALLOW_TYPES = ['single', 'pack', 'world_song',
|
||||||
|
'course_banner', 'world_unlock']
|
||||||
|
|
||||||
|
def __init__(self, user=None, method: str = 'unlock', item_types: list = ['single', 'pack']):
|
||||||
|
self.user = user
|
||||||
|
self.set_params(method=method, item_types=item_types)
|
||||||
|
|
||||||
|
def set_params(self, user_id: int = None, method: str = 'unlock', item_types: list = ['single', 'pack'], *args, **kwargs):
|
||||||
|
if user_id is not None:
|
||||||
|
self.user = User()
|
||||||
|
self.user.user_id = int(user_id)
|
||||||
|
if method in ['unlock', 'lock']:
|
||||||
|
self.method = method
|
||||||
|
if isinstance(item_types, list) and all(i in self.ALLOW_TYPES for i in item_types):
|
||||||
|
self.item_types = item_types
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.user is not None and self.user.user_id is not None:
|
||||||
|
if self.method == 'unlock':
|
||||||
|
self._one_user_insert()
|
||||||
|
else:
|
||||||
|
self._one_user_delete()
|
||||||
|
else:
|
||||||
|
if self.method == 'unlock':
|
||||||
|
self._all_insert()
|
||||||
|
else:
|
||||||
|
self._all_delete()
|
||||||
|
|
||||||
|
def _one_user_insert(self):
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute(
|
||||||
|
'''select exists(select * from user where user_id = ?)''', (self.user.user_id,))
|
||||||
|
if not c.fetchone()[0]:
|
||||||
|
raise NoData(
|
||||||
|
f'No such user: `{self.user.user_id}`', api_error_code=-110)
|
||||||
|
c.execute(
|
||||||
|
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||||
|
sql_list = [(self.user.user_id, i[0], i[1])
|
||||||
|
for i in c.fetchall()]
|
||||||
|
c.executemany(
|
||||||
|
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
|
||||||
|
|
||||||
|
def _all_insert(self):
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute('''select user_id from user''')
|
||||||
|
x = c.fetchall()
|
||||||
|
c.execute(
|
||||||
|
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||||
|
y = c.fetchall()
|
||||||
|
sql_list = [(i[0], j[0], j[1])
|
||||||
|
for i in x for j in y]
|
||||||
|
c.executemany(
|
||||||
|
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
|
||||||
|
|
||||||
|
def _one_user_delete(self):
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute(
|
||||||
|
f'''delete from user_item where user_id = ? and type in ({','.join(['?'] * len(self.item_types))})''', (self.user.user_id, *self.item_types))
|
||||||
|
|
||||||
|
def _all_delete(self):
|
||||||
|
with Connect() as c:
|
||||||
|
c.execute(
|
||||||
|
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from core.item import ItemFactory
|
|
||||||
|
|
||||||
from .error import ArcError, NoData
|
from .error import ArcError, NoData
|
||||||
|
from .item import CollectionItemMixin, ItemFactory
|
||||||
|
|
||||||
|
|
||||||
class Present:
|
class Present(CollectionItemMixin):
|
||||||
|
collection_item_const = {
|
||||||
|
'name': 'present',
|
||||||
|
'table_name': 'present_item',
|
||||||
|
'table_primary_key': 'present_id',
|
||||||
|
'id_name': 'present_id',
|
||||||
|
'items_name': 'items'
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, c=None) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
self.present_id: str = None
|
self.present_id: str = None
|
||||||
@@ -18,15 +25,39 @@ class Present:
|
|||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
return self.expire_ts < int(time() * 1000)
|
return self.expire_ts < int(time() * 1000)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self, has_items: bool = True) -> dict:
|
||||||
return {
|
r = {
|
||||||
'present_id': self.present_id,
|
'present_id': self.present_id,
|
||||||
'expire_ts': self.expire_ts,
|
'expire_ts': self.expire_ts,
|
||||||
'description': self.description,
|
'description': self.description
|
||||||
'items': [x.to_dict() for x in self.items]
|
|
||||||
}
|
}
|
||||||
|
if has_items:
|
||||||
|
r['items'] = [x.to_dict() for x in self.items]
|
||||||
|
return r
|
||||||
|
|
||||||
def select(self, present_id: str = None) -> None:
|
def from_dict(self, d: dict) -> 'Present':
|
||||||
|
self.present_id = d['present_id']
|
||||||
|
self.expire_ts = int(d['expire_ts'])
|
||||||
|
self.description = d.get('description', '')
|
||||||
|
self.items = [ItemFactory.from_dict(
|
||||||
|
i, c=self.c) for i in d.get('items', [])]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def from_list(self, l: list) -> 'Present':
|
||||||
|
self.present_id = l[0]
|
||||||
|
self.expire_ts = int(l[1]) if l[1] else 0
|
||||||
|
self.description = l[2] if l[2] else ''
|
||||||
|
return self
|
||||||
|
|
||||||
|
def select_exists(self) -> bool:
|
||||||
|
'''
|
||||||
|
查询present是否存在
|
||||||
|
'''
|
||||||
|
self.c.execute(
|
||||||
|
'''select exists(select * from present where present_id=?)''', (self.present_id,))
|
||||||
|
return bool(self.c.fetchone()[0])
|
||||||
|
|
||||||
|
def select(self, present_id: str = None) -> 'Present':
|
||||||
'''
|
'''
|
||||||
用present_id查询信息
|
用present_id查询信息
|
||||||
'''
|
'''
|
||||||
@@ -39,8 +70,8 @@ class Present:
|
|||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The present `%s` does not exist.' % self.present_id)
|
raise NoData('The present `%s` does not exist.' % self.present_id)
|
||||||
|
|
||||||
self.expire_ts = x[1] if x[1] else 0
|
self.from_list(x)
|
||||||
self.description = x[2] if x[2] else ''
|
return self
|
||||||
|
|
||||||
def select_items(self) -> None:
|
def select_items(self) -> None:
|
||||||
'''
|
'''
|
||||||
@@ -48,21 +79,49 @@ class Present:
|
|||||||
'''
|
'''
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select * from present_item where present_id=:a''', {'a': self.present_id})
|
'''select * from present_item where present_id=:a''', {'a': self.present_id})
|
||||||
x = self.c.fetchall()
|
|
||||||
if not x:
|
|
||||||
raise NoData('The present `%s` does not have any items.' %
|
|
||||||
self.present_id)
|
|
||||||
self.items = [ItemFactory.from_dict({
|
self.items = [ItemFactory.from_dict({
|
||||||
'item_id': i[1],
|
'item_id': i[1],
|
||||||
'type': i[2],
|
'type': i[2],
|
||||||
'amount': i[3] if i[3] else 1
|
'amount': i[3] if i[3] else 1
|
||||||
}, self.c) for i in x]
|
}, self.c) for i in self.c.fetchall()]
|
||||||
|
|
||||||
|
def insert_items(self) -> None:
|
||||||
|
for i in self.items:
|
||||||
|
self.c.execute('''insert or ignore into item values(?,?,?)''',
|
||||||
|
(i.item_id, i.item_type, i.is_available))
|
||||||
|
|
||||||
|
self.c.execute('''insert or ignore into present_item values(?,?,?,?)''',
|
||||||
|
(self.present_id, i.item_id, i.item_type, i.amount))
|
||||||
|
|
||||||
|
def insert(self) -> None:
|
||||||
|
self.c.execute('''insert into present values(?,?,?)''',
|
||||||
|
(self.present_id, self.expire_ts, self.description))
|
||||||
|
|
||||||
|
def insert_all(self) -> None:
|
||||||
|
self.insert()
|
||||||
|
self.insert_items()
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from present where present_id=?''', (self.present_id,))
|
||||||
|
|
||||||
|
def delete_present_item(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from present_item where present_id=?''', (self.present_id,))
|
||||||
|
|
||||||
|
def delete_all(self) -> None:
|
||||||
|
self.delete_present_item()
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self.c.execute('''update present set expire_ts=?, description=? where present_id=?''',
|
||||||
|
(self.expire_ts, self.description, self.present_id))
|
||||||
|
|
||||||
|
|
||||||
class UserPresent(Present):
|
class UserPresent(Present):
|
||||||
'''
|
'''
|
||||||
用户登录奖励类\
|
用户登录奖励类
|
||||||
忽视了description的多语言\
|
忽视了description的多语言
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .error import NoData, TicketNotEnough
|
from .error import InputError, NoData, TicketNotEnough
|
||||||
from .item import ItemFactory
|
from .item import CollectionItemMixin, ItemFactory
|
||||||
|
|
||||||
|
|
||||||
class Purchase:
|
class Purchase(CollectionItemMixin):
|
||||||
'''
|
'''
|
||||||
购买类\
|
购买类
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
collection_item_const = {
|
||||||
|
'name': 'purchase',
|
||||||
|
'table_name': 'purchase_item',
|
||||||
|
'table_primary_key': 'purchase_name',
|
||||||
|
'id_name': 'purchase_name',
|
||||||
|
'items_name': 'items'
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, c=None, user=None):
|
def __init__(self, c=None, user=None):
|
||||||
self.c = c
|
self.c = c
|
||||||
@@ -22,6 +29,8 @@ class Purchase:
|
|||||||
|
|
||||||
self.items: list = []
|
self.items: list = []
|
||||||
|
|
||||||
|
# TODO: "discount_reason": "extend"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def price_displayed(self) -> int:
|
def price_displayed(self) -> int:
|
||||||
'''
|
'''
|
||||||
@@ -32,27 +41,87 @@ class Purchase:
|
|||||||
if self.discount_reason == 'anni5tix':
|
if self.discount_reason == 'anni5tix':
|
||||||
x = ItemFactory(self.c).get_item('anni5tix')
|
x = ItemFactory(self.c).get_item('anni5tix')
|
||||||
x.item_id = 'anni5tix'
|
x.item_id = 'anni5tix'
|
||||||
x.select(self.user)
|
x.select_user_item(self.user)
|
||||||
if x.amount >= 1:
|
if x.amount >= 1:
|
||||||
return 0
|
return 0
|
||||||
return self.price
|
return self.price
|
||||||
return self.orig_price
|
return self.orig_price
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self, has_items: bool = True, show_real_price: bool = True) -> dict:
|
||||||
|
if show_real_price:
|
||||||
price = self.price_displayed
|
price = self.price_displayed
|
||||||
r = {
|
r = {
|
||||||
'name': self.purchase_name,
|
'name': self.purchase_name,
|
||||||
'price': price,
|
'price': price if show_real_price else self.price,
|
||||||
'orig_price': self.orig_price,
|
'orig_price': self.orig_price,
|
||||||
'items': [x.to_dict(has_is_available=True) for x in self.items]
|
|
||||||
}
|
}
|
||||||
|
if has_items:
|
||||||
|
r['items'] = [x.to_dict(has_is_available=True) for x in self.items]
|
||||||
if self.discount_from > 0 and self.discount_to > 0:
|
if self.discount_from > 0 and self.discount_to > 0:
|
||||||
r['discount_from'] = self.discount_from
|
r['discount_from'] = self.discount_from
|
||||||
r['discount_to'] = self.discount_to
|
r['discount_to'] = self.discount_to
|
||||||
if self.discount_reason == 'anni5tix' and price == 0:
|
if not show_real_price or (self.discount_reason == 'anni5tix' and price == 0):
|
||||||
r['discount_reason'] = self.discount_reason
|
r['discount_reason'] = self.discount_reason
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def from_dict(self, d: dict) -> 'Purchase':
|
||||||
|
self.purchase_name = d.get('name') or d.get('purchase_name')
|
||||||
|
if not self.purchase_name:
|
||||||
|
raise InputError('purchase_name is required')
|
||||||
|
self.orig_price = int(d['orig_price'])
|
||||||
|
self.price = d.get('price', self.orig_price)
|
||||||
|
self.discount_from = d.get('discount_from', -1)
|
||||||
|
self.discount_to = d.get('discount_to', -1)
|
||||||
|
self.discount_reason = d.get('discount_reason', '')
|
||||||
|
for i in d.get('items', []):
|
||||||
|
self.items.append(ItemFactory.from_dict(i))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def from_list(self, l: list) -> 'Purchase':
|
||||||
|
self.purchase_name = l[0]
|
||||||
|
self.price = l[1]
|
||||||
|
self.orig_price = l[2]
|
||||||
|
self.discount_from = l[3] if l[3] else -1
|
||||||
|
self.discount_to = l[4] if l[4] else -1
|
||||||
|
self.discount_reason = l[5] if l[5] else ''
|
||||||
|
return self
|
||||||
|
|
||||||
|
def insert_all(self) -> None:
|
||||||
|
'''向数据库插入,包括item表和purchase_item表'''
|
||||||
|
self.insert()
|
||||||
|
self.insert_items()
|
||||||
|
|
||||||
|
def insert(self) -> None:
|
||||||
|
'''向数据库插入,不包括item表和purchase_item表'''
|
||||||
|
self.c.execute('''insert into purchase values(?,?,?,?,?,?)''',
|
||||||
|
(self.purchase_name, self.price, self.orig_price, self.discount_from, self.discount_to, self.discount_reason))
|
||||||
|
|
||||||
|
# def insert_only_purchase_item(self) -> None:
|
||||||
|
# '''向数据库插入purchase_item表'''
|
||||||
|
# for i in self.items:
|
||||||
|
# self.c.execute('''insert into purchase_item values(?,?,?,?)''',
|
||||||
|
# (self.purchase_name, i.item_id, i.item_type, i.amount))
|
||||||
|
|
||||||
|
def insert_items(self) -> None:
|
||||||
|
'''向数据库插入物品,注意已存在的物品不会变更'''
|
||||||
|
for i in self.items:
|
||||||
|
self.c.execute('''insert or ignore into item values(?,?,?)''',
|
||||||
|
(i.item_id, i.item_type, i.is_available))
|
||||||
|
|
||||||
|
self.c.execute('''insert or ignore into purchase_item values(?,?,?,?)''',
|
||||||
|
(self.purchase_name, i.item_id, i.item_type, i.amount))
|
||||||
|
|
||||||
|
def select_exists(self, purchase_name: str = None) -> bool:
|
||||||
|
'''
|
||||||
|
用purchase_name查询存在性
|
||||||
|
'''
|
||||||
|
if purchase_name:
|
||||||
|
self.purchase_name = purchase_name
|
||||||
|
self.c.execute(
|
||||||
|
'''select exists(select * from purchase where purchase_name=?)''', (self.purchase_name,))
|
||||||
|
return self.c.fetchone() == (1,)
|
||||||
|
|
||||||
def select(self, purchase_name: str = None) -> 'Purchase':
|
def select(self, purchase_name: str = None) -> 'Purchase':
|
||||||
'''
|
'''
|
||||||
用purchase_name查询信息
|
用purchase_name查询信息
|
||||||
@@ -61,11 +130,11 @@ class Purchase:
|
|||||||
self.purchase_name = purchase_name
|
self.purchase_name = purchase_name
|
||||||
|
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select * from purchase where purchase_name=:name''', {'name': purchase_name})
|
'''select * from purchase where purchase_name=:name''', {'name': self.purchase_name})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if not x:
|
if not x:
|
||||||
raise NoData('The purchase `%s` does not exist.' %
|
raise NoData(
|
||||||
purchase_name, 501)
|
f'Purchase `{self.purchase_name}` does not exist.', 501)
|
||||||
|
|
||||||
self.price = x[1]
|
self.price = x[1]
|
||||||
self.orig_price = x[2]
|
self.orig_price = x[2]
|
||||||
@@ -80,9 +149,9 @@ class Purchase:
|
|||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
|
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
|
||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
if not x:
|
# if not x:
|
||||||
raise NoData('The items of the purchase `%s` does not exist.' %
|
# raise NoData(
|
||||||
self.purchase_name, 501)
|
# f'The items of the purchase `{self.purchase_name}` does not exist.', 501)
|
||||||
|
|
||||||
self.items = []
|
self.items = []
|
||||||
t = None
|
t = None
|
||||||
@@ -130,10 +199,31 @@ class Purchase:
|
|||||||
for i in self.items:
|
for i in self.items:
|
||||||
i.user_claim_item(self.user)
|
i.user_claim_item(self.user)
|
||||||
|
|
||||||
|
def delete_purchase_item(self) -> None:
|
||||||
|
'''删除purchase_item表'''
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from purchase_item where purchase_name=?''', (self.purchase_name, ))
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
'''删除purchase表'''
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from purchase where purchase_name=?''', (self.purchase_name, ))
|
||||||
|
|
||||||
|
def delete_all(self) -> None:
|
||||||
|
'''删除purchase表和purchase_item表'''
|
||||||
|
self.delete_purchase_item()
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
'''更新purchase表'''
|
||||||
|
self.c.execute('''update purchase set price=:a, orig_price=:b, discount_from=:c, discount_to=:d, discount_reason=:e where purchase_name=:f''', {
|
||||||
|
'a': self.price, 'b': self.orig_price, 'c': self.discount_from, 'd': self.discount_to, 'e': self.discount_reason, 'f': self.purchase_name})
|
||||||
|
|
||||||
|
|
||||||
class PurchaseList:
|
class PurchaseList:
|
||||||
'''
|
'''
|
||||||
购买列表类\
|
购买列表类
|
||||||
|
|
||||||
property: `user` - `User`类或子类的实例
|
property: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .score import UserScore
|
from .score import UserScore
|
||||||
from .song import Chart
|
from .song import Chart
|
||||||
|
from .sql import Query, Sql
|
||||||
from .user import UserInfo
|
from .user import UserInfo
|
||||||
|
|
||||||
|
|
||||||
class RankList:
|
class RankList:
|
||||||
'''
|
'''
|
||||||
排行榜类\
|
排行榜类
|
||||||
默认limit=20,limit<0认为是all\
|
默认limit=20,limit<0认为是all
|
||||||
|
|
||||||
property: `user` - `User`类或者子类的实例
|
property: `user` - `User`类或者子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -26,24 +28,29 @@ class RankList:
|
|||||||
得到top分数表
|
得到top分数表
|
||||||
'''
|
'''
|
||||||
if self.limit >= 0:
|
if self.limit >= 0:
|
||||||
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
||||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
|
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
|
||||||
else:
|
else:
|
||||||
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', {
|
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', {
|
||||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
|
'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
|
||||||
|
|
||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
if not x:
|
if not x:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||||
|
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||||
|
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||||
|
|
||||||
rank = 0
|
rank = 0
|
||||||
self.list = []
|
self.list = []
|
||||||
for i in x:
|
for i in x:
|
||||||
rank += 1
|
rank += 1
|
||||||
y = UserScore(self.c, UserInfo(self.c, i[0]))
|
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||||
y.song = self.song
|
y.song = self.song
|
||||||
y.select_score()
|
|
||||||
y.rank = rank
|
y.rank = rank
|
||||||
|
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||||
|
|
||||||
self.list.append(y)
|
self.list.append(y)
|
||||||
|
|
||||||
def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None:
|
def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None:
|
||||||
@@ -54,26 +61,33 @@ class RankList:
|
|||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.c.execute('''select user_id from best_score where user_id in (select :user_id union select user_id_other from friend where user_id_me = :user_id) and song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
user_ids = [self.user.user_id] + [x[0] for x in self.user.friend_ids]
|
||||||
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
|
|
||||||
|
self.c.execute(f'''select * from best_score where user_id in ({','.join(['?'] * len(user_ids))}) and song_id = ? and difficulty = ? order by score DESC, time_played DESC limit ?''', user_ids + [
|
||||||
|
self.song.song_id, self.song.difficulty, self.limit])
|
||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
if not x:
|
if not x:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||||
|
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||||
|
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||||
rank = 0
|
rank = 0
|
||||||
self.list = []
|
self.list = []
|
||||||
for i in x:
|
for i in x:
|
||||||
rank += 1
|
rank += 1
|
||||||
y = UserScore(self.c, UserInfo(self.c, i[0]))
|
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||||
y.song = self.song
|
y.song = self.song
|
||||||
y.select_score()
|
|
||||||
y.rank = rank
|
y.rank = rank
|
||||||
|
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||||
|
|
||||||
self.list.append(y)
|
self.list.append(y)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION):
|
def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION):
|
||||||
'''
|
'''
|
||||||
计算我的排名中的查询参数\
|
计算我的排名中的查询参数
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
`sql_limit`: int - 查询limit参数
|
`sql_limit`: int - 查询limit参数
|
||||||
`sql_offset`: int - 查询offset参数
|
`sql_offset`: int - 查询offset参数
|
||||||
@@ -91,7 +105,7 @@ class RankList:
|
|||||||
need_myself = True
|
need_myself = True
|
||||||
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
|
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
|
||||||
sql_offset = amount - all_limit
|
sql_offset = amount - all_limit
|
||||||
elif my_rank >= max_local_position and my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
|
elif max_local_position <= my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
|
||||||
sql_offset = my_rank - max_local_position
|
sql_offset = my_rank - max_local_position
|
||||||
else: # 我已经忘了这是什么了
|
else: # 我已经忘了这是什么了
|
||||||
sql_offset = max_global_position - all_limit
|
sql_offset = max_global_position - all_limit
|
||||||
@@ -118,19 +132,25 @@ class RankList:
|
|||||||
|
|
||||||
sql_limit, sql_offset, need_myself = self.get_my_rank_parameter(
|
sql_limit, sql_offset, need_myself = self.get_my_rank_parameter(
|
||||||
my_rank, int(self.c.fetchone()[0]), self.limit)
|
my_rank, int(self.c.fetchone()[0]), self.limit)
|
||||||
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': sql_limit, 'offset': sql_offset})
|
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': sql_limit, 'offset': sql_offset})
|
||||||
x = self.c.fetchall()
|
x = self.c.fetchall()
|
||||||
|
|
||||||
if x:
|
if x:
|
||||||
|
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||||
|
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||||
|
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||||
rank = sql_offset if sql_offset > 0 else 0
|
rank = sql_offset if sql_offset > 0 else 0
|
||||||
self.list = []
|
self.list = []
|
||||||
for i in x:
|
for i in x:
|
||||||
rank += 1
|
rank += 1
|
||||||
y = UserScore(self.c, UserInfo(self.c, i[0]))
|
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||||
y.song = self.song
|
y.song = self.song
|
||||||
y.select_score()
|
|
||||||
y.rank = rank
|
y.rank = rank
|
||||||
|
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||||
|
|
||||||
self.list.append(y)
|
self.list.append(y)
|
||||||
|
|
||||||
if need_myself:
|
if need_myself:
|
||||||
y = UserScore(self.c, UserInfo(self.c, self.user.user_id))
|
y = UserScore(self.c, UserInfo(self.c, self.user.user_id))
|
||||||
y.song = self.song
|
y.song = self.song
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
from .error import NoData, RedeemUnavailable
|
from .error import NoData, RedeemUnavailable
|
||||||
from .item import ItemFactory
|
from .item import CollectionItemMixin, ItemFactory
|
||||||
|
|
||||||
|
|
||||||
class Redeem:
|
class Redeem(CollectionItemMixin):
|
||||||
|
collection_item_const = {
|
||||||
|
'name': 'redeem',
|
||||||
|
'table_name': 'redeem_item',
|
||||||
|
'table_primary_key': 'code',
|
||||||
|
'id_name': 'code',
|
||||||
|
'items_name': 'items'
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, c=None) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
self.code: str = None
|
self.code: str = None
|
||||||
@@ -11,34 +19,87 @@ class Redeem:
|
|||||||
self.items: list = []
|
self.items: list = []
|
||||||
self.fragment: int = None
|
self.fragment: int = None
|
||||||
|
|
||||||
def select(self, code: str = None) -> None:
|
def to_dict(self, has_items: bool = True) -> dict:
|
||||||
|
r = {
|
||||||
|
'code': self.code,
|
||||||
|
'type': self.redeem_type
|
||||||
|
}
|
||||||
|
if has_items:
|
||||||
|
r['items'] = [x.to_dict() for x in self.items]
|
||||||
|
return r
|
||||||
|
|
||||||
|
def from_dict(self, d: dict) -> 'Redeem':
|
||||||
|
self.code = str(d['code'])
|
||||||
|
self.redeem_type = int(d.get('type') or d.get('redeem_type', 0))
|
||||||
|
self.items = [ItemFactory.from_dict(
|
||||||
|
i, c=self.c) for i in d.get('items', [])]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def from_list(self, l: list) -> 'Redeem':
|
||||||
|
self.code = l[0]
|
||||||
|
self.redeem_type = l[1]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def select_exists(self) -> bool:
|
||||||
|
self.c.execute(
|
||||||
|
'''select exists(select * from redeem where code=?)''', (self.code,))
|
||||||
|
return bool(self.c.fetchone()[0])
|
||||||
|
|
||||||
|
def select(self, code: str = None) -> 'Redeem':
|
||||||
if code:
|
if code:
|
||||||
self.code = code
|
self.code = code
|
||||||
self.c.execute('''select * from redeem where code=:a''',
|
self.c.execute('''select * from redeem where code=:a''',
|
||||||
{'a': self.code})
|
{'a': self.code})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The redeem `%s` does not exist.' % self.code, 504)
|
raise NoData(f'The redeem `{self.code}` does not exist.', 504)
|
||||||
|
|
||||||
self.redeem_type = x[1]
|
self.redeem_type = x[1]
|
||||||
|
return self
|
||||||
|
|
||||||
def select_items(self) -> None:
|
def select_items(self) -> None:
|
||||||
self.c.execute('''select * from redeem_item where code=:a''',
|
self.c.execute('''select * from redeem_item where code=:a''',
|
||||||
{'a': self.code})
|
{'a': self.code})
|
||||||
x = self.c.fetchall()
|
|
||||||
if not x:
|
|
||||||
raise NoData(
|
|
||||||
'The redeem `%s` does not have any items.' % self.code)
|
|
||||||
self.items = [ItemFactory.from_dict({
|
self.items = [ItemFactory.from_dict({
|
||||||
'item_id': i[1],
|
'item_id': i[1],
|
||||||
'type': i[2],
|
'type': i[2],
|
||||||
'amount': i[3] if i[3] else 1
|
'amount': i[3] if i[3] else 1
|
||||||
}, self.c) for i in x]
|
}, self.c) for i in self.c.fetchall()]
|
||||||
|
|
||||||
|
def insert(self) -> None:
|
||||||
|
self.c.execute('''insert into redeem values(?,?)''',
|
||||||
|
(self.code, self.redeem_type))
|
||||||
|
|
||||||
|
def insert_items(self) -> None:
|
||||||
|
for i in self.items:
|
||||||
|
i.insert(ignore=True)
|
||||||
|
self.c.execute('''insert into redeem_item values(?,?,?,?)''', (
|
||||||
|
self.code, i.item_id, i.item_type, i.amount))
|
||||||
|
|
||||||
|
def insert_all(self) -> None:
|
||||||
|
self.insert()
|
||||||
|
self.insert_items()
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.c.execute('''delete from redeem where code=?''', (self.code,))
|
||||||
|
|
||||||
|
def delete_redeem_item(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from redeem_item where code=?''', (self.code,))
|
||||||
|
|
||||||
|
def delete_all(self) -> None:
|
||||||
|
self.delete_redeem_item()
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self.c.execute('''update redeem set type=? where code=?''',
|
||||||
|
(self.redeem_type, self.code))
|
||||||
|
|
||||||
|
|
||||||
class UserRedeem(Redeem):
|
class UserRedeem(Redeem):
|
||||||
'''
|
'''
|
||||||
用户兑换码类\
|
用户兑换码类
|
||||||
|
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -77,7 +138,7 @@ class UserRedeem(Redeem):
|
|||||||
if self.redeem_type == 0:
|
if self.redeem_type == 0:
|
||||||
raise RedeemUnavailable(
|
raise RedeemUnavailable(
|
||||||
'The redeem `%s` is unavailable.' % self.code)
|
'The redeem `%s` is unavailable.' % self.code)
|
||||||
elif self.redeem_type == 1:
|
if self.redeem_type == 1:
|
||||||
raise RedeemUnavailable(
|
raise RedeemUnavailable(
|
||||||
'The redeem `%s` is unavailable.' % self.code, 506)
|
'The redeem `%s` is unavailable.' % self.code, 506)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from setting import Config
|
from .config_manager import Config
|
||||||
|
from .constant import Constant
|
||||||
from core.constant import Constant
|
from .error import InputError, NoData
|
||||||
|
|
||||||
from .error import InputError
|
|
||||||
from .util import md5
|
from .util import md5
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +11,8 @@ class SaveData:
|
|||||||
|
|
||||||
def __init__(self, c=None) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
|
self.user = None
|
||||||
|
|
||||||
self.scores_data = []
|
self.scores_data = []
|
||||||
self.clearlamps_data = []
|
self.clearlamps_data = []
|
||||||
self.clearedsongs_data = []
|
self.clearedsongs_data = []
|
||||||
@@ -56,6 +56,20 @@ class SaveData:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def select_scores(self, user) -> None:
|
||||||
|
'''
|
||||||
|
parameter: `user` - `User`类或子类的实例
|
||||||
|
'''
|
||||||
|
self.user = user
|
||||||
|
self.c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''',
|
||||||
|
{'a': user.user_id})
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData(f'User `{user.user_id}` has no cloud save data')
|
||||||
|
|
||||||
|
self.scores_data: list = json.loads(x[0])[""]
|
||||||
|
self.clearlamps_data: list = json.loads(x[1])[""]
|
||||||
|
|
||||||
def select_all(self, user) -> None:
|
def select_all(self, user) -> None:
|
||||||
'''
|
'''
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
@@ -82,13 +96,16 @@ 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
|
||||||
|
elif x[-5:-2] == '109':
|
||||||
i['complete'] = 3
|
i['complete'] = 3
|
||||||
else:
|
else:
|
||||||
i['complete'] = 1
|
i['complete'] = 1
|
||||||
@@ -101,9 +118,7 @@ class SaveData:
|
|||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
self.createdAt = int(time() * 1000)
|
self.createdAt = int(time() * 1000)
|
||||||
self.c.execute('''delete from user_save where user_id=:a''', {
|
self.c.execute('''insert or replace into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j)''', {
|
||||||
'a': user.user_id})
|
|
||||||
self.c.execute('''insert into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j)''', {
|
|
||||||
'a': user.user_id, 'b': json.dumps({'': self.scores_data}), 'c': json.dumps({'': self.clearlamps_data}), 'd': json.dumps({'': self.clearedsongs_data}), 'e': json.dumps({'': self.unlocklist_data}), 'f': json.dumps({'val': self.installid_data}), 'g': json.dumps({'val': self.devicemodelname_data}), 'h': json.dumps({'': self.story_data}), 'i': self.createdAt, 'j': self.finalestate_data})
|
'a': user.user_id, 'b': json.dumps({'': self.scores_data}), 'c': json.dumps({'': self.clearlamps_data}), 'd': json.dumps({'': self.clearedsongs_data}), 'e': json.dumps({'': self.unlocklist_data}), 'f': json.dumps({'val': self.installid_data}), 'g': json.dumps({'val': self.devicemodelname_data}), 'h': json.dumps({'': self.story_data}), 'i': self.createdAt, 'j': self.finalestate_data})
|
||||||
|
|
||||||
def set_value(self, key: str, value: str, checksum: str) -> None:
|
def set_value(self, key: str, value: str, checksum: str) -> None:
|
||||||
@@ -117,7 +132,7 @@ class SaveData:
|
|||||||
'Property `%s` is not found in the instance of `SaveData` class.' % key)
|
'Property `%s` is not found in the instance of `SaveData` class.' % key)
|
||||||
|
|
||||||
if md5(value) == checksum:
|
if md5(value) == checksum:
|
||||||
if key == 'installid_data' or key == 'devicemodelname_data' or key == 'finalestate_data':
|
if key in ('installid_data', 'devicemodelname_data', 'finalestate_data'):
|
||||||
self.__dict__[key] = json.loads(value)['val']
|
self.__dict__[key] = json.loads(value)['val']
|
||||||
else:
|
else:
|
||||||
self.__dict__[key] = json.loads(value)['']
|
self.__dict__[key] = json.loads(value)['']
|
||||||
|
|||||||
@@ -2,18 +2,22 @@ from base64 import b64encode
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
from .bgtask import BGTask, logdb_execute
|
||||||
|
from .config_manager import Config
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .course import CoursePlay
|
from .course import CoursePlay
|
||||||
from .error import NoData, StaminaNotEnough
|
from .error import NoData, StaminaNotEnough
|
||||||
from .item import ItemCore
|
from .item import ItemCore
|
||||||
from .song import Chart
|
from .song import Chart
|
||||||
from .sql import Query, Sql
|
from .sql import Connect, Query, Sql
|
||||||
from .util import md5
|
from .util import get_today_timestamp, md5
|
||||||
from .world import WorldPlay
|
from .world import WorldPlay
|
||||||
|
|
||||||
|
|
||||||
class Score:
|
class Score:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.c = None
|
||||||
|
|
||||||
self.song: 'Chart' = Chart()
|
self.song: 'Chart' = Chart()
|
||||||
self.score: int = None
|
self.score: int = None
|
||||||
self.shiny_perfect_count: int = None
|
self.shiny_perfect_count: int = None
|
||||||
@@ -45,17 +49,16 @@ class Score:
|
|||||||
'''分数转换为评级'''
|
'''分数转换为评级'''
|
||||||
if score >= 9900000: # EX+
|
if score >= 9900000: # EX+
|
||||||
return 6
|
return 6
|
||||||
elif 9800000 <= score < 9900000: # EX
|
if score >= 9800000: # EX
|
||||||
return 5
|
return 5
|
||||||
elif 9500000 <= score < 9800000: # AA
|
if score >= 9500000: # AA
|
||||||
return 4
|
return 4
|
||||||
elif 9200000 <= score < 9500000: # A
|
if score >= 9200000: # A
|
||||||
return 3
|
return 3
|
||||||
elif 8900000 <= score < 9200000: # B
|
if score >= 8900000: # B
|
||||||
return 2
|
return 2
|
||||||
elif 8600000 <= score < 8900000: # C
|
if score >= 8600000: # C
|
||||||
return 1
|
return 1
|
||||||
else:
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -67,21 +70,24 @@ class Score:
|
|||||||
'''clear_type转换为成绩状态,用数字大小标识便于比较'''
|
'''clear_type转换为成绩状态,用数字大小标识便于比较'''
|
||||||
if clear_type == 3: # PM
|
if clear_type == 3: # PM
|
||||||
return 5
|
return 5
|
||||||
elif clear_type == 2: # FC
|
if clear_type == 2: # FC
|
||||||
return 4
|
return 4
|
||||||
elif clear_type == 5: # Hard Clear
|
if clear_type == 5: # Hard Clear
|
||||||
return 3
|
return 3
|
||||||
elif clear_type == 1: # Clear
|
if clear_type == 1: # Clear
|
||||||
return 2
|
return 2
|
||||||
elif clear_type == 4: # Easy Clear
|
if clear_type == 4: # Easy Clear
|
||||||
return 1
|
return 1
|
||||||
else: # Track Lost
|
return 0 # Track Lost
|
||||||
return 0
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def song_state(self) -> int:
|
def song_state(self) -> int:
|
||||||
return self.get_song_state(self.clear_type)
|
return self.get_song_state(self.clear_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_note_count(self) -> int:
|
||||||
|
return self.perfect_count + self.near_count + self.miss_count
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
'''分数有效性检查'''
|
'''分数有效性检查'''
|
||||||
@@ -90,7 +96,7 @@ class Score:
|
|||||||
if self.song.difficulty not in (0, 1, 2, 3):
|
if self.song.difficulty not in (0, 1, 2, 3):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
all_note = self.perfect_count + self.near_count + self.miss_count
|
all_note = self.all_note_count
|
||||||
if all_note == 0:
|
if all_note == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -102,8 +108,8 @@ class Score:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_rating(defnum: int, score: int) -> float:
|
def calculate_rating(defnum: float, score: int) -> float:
|
||||||
'''计算rating,谱面定数小于等于0视为Unrank,这里的defnum = Chart const'''
|
'''计算rating,谱面定数小于等于0视为Unrank,返回值会为-1,这里的defnum = Chart const'''
|
||||||
if not defnum or defnum <= 0:
|
if not defnum or defnum <= 0:
|
||||||
# 谱面没定数或者定数小于等于0被视作Unrank
|
# 谱面没定数或者定数小于等于0被视作Unrank
|
||||||
return -1
|
return -1
|
||||||
@@ -112,8 +118,7 @@ class Score:
|
|||||||
ptt = defnum + 2
|
ptt = defnum + 2
|
||||||
elif score < 9800000:
|
elif score < 9800000:
|
||||||
ptt = defnum + (score-9500000) / 300000
|
ptt = defnum + (score-9500000) / 300000
|
||||||
if ptt < 0:
|
ptt = max(ptt, 0)
|
||||||
ptt = 0
|
|
||||||
else:
|
else:
|
||||||
ptt = defnum + 1 + (score-9800000) / 200000
|
ptt = defnum + 1 + (score-9800000) / 200000
|
||||||
|
|
||||||
@@ -155,9 +160,10 @@ class UserScore(Score):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
self.user = user
|
self.user = user
|
||||||
self.rank = None
|
self.rank = None # 成绩排名,给Ranklist用的
|
||||||
|
|
||||||
def select_score(self) -> None:
|
def select_score(self) -> None:
|
||||||
|
'''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询'''
|
||||||
self.c.execute('''select * from best_score where user_id = :a and song_id = :b and difficulty = :c''',
|
self.c.execute('''select * from best_score 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})
|
{'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
@@ -208,6 +214,7 @@ class UserPlay(UserScore):
|
|||||||
self.stamina_multiply: int = None
|
self.stamina_multiply: int = None
|
||||||
self.fragment_multiply: int = None
|
self.fragment_multiply: int = None
|
||||||
self.prog_boost_multiply: int = None
|
self.prog_boost_multiply: int = None
|
||||||
|
self.beyond_boost_gauge_usage: int = None
|
||||||
|
|
||||||
self.ptt: Potential = None # 临时用来计算用户ptt的
|
self.ptt: Potential = None # 临时用来计算用户ptt的
|
||||||
self.world_play: 'WorldPlay' = None
|
self.world_play: 'WorldPlay' = None
|
||||||
@@ -215,7 +222,13 @@ class UserPlay(UserScore):
|
|||||||
self.course_play_state: int = None
|
self.course_play_state: int = None
|
||||||
self.course_play: 'CoursePlay' = None
|
self.course_play: 'CoursePlay' = None
|
||||||
|
|
||||||
|
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||||
|
self.skill_cytusii_flag: str = None
|
||||||
|
self.highest_health: int = None
|
||||||
|
self.lowest_health: int = None
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
|
# 不能super
|
||||||
if self.is_world_mode is None or self.course_play_state is None:
|
if self.is_world_mode is None or self.course_play_state is None:
|
||||||
return {}
|
return {}
|
||||||
if self.course_play_state == 4:
|
if self.course_play_state == 4:
|
||||||
@@ -227,7 +240,7 @@ class UserPlay(UserScore):
|
|||||||
r['user_rating'] = self.user.rating_ptt
|
r['user_rating'] = self.user.rating_ptt
|
||||||
r['finale_challenge_higher'] = self.rating > self.ptt.value
|
r['finale_challenge_higher'] = self.rating > self.ptt.value
|
||||||
r['global_rank'] = self.user.global_rank
|
r['global_rank'] = self.user.global_rank
|
||||||
r['finale_play_value'] = self.rating * 5 # emmmm
|
r['finale_play_value'] = 9.065 * self.rating ** 0.5 # by Lost-MSth
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -247,10 +260,15 @@ 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 = self.song_token + self.song_hash + self.song.song_id + str(self.song.difficulty) + str(self.score) + str(self.shiny_perfect_count) + str(
|
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}'''
|
||||||
self.perfect_count) + str(self.near_count) + str(self.miss_count) + str(self.health) + str(self.modifier) + str(self.clear_type)
|
if self.combo_interval_bonus is not None:
|
||||||
y = str(self.user.user_id) + self.song_hash
|
if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
|
||||||
|
return False
|
||||||
|
x = x + str(self.combo_interval_bonus)
|
||||||
|
|
||||||
|
y = f'{self.user.user_id}{self.song_hash}'
|
||||||
checksum = md5(x+md5(y))
|
checksum = md5(x+md5(y))
|
||||||
|
|
||||||
if checksum != self.submission_hash:
|
if checksum != self.submission_hash:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -258,6 +276,12 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
def get_play_state(self) -> None:
|
def get_play_state(self) -> None:
|
||||||
'''检查token,当然这里不管有没有,是用来判断世界模式和课题模式的'''
|
'''检查token,当然这里不管有没有,是用来判断世界模式和课题模式的'''
|
||||||
|
if self.song_token == '1145141919810':
|
||||||
|
# 硬编码检查,绕过数据库
|
||||||
|
self.is_world_mode = False
|
||||||
|
self.course_play_state = -1
|
||||||
|
return None
|
||||||
|
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select * from songplay_token where token=:a ''', {'a': self.song_token})
|
'''select * from songplay_token where token=:a ''', {'a': self.song_token})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
@@ -265,7 +289,8 @@ class UserPlay(UserScore):
|
|||||||
self.is_world_mode = False
|
self.is_world_mode = False
|
||||||
self.course_play_state = -1
|
self.course_play_state = -1
|
||||||
return None
|
return None
|
||||||
self.song.set_chart(x[2], x[3])
|
# raise NoData('No token data.')
|
||||||
|
# self.song.set_chart(x[2], x[3])
|
||||||
if x[4]:
|
if x[4]:
|
||||||
self.course_play = CoursePlay(self.c, self.user, self)
|
self.course_play = CoursePlay(self.c, self.user, self)
|
||||||
self.course_play.course_id = x[4]
|
self.course_play.course_id = x[4]
|
||||||
@@ -277,24 +302,31 @@ class UserPlay(UserScore):
|
|||||||
self.stamina_multiply = int(x[8])
|
self.stamina_multiply = int(x[8])
|
||||||
self.fragment_multiply = int(x[9])
|
self.fragment_multiply = int(x[9])
|
||||||
self.prog_boost_multiply = int(x[10])
|
self.prog_boost_multiply = int(x[10])
|
||||||
|
self.beyond_boost_gauge_usage = int(x[11])
|
||||||
|
self.skill_cytusii_flag = x[12]
|
||||||
self.is_world_mode = True
|
self.is_world_mode = True
|
||||||
self.course_play_state = -1
|
self.course_play_state = -1
|
||||||
|
|
||||||
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0) -> None:
|
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0, skill_cytusii_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)
|
||||||
if self.prog_boost_multiply != 0:
|
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
|
||||||
self.c.execute('''select prog_boost from user where user_id=:a''', {
|
self.skill_cytusii_flag = skill_cytusii_flag
|
||||||
|
if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
|
||||||
|
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
|
||||||
'a': self.user.user_id})
|
'a': self.user.user_id})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x and x[0] == 300:
|
if x:
|
||||||
self.prog_boost_multiply = 300
|
self.prog_boost_multiply = 300 if x[0] == 300 else 0
|
||||||
|
if x[1] < self.beyond_boost_gauge_usage or self.beyond_boost_gauge_usage not in (100, 200):
|
||||||
|
# 注意:偷懒了,没判断是否是beyond图
|
||||||
|
self.beyond_boost_gauge_usage = 0
|
||||||
|
|
||||||
self.clear_play_state()
|
self.clear_play_state()
|
||||||
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f)''', {
|
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h)''', {
|
||||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 't': self.song_token})
|
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, '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()
|
||||||
@@ -326,7 +358,7 @@ class UserPlay(UserScore):
|
|||||||
self.course_play.score = 0
|
self.course_play.score = 0
|
||||||
self.course_play.clear_type = 3 # 设置为PM,即最大值
|
self.course_play.clear_type = 3 # 设置为PM,即最大值
|
||||||
|
|
||||||
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0)''', (self.song_token, self.user.user_id, self.song.song_id,
|
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0,"")''', (self.song_token, self.user.user_id, self.song.song_id,
|
||||||
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
||||||
self.user.select_user_about_stamina()
|
self.user.select_user_about_stamina()
|
||||||
if use_course_skip_purchase:
|
if use_course_skip_purchase:
|
||||||
@@ -360,8 +392,8 @@ class UserPlay(UserScore):
|
|||||||
'''更新此分数对应用户的recent30'''
|
'''更新此分数对应用户的recent30'''
|
||||||
old_recent_10 = self.ptt.recent_10
|
old_recent_10 = self.ptt.recent_10
|
||||||
if self.is_protected:
|
if self.is_protected:
|
||||||
old_r30 = [x for x in self.ptt.r30]
|
old_r30 = self.ptt.r30.copy()
|
||||||
old_s30 = [x for x in self.ptt.s30]
|
old_s30 = self.ptt.s30.copy()
|
||||||
|
|
||||||
# 寻找pop位置
|
# 寻找pop位置
|
||||||
songs = list(set(self.ptt.s30))
|
songs = list(set(self.ptt.s30))
|
||||||
@@ -375,12 +407,12 @@ class UserPlay(UserScore):
|
|||||||
r30_id = 29
|
r30_id = 29
|
||||||
elif self.song.song_id_difficulty in songs and n == 10:
|
elif self.song.song_id_difficulty in songs and n == 10:
|
||||||
i = 29
|
i = 29
|
||||||
while self.ptt.s30[i] == self.song.song_id_difficulty:
|
while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0:
|
||||||
i -= 1
|
i -= 1
|
||||||
r30_id = i
|
r30_id = i
|
||||||
elif self.song.song_id_difficulty not in songs and n == 9:
|
elif self.song.song_id_difficulty not in songs and n == 9:
|
||||||
i = 29
|
i = 29
|
||||||
while self.ptt.s30.count(self.ptt.s30[-1]) == 1:
|
while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0:
|
||||||
i -= 1
|
i -= 1
|
||||||
r30_id = i
|
r30_id = i
|
||||||
else:
|
else:
|
||||||
@@ -400,6 +432,23 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
self.ptt.insert_recent_30()
|
self.ptt.insert_recent_30()
|
||||||
|
|
||||||
|
def record_score(self) -> None:
|
||||||
|
'''向log数据库记录分数,请注意列名不同'''
|
||||||
|
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
|
||||||
|
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
|
||||||
|
|
||||||
|
def record_rating_ptt(self, user_rating_ptt: float) -> None:
|
||||||
|
'''向log数据库记录用户ptt变化'''
|
||||||
|
today_timestamp = get_today_timestamp()
|
||||||
|
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
|
||||||
|
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
|
||||||
|
self.user.user_id, today_timestamp)).fetchone()
|
||||||
|
|
||||||
|
old_ptt = 0 if old_ptt is None else old_ptt[0]
|
||||||
|
if old_ptt != user_rating_ptt:
|
||||||
|
c2.execute('''insert or replace into user_rating values(?,?,?)''',
|
||||||
|
(self.user.user_id, today_timestamp, user_rating_ptt))
|
||||||
|
|
||||||
def upload_score(self) -> None:
|
def upload_score(self) -> None:
|
||||||
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
||||||
self.get_play_state()
|
self.get_play_state()
|
||||||
@@ -412,6 +461,9 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
self.time_played = int(time())
|
self.time_played = int(time())
|
||||||
|
|
||||||
|
# 记录分数
|
||||||
|
self.record_score()
|
||||||
|
|
||||||
# recent更新
|
# recent更新
|
||||||
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
|
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
|
||||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})
|
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})
|
||||||
@@ -440,7 +492,9 @@ class UserPlay(UserScore):
|
|||||||
self.update_recent30()
|
self.update_recent30()
|
||||||
|
|
||||||
# 总PTT更新
|
# 总PTT更新
|
||||||
self.user.rating_ptt = int(self.ptt.value * 100)
|
user_rating_ptt = self.ptt.value
|
||||||
|
self.user.rating_ptt = int(user_rating_ptt * 100)
|
||||||
|
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
|
||||||
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
||||||
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
||||||
|
|
||||||
@@ -456,7 +510,8 @@ class UserPlay(UserScore):
|
|||||||
|
|
||||||
class Potential:
|
class Potential:
|
||||||
'''
|
'''
|
||||||
用户潜力值计算处理类\
|
用户潜力值计算处理类
|
||||||
|
|
||||||
property: `user` - `User`类或子类的实例
|
property: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -464,8 +519,8 @@ class Potential:
|
|||||||
self.c = c
|
self.c = c
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
self.r30: list = None
|
self.r30: 'list[float]' = None
|
||||||
self.s30: list = None
|
self.s30: 'list[str]' = None
|
||||||
self.songs_selected: list = None
|
self.songs_selected: list = None
|
||||||
|
|
||||||
self.b30: list = None
|
self.b30: list = None
|
||||||
@@ -480,13 +535,16 @@ class Potential:
|
|||||||
'''获取用户best30的总潜力值'''
|
'''获取用户best30的总潜力值'''
|
||||||
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
|
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
|
||||||
'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(self) -> None:
|
||||||
'''获取用户recent30数据'''
|
'''获取用户recent30数据'''
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
|
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData(
|
||||||
|
f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3)
|
||||||
self.r30 = []
|
self.r30 = []
|
||||||
self.s30 = []
|
self.s30 = []
|
||||||
if not x:
|
if not x:
|
||||||
@@ -521,11 +579,15 @@ class Potential:
|
|||||||
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()
|
||||||
return [{
|
r = []
|
||||||
'song_id': self.s30[i][:-1],
|
for x, y in zip(self.s30, self.r30):
|
||||||
'difficulty': int(self.s30[i][-1]),
|
if x:
|
||||||
'rating': self.r30[i]
|
r.append({
|
||||||
} for i in range(len(self.r30))]
|
'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:
|
def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None:
|
||||||
self.r30.pop(pop_index)
|
self.r30.pop(pop_index)
|
||||||
@@ -548,7 +610,8 @@ class Potential:
|
|||||||
|
|
||||||
class UserScoreList:
|
class UserScoreList:
|
||||||
'''
|
'''
|
||||||
用户分数查询类\
|
用户分数查询类
|
||||||
|
|
||||||
properties: `user` - `User`类或子类的实例
|
properties: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -580,4 +643,5 @@ class UserScoreList:
|
|||||||
for score in self.scores:
|
for score in self.scores:
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select name from chart where song_id = ?''', (score.song.song_id,))
|
'''select name from chart where song_id = ?''', (score.song.song_id,))
|
||||||
score.song.song_name = self.c.fetchone()[0]
|
x = self.c.fetchone()
|
||||||
|
score.song.song_name = x[0] if x else ''
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .error import NoData
|
from .error import NoData
|
||||||
|
from .config_manager import Config
|
||||||
|
|
||||||
|
|
||||||
class Chart:
|
class Chart:
|
||||||
@@ -6,6 +7,8 @@ class Chart:
|
|||||||
|
|
||||||
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
|
self.song_id: str = None
|
||||||
|
self.difficulty: int = None
|
||||||
self.set_chart(song_id, difficulty)
|
self.set_chart(song_id, difficulty)
|
||||||
self.defnum: int = None
|
self.defnum: int = None
|
||||||
self.song_name: str = None
|
self.song_name: str = None
|
||||||
@@ -33,14 +36,16 @@ class Chart:
|
|||||||
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
|
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
|
if Config.ALLOW_SCORE_WITH_NO_SONG:
|
||||||
self.defnum = -10
|
self.defnum = -10
|
||||||
# raise NoData('The song `%s` does not exist.' % self.song_id)
|
else:
|
||||||
|
raise NoData(f'The song `{self.song_id}` does not exist.', 120)
|
||||||
else:
|
else:
|
||||||
self.defnum = x[self.difficulty]
|
self.defnum = x[self.difficulty]
|
||||||
|
|
||||||
|
|
||||||
class Song:
|
class Song:
|
||||||
def __init__(self, c=None, song_id=None) -> None:
|
def __init__(self, c=None, song_id: str = None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
self.song_id: str = song_id
|
self.song_id: str = song_id
|
||||||
self.name: str = None
|
self.name: str = None
|
||||||
@@ -65,10 +70,39 @@ class Song:
|
|||||||
self.charts[3].defnum = x[5]
|
self.charts[3].defnum = x[5]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def from_dict(self, d: dict) -> 'Song':
|
||||||
|
self.song_id = d['song_id']
|
||||||
|
self.name = d.get('name', '')
|
||||||
|
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
|
||||||
|
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)]
|
||||||
|
for i in range(4):
|
||||||
|
self.charts[i].defnum = -10
|
||||||
|
for chart in d['charts']:
|
||||||
|
self.charts[chart['difficulty']].defnum = round(
|
||||||
|
chart['chart_const'] * 10)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.c.execute(
|
||||||
|
'''delete from chart where song_id=?''', (self.song_id,))
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
'''全部更新'''
|
||||||
|
self.c.execute(
|
||||||
|
'''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.song_id))
|
||||||
|
|
||||||
def insert(self) -> None:
|
def insert(self) -> None:
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum))
|
'''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum))
|
||||||
|
|
||||||
|
def select_exists(self, song_id: str = None) -> bool:
|
||||||
|
if song_id is not None:
|
||||||
|
self.song_id = song_id
|
||||||
|
|
||||||
|
self.c.execute(
|
||||||
|
'''select exists(select * from chart where song_id=?)''', (self.song_id,))
|
||||||
|
return bool(self.c.fetchone()[0])
|
||||||
|
|
||||||
def select(self, song_id: str = None) -> 'Song':
|
def select(self, song_id: str = None) -> 'Song':
|
||||||
if song_id is not None:
|
if song_id is not None:
|
||||||
self.song_id = song_id
|
self.song_id = song_id
|
||||||
@@ -77,6 +111,6 @@ class Song:
|
|||||||
'a': self.song_id})
|
'a': self.song_id})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('The song `%s` does not exist.' % self.song_id)
|
raise NoData(f'The song `{self.song_id}` does not exist.')
|
||||||
|
|
||||||
return self.from_list(x)
|
return self.from_list(x)
|
||||||
|
|||||||
@@ -1,24 +1,36 @@
|
|||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import traceback
|
import traceback
|
||||||
|
from atexit import register
|
||||||
|
|
||||||
from flask import current_app
|
from .config_manager import Config
|
||||||
|
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
|
||||||
from .constant import Constant
|
|
||||||
from .error import ArcError, InputError
|
from .error import ArcError, InputError
|
||||||
|
|
||||||
|
|
||||||
class Connect:
|
class Connect:
|
||||||
# 数据库连接类,上下文管理
|
# 数据库连接类,上下文管理
|
||||||
|
logger = None
|
||||||
|
|
||||||
def __init__(self, file_path=Constant.SQLITE_DATABASE_PATH):
|
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
|
||||||
"""
|
"""
|
||||||
数据库连接,默认连接arcaea_database.db\
|
数据库连接,默认连接arcaea_database.db
|
||||||
接受:文件路径\
|
接受:文件路径
|
||||||
返回:sqlite3连接操作对象
|
返回:sqlite3连接操作对象
|
||||||
"""
|
"""
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
|
self.in_memory: bool = in_memory
|
||||||
|
if logger is not None:
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
def __enter__(self):
|
self.conn: sqlite3.Connection = None
|
||||||
|
self.c: sqlite3.Cursor = None
|
||||||
|
|
||||||
|
def __enter__(self) -> sqlite3.Cursor:
|
||||||
|
if self.in_memory:
|
||||||
|
self.conn = sqlite3.connect(
|
||||||
|
'file:arc_tmp?mode=memory&cache=shared', uri=True, timeout=10)
|
||||||
|
else:
|
||||||
self.conn = sqlite3.connect(self.file_path, timeout=10)
|
self.conn = sqlite3.connect(self.file_path, timeout=10)
|
||||||
self.c = self.conn.cursor()
|
self.c = self.conn.cursor()
|
||||||
return self.c
|
return self.c
|
||||||
@@ -29,13 +41,11 @@ class Connect:
|
|||||||
if issubclass(exc_type, ArcError):
|
if issubclass(exc_type, ArcError):
|
||||||
flag = False
|
flag = False
|
||||||
else:
|
else:
|
||||||
if self.conn:
|
|
||||||
self.conn.rollback()
|
self.conn.rollback()
|
||||||
|
|
||||||
current_app.logger.error(
|
self.logger.error(
|
||||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||||
|
|
||||||
if self.conn:
|
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
@@ -45,15 +55,18 @@ class Connect:
|
|||||||
class Query:
|
class Query:
|
||||||
'''查询参数类'''
|
'''查询参数类'''
|
||||||
|
|
||||||
def __init__(self, query_able: list = None, quzzy_query_able: list = None, sort_able: list = None) -> None:
|
def __init__(self, query_able: list = None, fuzzy_query_able: list = None, sort_able: list = None) -> None:
|
||||||
self.query_able: list = query_able
|
self.query_able: list = query_able # None表示不限制
|
||||||
self.quzzy_query_able: list = quzzy_query_able
|
self.fuzzy_query_able: list = fuzzy_query_able # None表示不限制
|
||||||
self.sort_able: list = sort_able
|
self.sort_able: list = sort_able
|
||||||
|
|
||||||
self.__limit: int = -1
|
self.__limit: int = -1
|
||||||
self.__offset: int = 0
|
self.__offset: int = 0
|
||||||
self.__query: dict = {} # {'name': 'admin'}
|
|
||||||
|
# {'name': 'admin'} or {'name': ['admin', 'user']}
|
||||||
|
self.__query: dict = {}
|
||||||
self.__fuzzy_query: dict = {} # {'name': 'dmi'}
|
self.__fuzzy_query: dict = {} # {'name': 'dmi'}
|
||||||
|
|
||||||
# [{'column': 'user_id', 'order': 'ASC'}, ...]
|
# [{'column': 'user_id', 'order': 'ASC'}, ...]
|
||||||
self.__sort: list = []
|
self.__sort: list = []
|
||||||
|
|
||||||
@@ -108,7 +121,7 @@ class Query:
|
|||||||
def fuzzy_query_append(self, fuzzy_query: dict) -> None:
|
def fuzzy_query_append(self, fuzzy_query: dict) -> None:
|
||||||
if not isinstance(fuzzy_query, dict):
|
if not isinstance(fuzzy_query, dict):
|
||||||
raise InputError(api_error_code=-101)
|
raise InputError(api_error_code=-101)
|
||||||
if self.quzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.quzzy_query_able)):
|
if self.fuzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.fuzzy_query_able)):
|
||||||
raise InputError(api_error_code=-102)
|
raise InputError(api_error_code=-102)
|
||||||
if not self.__fuzzy_query:
|
if not self.__fuzzy_query:
|
||||||
self.__fuzzy_query = fuzzy_query
|
self.__fuzzy_query = fuzzy_query
|
||||||
@@ -136,18 +149,22 @@ class Query:
|
|||||||
raise InputError(api_error_code=-104)
|
raise InputError(api_error_code=-104)
|
||||||
self.__sort = sort
|
self.__sort = sort
|
||||||
|
|
||||||
def set_value(self, limit=-1, offset=0, query={}, fuzzy_query={}, sort=[]) -> None:
|
def set_value(self, limit=-1, offset=0, query=None, fuzzy_query=None, sort=None) -> None:
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.query = query
|
self.query = query if query is not None else {}
|
||||||
self.fuzzy_query = fuzzy_query
|
self.fuzzy_query = fuzzy_query if fuzzy_query is not None else {}
|
||||||
self.sort = sort
|
self.sort = sort if sort is not None else []
|
||||||
|
|
||||||
def from_data(self, d: dict) -> 'Query':
|
def from_dict(self, d: dict) -> 'Query':
|
||||||
self.set_value(d.get('limit', -1), d.get('offset', 0),
|
self.set_value(d.get('limit', -1), d.get('offset', 0),
|
||||||
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
|
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = None, fuzzy_query: dict = None) -> 'Query':
|
||||||
|
self.set_value(limit, offset, query, fuzzy_query, sort)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class Sql:
|
class Sql:
|
||||||
'''
|
'''
|
||||||
@@ -157,63 +174,277 @@ class Sql:
|
|||||||
def __init__(self, c=None) -> None:
|
def __init__(self, c=None) -> None:
|
||||||
self.c = c
|
self.c = c
|
||||||
|
|
||||||
def select(self, table_name: str, target_column: 'list' = [], query: 'Query' = None) -> list:
|
@staticmethod
|
||||||
'''单表内行查询单句sql语句,返回fetchall数据'''
|
def get_select_sql(table_name: str, target_column: list = None, query: 'Query' = None):
|
||||||
|
'''拼接单表内行查询单句sql语句,返回语句和参数列表'''
|
||||||
sql = 'select '
|
|
||||||
sql_list = []
|
sql_list = []
|
||||||
if len(target_column) >= 2:
|
if not target_column:
|
||||||
sql += target_column[0]
|
sql = f'select * from {table_name}'
|
||||||
for i in range(1, len(target_column)):
|
|
||||||
sql += ',' + target_column[i]
|
|
||||||
sql += ' from ' + table_name
|
|
||||||
elif len(target_column) == 1:
|
|
||||||
sql += target_column[0] + ' from ' + table_name
|
|
||||||
else:
|
else:
|
||||||
sql += '* from ' + table_name
|
sql = f"select {', '.join(target_column)} from {table_name}"
|
||||||
|
|
||||||
if query is None:
|
if query is None:
|
||||||
self.c.execute(sql)
|
return sql, sql_list
|
||||||
return self.c.fetchall()
|
|
||||||
|
|
||||||
where_key = []
|
where_key = []
|
||||||
where_like_key = []
|
for k, v in query.query.items():
|
||||||
for i in query.query:
|
if isinstance(v, list):
|
||||||
where_key.append(i)
|
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
|
||||||
sql_list.append(query.query[i])
|
sql_list.extend(v)
|
||||||
|
|
||||||
for i in query.fuzzy_query:
|
|
||||||
where_like_key.append(i)
|
|
||||||
sql_list.append('%' + query.fuzzy_query[i] + '%')
|
|
||||||
|
|
||||||
if where_key or where_like_key:
|
|
||||||
sql += ' where '
|
|
||||||
if where_key:
|
|
||||||
sql += where_key[0] + '=?'
|
|
||||||
if len(where_key) >= 2:
|
|
||||||
for i in range(1, len(where_key)):
|
|
||||||
sql += ' and ' + where_key[i] + '=?'
|
|
||||||
if where_like_key:
|
|
||||||
for i in where_like_key:
|
|
||||||
sql += ' and ' + i + ' like ?'
|
|
||||||
else:
|
else:
|
||||||
sql += where_like_key[0] + ' like ?'
|
where_key.append(f'{k}=?')
|
||||||
|
sql_list.append(v)
|
||||||
|
|
||||||
if len(where_like_key) >= 2:
|
for k, v in query.fuzzy_query.items():
|
||||||
for i in range(1, len(where_key)):
|
where_key.append(f'{k} like ?')
|
||||||
sql += ' and ' + where_key[i] + ' like ?'
|
sql_list.append(f'%{v}%')
|
||||||
|
|
||||||
|
if where_key:
|
||||||
|
sql += ' where '
|
||||||
|
sql += ' and '.join(where_key)
|
||||||
|
|
||||||
if query.sort:
|
if query.sort:
|
||||||
sql += ' order by ' + \
|
sql += ' order by ' + \
|
||||||
query.sort[0]['column'] + ' ' + query.sort[0]['order']
|
', '.join([x['column'] + ' ' + x['order'] for x in query.sort])
|
||||||
for i in range(1, len(query.sort)):
|
|
||||||
sql += ', ' + query.sort[i]['column'] + \
|
|
||||||
' ' + query.sort[i]['order']
|
|
||||||
|
|
||||||
if query.limit >= 0:
|
if query.limit >= 0:
|
||||||
sql += ' limit ? offset ?'
|
sql += ' limit ? offset ?'
|
||||||
sql_list.append(query.limit)
|
sql_list.append(query.limit)
|
||||||
sql_list.append(query.offset)
|
sql_list.append(query.offset)
|
||||||
|
|
||||||
|
return sql, sql_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_insert_sql(table_name: str, key: list = None, value_len: int = None, insert_type: str = None) -> str:
|
||||||
|
'''拼接insert语句,请注意只返回sql语句,insert_type为replace或ignore'''
|
||||||
|
if key is None:
|
||||||
|
key = []
|
||||||
|
insert_type = 'replace' if insert_type in [
|
||||||
|
'replace', 'R', 'r', 'REPLACE'] else 'ignore'
|
||||||
|
return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_update_sql(table_name: str, d: dict = None, query: 'Query' = None):
|
||||||
|
if not d:
|
||||||
|
return None
|
||||||
|
sql_list = []
|
||||||
|
sql = f"update {table_name} set {','.join([f'{k}=?' for k in d.keys()])}"
|
||||||
|
sql_list.extend(d.values())
|
||||||
|
|
||||||
|
if query is None:
|
||||||
|
return sql, sql_list
|
||||||
|
|
||||||
|
where_key = []
|
||||||
|
for k, v in query.query.items():
|
||||||
|
if isinstance(v, list):
|
||||||
|
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
|
||||||
|
sql_list.extend(v)
|
||||||
|
else:
|
||||||
|
where_key.append(f'{k}=?')
|
||||||
|
sql_list.append(v)
|
||||||
|
|
||||||
|
for k, v in query.fuzzy_query.items():
|
||||||
|
where_key.append(f'{k} like ?')
|
||||||
|
sql_list.append(f'%{v}%')
|
||||||
|
|
||||||
|
if where_key:
|
||||||
|
sql += ' where '
|
||||||
|
sql += ' and '.join(where_key)
|
||||||
|
|
||||||
|
return sql, sql_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_update_many_sql(table_name: str, key: list = None, where_key: list = None) -> str:
|
||||||
|
'''拼接update语句,这里不用Query类,也不用字典,请注意只返回sql语句'''
|
||||||
|
if not key or not where_key:
|
||||||
|
return None
|
||||||
|
return f"update {table_name} set {','.join([f'{k}=?' for k in key])} where {' and '.join([f'{k}=?' for k in where_key])}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_delete_sql(table_name: str, query: 'Query' = None):
|
||||||
|
'''拼接删除语句,query中只有query和fuzzy_query会被处理'''
|
||||||
|
sql = f'delete from {table_name}'
|
||||||
|
|
||||||
|
if query is None:
|
||||||
|
return sql, []
|
||||||
|
|
||||||
|
sql_list = []
|
||||||
|
where_key = []
|
||||||
|
for k, v in query.query.items():
|
||||||
|
if isinstance(v, list):
|
||||||
|
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
|
||||||
|
sql_list.extend(v)
|
||||||
|
else:
|
||||||
|
where_key.append(f'{k}=?')
|
||||||
|
sql_list.append(v)
|
||||||
|
|
||||||
|
for k, v in query.fuzzy_query.items():
|
||||||
|
where_key.append(f'{k} like ?')
|
||||||
|
sql_list.append(f'%{v}%')
|
||||||
|
|
||||||
|
if where_key:
|
||||||
|
sql += ' where '
|
||||||
|
sql += ' and '.join(where_key)
|
||||||
|
|
||||||
|
return sql, sql_list
|
||||||
|
|
||||||
|
def select(self, table_name: str, target_column: list = None, query: 'Query' = None) -> list:
|
||||||
|
'''单表内行select单句sql语句,返回fetchall数据'''
|
||||||
|
sql, sql_list = self.get_select_sql(table_name, target_column, query)
|
||||||
self.c.execute(sql, sql_list)
|
self.c.execute(sql, sql_list)
|
||||||
return self.c.fetchall()
|
return self.c.fetchall()
|
||||||
|
|
||||||
|
def select_exists(self, table_name: str, target_column: list = None, query: 'Query' = None) -> bool:
|
||||||
|
'''单表内行select exists单句sql语句,返回bool值'''
|
||||||
|
sql, sql_list = self.get_select_sql(table_name, target_column, query)
|
||||||
|
self.c.execute('select exists(' + sql + ')', sql_list)
|
||||||
|
return self.c.fetchone() == (1,)
|
||||||
|
|
||||||
|
def insert(self, table_name: str, key: list, value: tuple, insert_type: str = None) -> None:
|
||||||
|
'''单行插入或覆盖插入,key传[]则为全部列,insert_type为replace或ignore'''
|
||||||
|
self.c.execute(self.get_insert_sql(
|
||||||
|
table_name, key, len(value), insert_type), value)
|
||||||
|
|
||||||
|
def insert_many(self, table_name: str, key: list, value_list: list, insert_type: str = None) -> None:
|
||||||
|
'''多行插入或覆盖插入,key传[]则为全部列,insert_type为replace或ignore'''
|
||||||
|
if not value_list:
|
||||||
|
return
|
||||||
|
self.c.executemany(self.get_insert_sql(
|
||||||
|
table_name, key, len(value_list[0]), insert_type), value_list)
|
||||||
|
|
||||||
|
def update(self, table_name: str, d: dict, query: 'Query' = None) -> None:
|
||||||
|
'''单表内行update单句sql语句'''
|
||||||
|
if not d:
|
||||||
|
return
|
||||||
|
sql, sql_list = self.get_update_sql(table_name, d, query)
|
||||||
|
self.c.execute(sql, sql_list)
|
||||||
|
|
||||||
|
def update_many(self, table_name: str, key: list, value_list: list, where_key: list, where_value_list: list) -> None:
|
||||||
|
'''单表内行update多句sql语句,这里不用Query类,也不用字典,要求值list长度一致,有点像insert_many'''
|
||||||
|
if not key or not value_list or not where_key or not where_value_list or not len(key) == len(value_list[0]) or not len(where_key) == len(where_value_list[0]) or not len(value_list) == len(where_value_list):
|
||||||
|
raise ValueError
|
||||||
|
self.c.executemany(self.get_update_many_sql(
|
||||||
|
table_name, key, where_key), [x + y for x, y in zip(value_list, where_value_list)])
|
||||||
|
|
||||||
|
def delete(self, table_name: str, query: 'Query' = None) -> None:
|
||||||
|
'''删除,query中只有query和fuzzy_query会被处理'''
|
||||||
|
sql, sql_list = self.get_delete_sql(table_name, query)
|
||||||
|
self.c.execute(sql, sql_list)
|
||||||
|
|
||||||
|
def get_table_info(self, table_name: str):
|
||||||
|
'''得到表结构,返回主键列表和字段名列表'''
|
||||||
|
pk = []
|
||||||
|
name = []
|
||||||
|
|
||||||
|
self.c.execute(f'''pragma table_info ("{table_name}")''') # 这里无法参数化
|
||||||
|
x = self.c.fetchall()
|
||||||
|
if x:
|
||||||
|
for i in x:
|
||||||
|
name.append(i[1])
|
||||||
|
if i[5] != 0:
|
||||||
|
pk.append(i[1])
|
||||||
|
|
||||||
|
return pk, name
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMigrator:
|
||||||
|
|
||||||
|
def __init__(self, c1_path: str, c2_path: str) -> None:
|
||||||
|
self.c1_path = c1_path
|
||||||
|
self.c2_path = c2_path
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_one_table(c1, c2, table_name: str) -> bool:
|
||||||
|
'''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖'''
|
||||||
|
c1.execute(
|
||||||
|
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
||||||
|
c2.execute(
|
||||||
|
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
||||||
|
if not c1.fetchone() or not c2.fetchone():
|
||||||
|
return False
|
||||||
|
|
||||||
|
sql1 = Sql(c1)
|
||||||
|
sql2 = Sql(c2)
|
||||||
|
db1_pk, db1_name = sql1.get_table_info(table_name)
|
||||||
|
db2_pk, db2_name = sql2.get_table_info(table_name)
|
||||||
|
if db1_pk != db2_pk:
|
||||||
|
return False
|
||||||
|
|
||||||
|
public_column = list(filter(lambda x: x in db2_name, db1_name))
|
||||||
|
|
||||||
|
sql2.insert_many(table_name, public_column, sql1.select(
|
||||||
|
table_name, public_column), insert_type='replace')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_user_char_full(c) -> None:
|
||||||
|
'''用character表数据更新user_char_full'''
|
||||||
|
c.execute('''select character_id, max_level, is_uncapped from character''')
|
||||||
|
x = c.fetchall()
|
||||||
|
c.execute('''select user_id from user''')
|
||||||
|
y = c.fetchall()
|
||||||
|
c.execute('''delete from user_char_full''')
|
||||||
|
for i in x:
|
||||||
|
exp = 25000 if i[1] == 30 else 10000
|
||||||
|
c.executemany('''insert into user_char_full values(?,?,?,?,?,?,0)''', [
|
||||||
|
(j[0], i[0], i[1], exp, i[2], 0) for j in y])
|
||||||
|
|
||||||
|
def update_database(self) -> None:
|
||||||
|
'''
|
||||||
|
将c1数据库不存在数据加入或覆盖到c2数据库上
|
||||||
|
对于c2,更新一些表,并用character数据更新user_char_full
|
||||||
|
'''
|
||||||
|
with Connect(self.c2_path) as c2:
|
||||||
|
with Connect(self.c1_path) as c1:
|
||||||
|
for i in Constant.DATABASE_MIGRATE_TABLES:
|
||||||
|
self.update_one_table(c1, c2, i)
|
||||||
|
|
||||||
|
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||||
|
self.update_one_table(c1, c2, 'character')
|
||||||
|
|
||||||
|
self.update_user_char_full(c2) # 更新user_char_full
|
||||||
|
|
||||||
|
|
||||||
|
class LogDatabaseMigrator:
|
||||||
|
|
||||||
|
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
|
||||||
|
self.c1_path = c1_path
|
||||||
|
# self.c2_path = c2_path
|
||||||
|
self.init_folder_path = Config.DATABASE_INIT_PATH
|
||||||
|
self.c = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sql_path(self) -> str:
|
||||||
|
return os.path.join(self.init_folder_path, 'log_tables.sql')
|
||||||
|
|
||||||
|
def table_update(self) -> None:
|
||||||
|
'''直接更新数据库结构'''
|
||||||
|
with open(self.sql_path, 'r') as f:
|
||||||
|
self.c.executescript(f.read())
|
||||||
|
self.c.execute(
|
||||||
|
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
|
||||||
|
|
||||||
|
def update_database(self) -> None:
|
||||||
|
with Connect(self.c1_path) as c:
|
||||||
|
self.c = c
|
||||||
|
self.table_update()
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryDatabase:
|
||||||
|
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.c = self.conn.cursor()
|
||||||
|
self.c.execute('''PRAGMA journal_mode = OFF''')
|
||||||
|
self.c.execute('''PRAGMA synchronous = 0''')
|
||||||
|
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));''')
|
||||||
|
self.c.execute(
|
||||||
|
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@register
|
||||||
|
def atexit():
|
||||||
|
MemoryDatabase.conn.close()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class GameInfo:
|
|||||||
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
||||||
"core_exp": Constant.CORE_EXP,
|
"core_exp": Constant.CORE_EXP,
|
||||||
"curr_ts": int(time()*1000),
|
"curr_ts": int(time()*1000),
|
||||||
"level_steps": [{'level': i, 'level_exp': Constant.LEVEL_STEPS[i]} for i in Constant.LEVEL_STEPS],
|
"level_steps": [{'level': k, 'level_exp': v} for k, v in Constant.LEVEL_STEPS.items()],
|
||||||
"world_ranking_enabled": True,
|
"world_ranking_enabled": True,
|
||||||
"is_byd_chapter_unlocked": True
|
"is_byd_chapter_unlocked": True
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ import base64
|
|||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from os import urandom
|
from os import urandom
|
||||||
|
from random import randint
|
||||||
from setting import Config
|
|
||||||
|
|
||||||
from .character import UserCharacter, UserCharacterList
|
from .character import UserCharacter, UserCharacterList
|
||||||
|
from .config_manager import Config
|
||||||
from .constant import Constant
|
from .constant import Constant
|
||||||
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
||||||
NoData, UserBan)
|
NoData, RateLimit, UserBan)
|
||||||
from .item import UserItemList
|
from .item import UserItemList
|
||||||
|
from .limiter import ArcLimiter
|
||||||
from .score import Score
|
from .score import Score
|
||||||
from .sql import Connect
|
from .sql import Query, Sql
|
||||||
from .world import Map, UserMap, UserStamina
|
from .world import Map, UserMap, UserStamina
|
||||||
|
|
||||||
|
|
||||||
@@ -32,17 +33,17 @@ def code_get_id(c, user_code: str) -> int:
|
|||||||
|
|
||||||
class User:
|
class User:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.name = None
|
self.name: str = None
|
||||||
self.email = None
|
self.email: str = None
|
||||||
self.password = None
|
self.password: str = None
|
||||||
self.user_id = None
|
self.user_id: int = None
|
||||||
self.user_code = None
|
self.user_code: str = None
|
||||||
|
|
||||||
self.join_date = None
|
self.join_date = None
|
||||||
self.rating_ptt: int = None # 100 times
|
self.rating_ptt: int = None # 100 times
|
||||||
|
|
||||||
self.ticket = None
|
self.ticket: int = None
|
||||||
self.world_rank_score = None
|
self.world_rank_score: int = None
|
||||||
self.ban_flag = None
|
self.ban_flag = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -63,7 +64,7 @@ class UserRegister(User):
|
|||||||
if self.c.fetchone() == (0,):
|
if self.c.fetchone() == (0,):
|
||||||
self.name = name
|
self.name = name
|
||||||
else:
|
else:
|
||||||
raise DataExist('Username exists.', 101, -203)
|
raise DataExist('Username exists.', 101, -210)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise InputError('Username is invalid.')
|
raise InputError('Username is invalid.')
|
||||||
@@ -76,19 +77,30 @@ class UserRegister(User):
|
|||||||
|
|
||||||
def set_email(self, email: str):
|
def set_email(self, email: str):
|
||||||
# 邮箱格式懒得多判断
|
# 邮箱格式懒得多判断
|
||||||
if 4 <= len(email) <= 32 and '@' in email and '.' in email:
|
if 4 <= len(email) <= 64 and '@' in email and '.' in email:
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select exists(select * from user where email = :email)''', {'email': email})
|
'''select exists(select * from user where email = :email)''', {'email': email})
|
||||||
if self.c.fetchone() == (0,):
|
if self.c.fetchone() == (0,):
|
||||||
self.email = email
|
self.email = email
|
||||||
else:
|
else:
|
||||||
raise DataExist('Email address exists.', 102, -204)
|
raise DataExist('Email address exists.', 102, -211)
|
||||||
else:
|
else:
|
||||||
raise InputError('Email address is invalid.')
|
raise InputError('Email address is invalid.')
|
||||||
|
|
||||||
|
def set_user_code(self, user_code: str) -> None:
|
||||||
|
'''设置用户的user_code'''
|
||||||
|
if len(user_code) == 9 and user_code.isdigit():
|
||||||
|
self.c.execute(
|
||||||
|
'''select exists(select * from user where user_code = ?)''', (user_code, ))
|
||||||
|
if self.c.fetchone() == (0,):
|
||||||
|
self.user_code = user_code
|
||||||
|
else:
|
||||||
|
raise DataExist('User code exists.', 103, -212)
|
||||||
|
else:
|
||||||
|
raise InputError('User code is invalid.')
|
||||||
|
|
||||||
def _build_user_code(self):
|
def _build_user_code(self):
|
||||||
# 生成9位的user_code,用的自然是随机
|
# 生成9位的user_code,用的自然是随机
|
||||||
from random import randint
|
|
||||||
random_times = 0
|
random_times = 0
|
||||||
|
|
||||||
while random_times <= 1000:
|
while random_times <= 1000:
|
||||||
@@ -115,9 +127,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''')
|
||||||
@@ -125,12 +137,14 @@ 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 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):
|
||||||
now = int(time.time() * 1000)
|
now = int(time.time() * 1000)
|
||||||
|
if self.user_code is None:
|
||||||
self._build_user_code()
|
self._build_user_code()
|
||||||
|
if self.user_id is None:
|
||||||
self._build_user_id()
|
self._build_user_id()
|
||||||
self._insert_user_char()
|
self._insert_user_char()
|
||||||
|
|
||||||
@@ -144,6 +158,8 @@ class UserRegister(User):
|
|||||||
|
|
||||||
class UserLogin(User):
|
class UserLogin(User):
|
||||||
# 密码和token的加密方式为 SHA-256
|
# 密码和token的加密方式为 SHA-256
|
||||||
|
limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login')
|
||||||
|
|
||||||
def __init__(self, c) -> None:
|
def __init__(self, c) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.c = c
|
self.c = c
|
||||||
@@ -220,30 +236,35 @@ class UserLogin(User):
|
|||||||
if ip:
|
if ip:
|
||||||
self.set_ip(ip)
|
self.set_ip(ip)
|
||||||
|
|
||||||
|
if not self.limiter.hit(name):
|
||||||
|
raise RateLimit(
|
||||||
|
f'Too many login attempts of username `{name}`', 123, -203)
|
||||||
|
|
||||||
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
|
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
|
||||||
'name': self.name})
|
'name': self.name})
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x is None:
|
if x is None:
|
||||||
raise NoData('Username does not exist.', 104)
|
raise NoData(f'Username `{self.name}` does not exist.', 104)
|
||||||
|
|
||||||
|
self.user_id = x[0]
|
||||||
self.now = int(time.time() * 1000)
|
self.now = int(time.time() * 1000)
|
||||||
if x[2] is not None and x[2] != '':
|
if x[2] is not None and x[2] != '':
|
||||||
# 自动封号检查
|
# 自动封号检查
|
||||||
ban_timestamp = int(x[2].split(':', 1)[1])
|
ban_timestamp = int(x[2].split(':', 1)[1])
|
||||||
if ban_timestamp > self.now:
|
if ban_timestamp > self.now:
|
||||||
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
|
raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
|
||||||
'remaining_ts': ban_timestamp-self.now})
|
'remaining_ts': ban_timestamp-self.now})
|
||||||
|
|
||||||
if x[1] == '':
|
if x[1] == '':
|
||||||
# 账号封禁
|
# 账号封禁
|
||||||
raise UserBan('The account has been banned.', 106)
|
raise UserBan(
|
||||||
|
f'The account `{self.user_id}` has been banned.', 106)
|
||||||
|
|
||||||
if x[1] != self.hash_pwd:
|
if x[1] != self.hash_pwd:
|
||||||
raise NoAccess('Wrong password.', 104)
|
raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
|
||||||
|
|
||||||
self.user_id = str(x[0])
|
|
||||||
self.token = base64.b64encode(hashlib.sha256(
|
self.token = base64.b64encode(hashlib.sha256(
|
||||||
(self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
||||||
|
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
|
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
|
||||||
@@ -276,7 +297,7 @@ class UserAuth(User):
|
|||||||
|
|
||||||
class UserInfo(User):
|
class UserInfo(User):
|
||||||
def __init__(self, c, user_id=None) -> None:
|
def __init__(self, c, user_id=None) -> None:
|
||||||
super().__init__()
|
User.__init__(self)
|
||||||
self.c = c
|
self.c = c
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.character = None
|
self.character = None
|
||||||
@@ -285,9 +306,12 @@ class UserInfo(User):
|
|||||||
self.recent_score = Score()
|
self.recent_score = Score()
|
||||||
self.favorite_character = None
|
self.favorite_character = None
|
||||||
self.max_stamina_notification_enabled = False
|
self.max_stamina_notification_enabled = False
|
||||||
self.prog_boost = 0
|
self.prog_boost: int = 0
|
||||||
|
self.beyond_boost_gauge: float = 0
|
||||||
self.next_fragstam_ts: int = None
|
self.next_fragstam_ts: int = None
|
||||||
self.world_mode_locked_end_ts: int = None
|
self.world_mode_locked_end_ts: int = None
|
||||||
|
self.current_map: 'Map' = None
|
||||||
|
self.stamina: 'UserStamina' = None
|
||||||
|
|
||||||
self.__cores: list = None
|
self.__cores: list = None
|
||||||
self.__packs: list = None
|
self.__packs: list = None
|
||||||
@@ -370,20 +394,22 @@ class UserInfo(User):
|
|||||||
self.favorite_character.select_character_uncap_condition(self)
|
self.favorite_character.select_character_uncap_condition(self)
|
||||||
return self.favorite_character
|
return self.favorite_character
|
||||||
|
|
||||||
|
@property
|
||||||
|
def friend_ids(self) -> list:
|
||||||
|
self.c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
|
||||||
|
'user_id': self.user_id})
|
||||||
|
return self.c.fetchall()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def friends(self) -> list:
|
def friends(self) -> list:
|
||||||
# 得到用户的朋友列表
|
# 得到用户的朋友列表
|
||||||
if self.__friends is None:
|
if self.__friends is None:
|
||||||
self.c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
|
|
||||||
'user_id': self.user_id})
|
|
||||||
x = self.c.fetchall()
|
|
||||||
s = []
|
s = []
|
||||||
if x != [] and x[0][0] is not None:
|
for i in self.friend_ids:
|
||||||
for i in x:
|
|
||||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||||
{'x': i[0], 'y': self.user_id})
|
{'x': i[0], 'y': self.user_id})
|
||||||
|
|
||||||
is_mutual = True if self.c.fetchone() == (1,) else False
|
is_mutual = self.c.fetchone() == (1,)
|
||||||
|
|
||||||
you = UserOnline(self.c, i[0])
|
you = UserOnline(self.c, i[0])
|
||||||
you.select_user()
|
you.select_user()
|
||||||
@@ -474,6 +500,7 @@ class UserInfo(User):
|
|||||||
"is_skill_sealed": self.is_skill_sealed,
|
"is_skill_sealed": self.is_skill_sealed,
|
||||||
"current_map": self.current_map.map_id,
|
"current_map": self.current_map.map_id,
|
||||||
"prog_boost": self.prog_boost,
|
"prog_boost": self.prog_boost,
|
||||||
|
"beyond_boost_gauge": self.beyond_boost_gauge,
|
||||||
"next_fragstam_ts": self.next_fragstam_ts,
|
"next_fragstam_ts": self.next_fragstam_ts,
|
||||||
"max_stamina_ts": self.stamina.max_stamina_ts,
|
"max_stamina_ts": self.stamina.max_stamina_ts,
|
||||||
"stamina": self.stamina.stamina,
|
"stamina": self.stamina.stamina,
|
||||||
@@ -531,6 +558,7 @@ class UserInfo(User):
|
|||||||
self.stamina = UserStamina(self.c, self)
|
self.stamina = UserStamina(self.c, self)
|
||||||
self.stamina.set_value(x[32], x[33])
|
self.stamina.set_value(x[32], x[33])
|
||||||
self.world_mode_locked_end_ts = x[34] if x[34] else -1
|
self.world_mode_locked_end_ts = x[34] if x[34] else -1
|
||||||
|
self.beyond_boost_gauge = x[35] if x[35] else 0
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -561,16 +589,8 @@ class UserInfo(User):
|
|||||||
self.stamina = UserStamina(self.c, self)
|
self.stamina = UserStamina(self.c, self)
|
||||||
self.stamina.set_value(x[0], x[1])
|
self.stamina.set_value(x[0], x[1])
|
||||||
|
|
||||||
def select_user_about_character(self) -> None:
|
def from_list_about_character(self, x: list) -> None:
|
||||||
'''
|
'''从数据库user表获取搭档信息'''
|
||||||
查询user表有关角色的信息
|
|
||||||
'''
|
|
||||||
self.c.execute('''select name, character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, favorite_character from user where user_id = :a''', {
|
|
||||||
'a': self.user_id})
|
|
||||||
x = self.c.fetchone()
|
|
||||||
if not x:
|
|
||||||
raise NoData('No user.', 108, -3)
|
|
||||||
|
|
||||||
self.name = x[0]
|
self.name = x[0]
|
||||||
self.character = UserCharacter(self.c, x[1], self)
|
self.character = UserCharacter(self.c, x[1], self)
|
||||||
self.is_skill_sealed = x[2] == 1
|
self.is_skill_sealed = x[2] == 1
|
||||||
@@ -579,12 +599,24 @@ class UserInfo(User):
|
|||||||
self.favorite_character = None if x[5] == - \
|
self.favorite_character = None if x[5] == - \
|
||||||
1 else UserCharacter(self.c, x[5], self)
|
1 else UserCharacter(self.c, x[5], self)
|
||||||
|
|
||||||
|
def select_user_about_character(self) -> None:
|
||||||
|
'''
|
||||||
|
查询user表有关搭档的信息
|
||||||
|
'''
|
||||||
|
self.c.execute('''select name, character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, favorite_character from user where user_id = :a''', {
|
||||||
|
'a': self.user_id})
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData('No user.', 108, -3)
|
||||||
|
|
||||||
|
self.from_list_about_character(x)
|
||||||
|
|
||||||
def select_user_about_world_play(self) -> None:
|
def select_user_about_world_play(self) -> None:
|
||||||
'''
|
'''
|
||||||
查询user表有关世界模式打歌的信息
|
查询user表有关世界模式打歌的信息
|
||||||
'''
|
'''
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts from user where user_id=?''', (self.user_id,))
|
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge from user where user_id=?''', (self.user_id,))
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if not x:
|
if not x:
|
||||||
raise NoData('No user.', 108, -3)
|
raise NoData('No user.', 108, -3)
|
||||||
@@ -597,6 +629,7 @@ class UserInfo(User):
|
|||||||
self.character.is_uncapped_override = x[5] == 1
|
self.character.is_uncapped_override = x[5] == 1
|
||||||
self.current_map = UserMap(self.c, x[6], self)
|
self.current_map = UserMap(self.c, x[6], self)
|
||||||
self.world_mode_locked_end_ts = x[7] if x[7] else -1
|
self.world_mode_locked_end_ts = x[7] if x[7] else -1
|
||||||
|
self.beyond_boost_gauge = x[8] if x[8] else 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def global_rank(self) -> int:
|
def global_rank(self) -> int:
|
||||||
@@ -616,9 +649,9 @@ class UserInfo(User):
|
|||||||
|
|
||||||
def update_global_rank(self) -> None:
|
def update_global_rank(self) -> None:
|
||||||
'''用户世界排名计算,有新增成绩则要更新'''
|
'''用户世界排名计算,有新增成绩则要更新'''
|
||||||
with Connect() as c2:
|
|
||||||
c2.execute('''select song_id, rating_ftr, rating_byn from chart''')
|
self.c.execute('''select song_id, rating_ftr, rating_byn from chart''')
|
||||||
x = c2.fetchall()
|
x = self.c.fetchall()
|
||||||
|
|
||||||
song_list_ftr = [self.user_id]
|
song_list_ftr = [self.user_id]
|
||||||
song_list_byn = [self.user_id]
|
song_list_byn = [self.user_id]
|
||||||
@@ -630,16 +663,16 @@ class UserInfo(User):
|
|||||||
|
|
||||||
score_sum = 0
|
score_sum = 0
|
||||||
if len(song_list_ftr) >= 2:
|
if len(song_list_ftr) >= 2:
|
||||||
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({0})'''.format(
|
self.c.execute(
|
||||||
','.join(['?']*(len(song_list_ftr)-1))), tuple(song_list_ftr))
|
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))
|
||||||
|
|
||||||
x = self.c.fetchone()
|
x = self.c.fetchone()
|
||||||
if x[0] is not None:
|
if x[0] is not None:
|
||||||
score_sum += x[0]
|
score_sum += x[0]
|
||||||
|
|
||||||
if len(song_list_byn) >= 2:
|
if len(song_list_byn) >= 2:
|
||||||
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({0})'''.format(
|
self.c.execute(
|
||||||
','.join(['?']*(len(song_list_byn)-1))), tuple(song_list_byn))
|
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()
|
x = self.c.fetchone()
|
||||||
if x[0] is not None:
|
if x[0] is not None:
|
||||||
@@ -652,13 +685,13 @@ class UserInfo(User):
|
|||||||
|
|
||||||
def select_user_one_column(self, column_name: str, default_value=None) -> None:
|
def select_user_one_column(self, column_name: str, default_value=None) -> None:
|
||||||
'''
|
'''
|
||||||
查询user表的某个属性\
|
查询user表的某个属性
|
||||||
请注意必须是一个普通属性,不能是一个类的实例
|
请注意必须是一个普通属性,不能是一个类的实例
|
||||||
'''
|
'''
|
||||||
if column_name not in self.__dict__:
|
if column_name not in self.__dict__:
|
||||||
raise InputError('No such column.')
|
raise InputError('No such column.')
|
||||||
self.c.execute('''select %s from user where user_id = :a''' %
|
self.c.execute(f'''select {column_name} from user where user_id = :a''', {
|
||||||
column_name, {'a': self.user_id})
|
'a': 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)
|
||||||
@@ -667,15 +700,15 @@ class UserInfo(User):
|
|||||||
|
|
||||||
def update_user_one_column(self, column_name: str, value=None) -> None:
|
def update_user_one_column(self, column_name: str, value=None) -> None:
|
||||||
'''
|
'''
|
||||||
更新user表的某个属性\
|
更新user表的某个属性
|
||||||
请注意必须是一个普通属性,不能是一个类的实例
|
请注意必须是一个普通属性,不能是一个类的实例
|
||||||
'''
|
'''
|
||||||
if column_name not in self.__dict__:
|
if column_name not in self.__dict__:
|
||||||
raise InputError('No such column.')
|
raise InputError('No such column.')
|
||||||
if value is not None:
|
if value is not None:
|
||||||
self.__dict__[column_name] = value
|
self.__dict__[column_name] = value
|
||||||
self.c.execute('''update user set %s = :a where user_id = :b''' %
|
self.c.execute(f'''update user set {column_name} = :a where user_id = :b''', {
|
||||||
column_name, {'a': self.__dict__[column_name], 'b': self.user_id})
|
'a': self.__dict__[column_name], 'b': self.user_id})
|
||||||
|
|
||||||
|
|
||||||
class UserOnline(UserInfo):
|
class UserOnline(UserInfo):
|
||||||
@@ -720,3 +753,21 @@ class UserOnline(UserInfo):
|
|||||||
self.favorite_character = UserCharacter(self.c, character_id, self)
|
self.favorite_character = UserCharacter(self.c, character_id, self)
|
||||||
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})
|
||||||
|
|
||||||
|
|
||||||
|
class UserChanger(UserInfo, UserRegister):
|
||||||
|
|
||||||
|
def __init__(self, c, user_id=None) -> None:
|
||||||
|
super().__init__(c, user_id)
|
||||||
|
|
||||||
|
def update_columns(self, columns: list = None, d: dict = None) -> None:
|
||||||
|
if columns is not None:
|
||||||
|
d = {}
|
||||||
|
for column in columns:
|
||||||
|
if column == 'password' and self.password != '':
|
||||||
|
d[column] = self.hash_pwd
|
||||||
|
else:
|
||||||
|
d[column] = self.__dict__[column]
|
||||||
|
|
||||||
|
Sql(self.c).update('user', d, Query().from_args(
|
||||||
|
{'user_id': self.user_id}))
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from datetime import date
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
|
||||||
def md5(code):
|
def aes_gcm_128_encrypt(key, plaintext, associated_data):
|
||||||
|
iv = os.urandom(12)
|
||||||
|
encryptor = Cipher(
|
||||||
|
algorithms.AES(key),
|
||||||
|
modes.GCM(iv, min_tag_length=12),
|
||||||
|
).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=12),
|
||||||
|
).decryptor()
|
||||||
|
decryptor.authenticate_additional_data(associated_data)
|
||||||
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
|
||||||
|
|
||||||
|
def md5(code: str) -> str:
|
||||||
# md5加密算法
|
# md5加密算法
|
||||||
code = code.encode()
|
code = code.encode()
|
||||||
md5s = hashlib.md5()
|
md5s = hashlib.md5()
|
||||||
@@ -12,16 +35,33 @@ def md5(code):
|
|||||||
return codes
|
return codes
|
||||||
|
|
||||||
|
|
||||||
def get_file_md5(file_path):
|
def get_file_md5(file_path: str) -> str:
|
||||||
# 计算文件MD5
|
'''计算文件MD5,假设是文件'''
|
||||||
if not os.path.isfile(file_path):
|
|
||||||
return None
|
|
||||||
myhash = hashlib.md5()
|
myhash = hashlib.md5()
|
||||||
with open(file_path, 'rb') as f:
|
with open(file_path, 'rb') as f:
|
||||||
while True:
|
while True:
|
||||||
b = f.read(8096)
|
b = f.read(8192)
|
||||||
if not b:
|
if not b:
|
||||||
break
|
break
|
||||||
myhash.update(b)
|
myhash.update(b)
|
||||||
|
|
||||||
return myhash.hexdigest()
|
return myhash.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def try_rename(path: str, new_path: str) -> str:
|
||||||
|
'''尝试重命名文件,并尝试避免命名冲突(在后面加自增数字),返回最终路径'''
|
||||||
|
final_path = new_path
|
||||||
|
if os.path.exists(new_path):
|
||||||
|
i = 1
|
||||||
|
while os.path.exists(new_path + str(i)):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
final_path = new_path + str(i)
|
||||||
|
|
||||||
|
os.rename(path, final_path)
|
||||||
|
return final_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_today_timestamp():
|
||||||
|
'''相对于本机本地时间的今天0点的时间戳'''
|
||||||
|
return int(mktime(date.today().timetuple()))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from json import load
|
||||||
from random import random
|
from random import random
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
@@ -25,15 +25,15 @@ def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list:
|
|||||||
def get_world_info(map_id: str) -> dict:
|
def get_world_info(map_id: str) -> dict:
|
||||||
'''读取json文件内容,返回字典'''
|
'''读取json文件内容,返回字典'''
|
||||||
world_info = {}
|
world_info = {}
|
||||||
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, map_id+'.json'), 'r') as f:
|
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, f'{map_id}.json'), 'rb') as f:
|
||||||
world_info = json.load(f)
|
world_info = load(f)
|
||||||
|
|
||||||
return world_info
|
return world_info
|
||||||
|
|
||||||
|
|
||||||
def get_world_all(c, user) -> list:
|
def get_world_all(c, user) -> list:
|
||||||
'''
|
'''
|
||||||
读取所有地图信息,返回列表\
|
读取所有地图信息,返回列表
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
worlds = get_world_name()
|
worlds = get_world_name()
|
||||||
@@ -44,12 +44,13 @@ class Step:
|
|||||||
'''台阶类'''
|
'''台阶类'''
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.postion: int = None
|
self.position: int = None
|
||||||
self.capture: int = None
|
self.capture: int = None
|
||||||
self.items: list = []
|
self.items: list = []
|
||||||
self.restrict_id: str = None
|
self.restrict_id: str = None
|
||||||
self.restrict_ids: list = []
|
self.restrict_ids: list = []
|
||||||
self.restrict_type: str = None
|
self.restrict_type: str = None
|
||||||
|
self.restrict_difficulty: int = None
|
||||||
self.step_type: list = None
|
self.step_type: list = None
|
||||||
self.speed_limit_value: int = None
|
self.speed_limit_value: int = None
|
||||||
self.plus_stamina_value: int = None
|
self.plus_stamina_value: int = None
|
||||||
@@ -61,12 +62,14 @@ class Step:
|
|||||||
}
|
}
|
||||||
if self.items:
|
if self.items:
|
||||||
r['items'] = [i.to_dict() for i in self.items]
|
r['items'] = [i.to_dict() for i in self.items]
|
||||||
|
if self.restrict_type:
|
||||||
|
r['restrict_type'] = self.restrict_type
|
||||||
if self.restrict_id:
|
if self.restrict_id:
|
||||||
r['restrict_id'] = self.restrict_id
|
r['restrict_id'] = self.restrict_id
|
||||||
if self.restrict_ids:
|
if self.restrict_ids:
|
||||||
r['restrict_ids'] = self.restrict_ids
|
r['restrict_ids'] = self.restrict_ids
|
||||||
if self.restrict_type:
|
if self.restrict_difficulty is not None:
|
||||||
r['restrict_type'] = self.restrict_type
|
r['restrict_difficulty'] = self.restrict_difficulty
|
||||||
if self.step_type:
|
if self.step_type:
|
||||||
r['step_type'] = self.step_type
|
r['step_type'] = self.step_type
|
||||||
if self.speed_limit_value:
|
if self.speed_limit_value:
|
||||||
@@ -82,6 +85,7 @@ class Step:
|
|||||||
self.restrict_id = d.get('restrict_id')
|
self.restrict_id = d.get('restrict_id')
|
||||||
self.restrict_ids = d.get('restrict_ids')
|
self.restrict_ids = d.get('restrict_ids')
|
||||||
self.restrict_type = d.get('restrict_type')
|
self.restrict_type = d.get('restrict_type')
|
||||||
|
self.restrict_difficulty = d.get('restrict_difficulty')
|
||||||
self.step_type = d.get('step_type')
|
self.step_type = d.get('step_type')
|
||||||
self.speed_limit_value = d.get('speed_limit_value')
|
self.speed_limit_value = d.get('speed_limit_value')
|
||||||
self.plus_stamina_value = d.get('plus_stamina_value')
|
self.plus_stamina_value = d.get('plus_stamina_value')
|
||||||
@@ -102,7 +106,7 @@ class Map:
|
|||||||
self.available_from: int = None
|
self.available_from: int = None
|
||||||
self.available_to: int = None
|
self.available_to: int = None
|
||||||
self.is_repeatable: bool = None
|
self.is_repeatable: bool = None
|
||||||
self.require_id: str = None
|
self.require_id: 'str | list[str]' = None
|
||||||
self.require_type: str = None
|
self.require_type: str = None
|
||||||
self.require_value: int = None
|
self.require_value: int = None
|
||||||
self.coordinate: str = None
|
self.coordinate: str = None
|
||||||
@@ -111,6 +115,10 @@ class Map:
|
|||||||
self.steps: list = []
|
self.steps: list = []
|
||||||
self.__rewards: list = None
|
self.__rewards: list = None
|
||||||
|
|
||||||
|
self.require_localunlock_songid: str = None
|
||||||
|
self.require_localunlock_challengeid: str = None
|
||||||
|
self.chain_info: dict = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rewards(self) -> list:
|
def rewards(self) -> list:
|
||||||
if self.__rewards is None:
|
if self.__rewards is None:
|
||||||
@@ -133,7 +141,7 @@ class Map:
|
|||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
if self.chapter is None:
|
if self.chapter is None:
|
||||||
self.select_map_info()
|
self.select_map_info()
|
||||||
return {
|
r = {
|
||||||
'map_id': self.map_id,
|
'map_id': self.map_id,
|
||||||
'is_legacy': self.is_legacy,
|
'is_legacy': self.is_legacy,
|
||||||
'is_beyond': self.is_beyond,
|
'is_beyond': self.is_beyond,
|
||||||
@@ -151,8 +159,13 @@ class Map:
|
|||||||
'custom_bg': self.custom_bg,
|
'custom_bg': self.custom_bg,
|
||||||
'stamina_cost': self.stamina_cost,
|
'stamina_cost': self.stamina_cost,
|
||||||
'step_count': self.step_count,
|
'step_count': self.step_count,
|
||||||
|
'require_localunlock_songid': self.require_localunlock_songid,
|
||||||
|
'require_localunlock_challengeid': self.require_localunlock_challengeid,
|
||||||
'steps': [s.to_dict() for s in self.steps],
|
'steps': [s.to_dict() for s in self.steps],
|
||||||
}
|
}
|
||||||
|
if self.chain_info is not None:
|
||||||
|
r['chain_info'] = self.chain_info
|
||||||
|
return r
|
||||||
|
|
||||||
def from_dict(self, raw_dict: dict) -> 'Map':
|
def from_dict(self, raw_dict: dict) -> 'Map':
|
||||||
self.is_legacy = raw_dict.get('is_legacy')
|
self.is_legacy = raw_dict.get('is_legacy')
|
||||||
@@ -170,6 +183,11 @@ class Map:
|
|||||||
self.coordinate = raw_dict.get('coordinate')
|
self.coordinate = raw_dict.get('coordinate')
|
||||||
self.custom_bg = raw_dict.get('custom_bg', '')
|
self.custom_bg = raw_dict.get('custom_bg', '')
|
||||||
self.stamina_cost = raw_dict.get('stamina_cost')
|
self.stamina_cost = raw_dict.get('stamina_cost')
|
||||||
|
self.require_localunlock_songid = raw_dict.get(
|
||||||
|
'require_localunlock_songid', '')
|
||||||
|
self.require_localunlock_challengeid = raw_dict.get(
|
||||||
|
'require_localunlock_challengeid', '')
|
||||||
|
self.chain_info = raw_dict.get('chain_info')
|
||||||
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
|
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -180,7 +198,7 @@ class Map:
|
|||||||
|
|
||||||
class UserMap(Map):
|
class UserMap(Map):
|
||||||
'''
|
'''
|
||||||
用户地图类\
|
用户地图类
|
||||||
parameters: `user` - `User`类或者子类的实例
|
parameters: `user` - `User`类或者子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -290,7 +308,7 @@ class UserMap(Map):
|
|||||||
if self.require_type in ['pack', 'single']:
|
if self.require_type in ['pack', 'single']:
|
||||||
item = ItemFactory(self.c).get_item(self.require_type)
|
item = ItemFactory(self.c).get_item(self.require_type)
|
||||||
item.item_id = self.require_id
|
item.item_id = self.require_id
|
||||||
item.select(self.user)
|
item.select_user_item(self.user)
|
||||||
if not item.amount:
|
if not item.amount:
|
||||||
self.is_locked = True
|
self.is_locked = True
|
||||||
|
|
||||||
@@ -395,7 +413,8 @@ class Stamina:
|
|||||||
|
|
||||||
class UserStamina(Stamina):
|
class UserStamina(Stamina):
|
||||||
'''
|
'''
|
||||||
用户体力类\
|
用户体力类
|
||||||
|
|
||||||
parameter: `user` - `User`类或子类的实例
|
parameter: `user` - `User`类或子类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -421,8 +440,9 @@ class UserStamina(Stamina):
|
|||||||
|
|
||||||
class WorldPlay:
|
class WorldPlay:
|
||||||
'''
|
'''
|
||||||
世界模式打歌类,处理特殊角色技能,联动UserMap和UserPlay\
|
世界模式打歌类,处理特殊角色技能,联动UserMap和UserPlay
|
||||||
parameter: `user` - `UserOnline`类或子类的实例\
|
|
||||||
|
parameter: `user` - `UserOnline`类或子类的实例
|
||||||
'user_play` - `UserPlay`类的实例
|
'user_play` - `UserPlay`类的实例
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -436,8 +456,9 @@ class WorldPlay:
|
|||||||
self.step_value: float = None
|
self.step_value: float = None
|
||||||
|
|
||||||
self.prog_tempest: float = None
|
self.prog_tempest: float = None
|
||||||
self.overdrive_extra: float = None
|
|
||||||
self.character_bonus_progress: float = None
|
self.character_bonus_progress: float = None
|
||||||
|
self.prog_skill_increase: float = None
|
||||||
|
self.over_skill_increase: float = None
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
arcmap: 'UserMap' = self.user.current_map
|
arcmap: 'UserMap' = self.user.current_map
|
||||||
@@ -465,21 +486,27 @@ class WorldPlay:
|
|||||||
},
|
},
|
||||||
"current_stamina": self.user.stamina.stamina,
|
"current_stamina": self.user.stamina.stamina,
|
||||||
"max_stamina_ts": self.user.stamina.max_stamina_ts,
|
"max_stamina_ts": self.user.stamina.max_stamina_ts,
|
||||||
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts
|
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
|
||||||
|
'beyond_boost_gauge': self.user.beyond_boost_gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.overdrive_extra is not None:
|
if self.over_skill_increase is not None:
|
||||||
r['char_stats']['overdrive'] += self.overdrive_extra
|
r['char_stats']['over_skill_increase'] = self.over_skill_increase
|
||||||
|
if self.prog_skill_increase is not None:
|
||||||
|
r['char_stats']['prog_skill_increase'] = self.prog_skill_increase
|
||||||
|
|
||||||
if self.prog_tempest is not None:
|
if self.prog_tempest is not None:
|
||||||
r['char_stats']['prog'] += self.prog_tempest
|
r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样
|
||||||
r['char_stats']['prog_tempest'] = self.prog_tempest
|
r['char_stats']['prog_tempest'] = self.prog_tempest
|
||||||
|
|
||||||
if self.character_bonus_progress is not None:
|
if self.character_bonus_progress is not None:
|
||||||
# 猜的,为了让客户端正确显示,当然结果是没问题的
|
# 猜的,为了让客户端正确显示
|
||||||
r['base_progress'] += self.character_bonus_progress
|
r['progress'] -= self.character_bonus_progress
|
||||||
r['character_bonus_progress'] = self.character_bonus_progress
|
r['character_bonus_progress'] = self.character_bonus_progress
|
||||||
|
|
||||||
|
if self.character_used.skill_id_displayed == 'skill_maya':
|
||||||
|
r['char_stats']['skill_state'] = self.character_used.skill_state
|
||||||
|
|
||||||
if self.user_play.beyond_gauge == 0:
|
if self.user_play.beyond_gauge == 0:
|
||||||
r["user_map"]["steps"] = [
|
r["user_map"]["steps"] = [
|
||||||
x.to_dict() for x in arcmap.steps_for_climbing]
|
x.to_dict() for x in arcmap.steps_for_climbing]
|
||||||
@@ -492,16 +519,45 @@ class WorldPlay:
|
|||||||
r['fragment_multiply'] = self.user_play.fragment_multiply
|
r['fragment_multiply'] = self.user_play.fragment_multiply
|
||||||
if self.user_play.prog_boost_multiply != 0:
|
if self.user_play.prog_boost_multiply != 0:
|
||||||
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
|
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
|
||||||
|
if self.user_play.beyond_boost_gauge_usage != 0:
|
||||||
|
r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
@property
|
||||||
|
def beyond_boost_gauge_addition(self) -> float:
|
||||||
|
# guessed by Lost-MSth
|
||||||
|
return 2.45 * self.user_play.rating ** 0.5 + 27
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def step_times(self) -> float:
|
def step_times(self) -> float:
|
||||||
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (self.user_play.prog_boost_multiply+100) / 100
|
prog_boost_multiply = self.user_play.prog_boost_multiply + 100
|
||||||
|
beyond_boost_times = 1
|
||||||
|
|
||||||
|
if self.user_play.beyond_gauge == 1:
|
||||||
|
if prog_boost_multiply > 100:
|
||||||
|
prog_boost_multiply -= 100
|
||||||
|
if self.user_play.beyond_boost_gauge_usage == 100:
|
||||||
|
beyond_boost_times = 2
|
||||||
|
elif self.user_play.beyond_boost_gauge_usage == 200:
|
||||||
|
beyond_boost_times = 3
|
||||||
|
|
||||||
|
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * prog_boost_multiply / 100 * beyond_boost_times
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def exp_times(self) -> float:
|
def exp_times(self) -> float:
|
||||||
return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply+100) / 100
|
prog_boost_multiply = self.user_play.prog_boost_multiply + 100
|
||||||
|
beyond_boost_times = 1
|
||||||
|
|
||||||
|
if self.user_play.beyond_gauge == 1:
|
||||||
|
if prog_boost_multiply > 100:
|
||||||
|
prog_boost_multiply -= 100
|
||||||
|
if self.user_play.beyond_boost_gauge_usage == 100:
|
||||||
|
beyond_boost_times = 2
|
||||||
|
elif self.user_play.beyond_boost_gauge_usage == 200:
|
||||||
|
beyond_boost_times = 3
|
||||||
|
|
||||||
|
return self.user_play.stamina_multiply * prog_boost_multiply / 100 * beyond_boost_times
|
||||||
|
|
||||||
def get_step(self) -> None:
|
def get_step(self) -> None:
|
||||||
if self.user_play.beyond_gauge == 0:
|
if self.user_play.beyond_gauge == 0:
|
||||||
@@ -510,6 +566,8 @@ class WorldPlay:
|
|||||||
self.character_used.level)
|
self.character_used.level)
|
||||||
if self.prog_tempest:
|
if self.prog_tempest:
|
||||||
prog += self.prog_tempest
|
prog += self.prog_tempest
|
||||||
|
if self.prog_skill_increase:
|
||||||
|
prog += self.prog_skill_increase
|
||||||
|
|
||||||
self.step_value = self.base_step_value * prog / 50 * self.step_times
|
self.step_value = self.base_step_value * prog / 50 * self.step_times
|
||||||
else:
|
else:
|
||||||
@@ -528,8 +586,8 @@ class WorldPlay:
|
|||||||
|
|
||||||
overdrive = self.character_used.overdrive.get_value(
|
overdrive = self.character_used.overdrive.get_value(
|
||||||
self.character_used.level)
|
self.character_used.level)
|
||||||
if self.overdrive_extra:
|
if self.over_skill_increase:
|
||||||
overdrive += self.overdrive_extra
|
overdrive += self.over_skill_increase
|
||||||
|
|
||||||
self.step_value = self.base_step_value * overdrive / \
|
self.step_value = self.base_step_value * overdrive / \
|
||||||
50 * self.step_times * affinity_multiplier
|
50 * self.step_times * affinity_multiplier
|
||||||
@@ -542,6 +600,20 @@ class WorldPlay:
|
|||||||
self.user_play.clear_play_state()
|
self.user_play.clear_play_state()
|
||||||
self.user.select_user_about_world_play()
|
self.user.select_user_about_world_play()
|
||||||
|
|
||||||
|
if self.user_play.beyond_gauge == 0:
|
||||||
|
# 更新byd大招蓄力条
|
||||||
|
self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition
|
||||||
|
self.user.beyond_boost_gauge = min(
|
||||||
|
self.user.beyond_boost_gauge, 200)
|
||||||
|
self.user.update_user_one_column(
|
||||||
|
'beyond_boost_gauge', self.user.beyond_boost_gauge)
|
||||||
|
elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge:
|
||||||
|
self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage
|
||||||
|
if abs(self.user.beyond_boost_gauge) <= 1e-5:
|
||||||
|
self.user.beyond_boost_gauge = 0
|
||||||
|
self.user.update_user_one_column(
|
||||||
|
'beyond_boost_gauge', self.user.beyond_boost_gauge)
|
||||||
|
|
||||||
self.character_used = Character()
|
self.character_used = Character()
|
||||||
|
|
||||||
self.user.character.select_character_info()
|
self.user.character.select_character_info()
|
||||||
@@ -584,16 +656,17 @@ class WorldPlay:
|
|||||||
self.user.current_map.update()
|
self.user.current_map.update()
|
||||||
|
|
||||||
def before_calculate(self) -> None:
|
def before_calculate(self) -> None:
|
||||||
if self.user_play.beyond_gauge == 0:
|
factory_dict = {'skill_vita': self._skill_vita, 'skill_mika': self._skill_mika, 'skill_ilith_ivy': self._skill_ilith_ivy,
|
||||||
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
|
'ilith_awakened_skill': self._ilith_awakened_skill, 'skill_hikari_vanessa': self._skill_hikari_vanessa}
|
||||||
|
if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
|
||||||
self._special_tempest()
|
self._special_tempest()
|
||||||
else:
|
|
||||||
if self.character_used.skill_id_displayed == 'skill_vita':
|
if self.character_used.skill_id_displayed in factory_dict:
|
||||||
self._skill_vita()
|
factory_dict[self.character_used.skill_id_displayed]()
|
||||||
|
|
||||||
def after_climb(self) -> None:
|
def after_climb(self) -> None:
|
||||||
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
|
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
|
||||||
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis}
|
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane, 'skill_maya': self._skill_maya, 'skill_mithra': self._skill_mithra}
|
||||||
if self.character_used.skill_id_displayed in factory_dict:
|
if self.character_used.skill_id_displayed in factory_dict:
|
||||||
factory_dict[self.character_used.skill_id_displayed]()
|
factory_dict[self.character_used.skill_id_displayed]()
|
||||||
|
|
||||||
@@ -613,12 +686,12 @@ class WorldPlay:
|
|||||||
|
|
||||||
def _skill_vita(self) -> None:
|
def _skill_vita(self) -> None:
|
||||||
'''
|
'''
|
||||||
vita技能,overdrive随回忆率提升,提升量最多为10\
|
vita技能,overdrive随回忆率提升,提升量最多为10
|
||||||
此处采用线性函数
|
此处采用线性函数
|
||||||
'''
|
'''
|
||||||
self.overdrive_extra = 0
|
self.over_skill_increase = 0
|
||||||
if 0 < self.user_play.health <= 100:
|
if 0 < self.user_play.health <= 100:
|
||||||
self.overdrive_extra = self.user_play.health / 10
|
self.over_skill_increase = self.user_play.health / 10
|
||||||
|
|
||||||
def _eto_uncap(self) -> None:
|
def _eto_uncap(self) -> None:
|
||||||
'''eto觉醒技能,获得残片奖励时世界模式进度加7'''
|
'''eto觉醒技能,获得残片奖励时世界模式进度加7'''
|
||||||
@@ -634,7 +707,7 @@ class WorldPlay:
|
|||||||
|
|
||||||
if fragment_flag:
|
if fragment_flag:
|
||||||
self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS
|
self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS
|
||||||
self.step_value += self.character_bonus_progress * self.step_times
|
self.step_value += self.character_bonus_progress
|
||||||
|
|
||||||
self.user.current_map.reclimb(self.step_value)
|
self.user.current_map.reclimb(self.step_value)
|
||||||
|
|
||||||
@@ -643,7 +716,7 @@ class WorldPlay:
|
|||||||
x: 'Step' = self.user.current_map.steps_for_climbing[0]
|
x: 'Step' = self.user.current_map.steps_for_climbing[0]
|
||||||
if x.restrict_id and x.restrict_type:
|
if x.restrict_id and x.restrict_type:
|
||||||
self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
|
self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
|
||||||
self.step_value += self.character_bonus_progress * self.step_times
|
self.step_value += self.character_bonus_progress
|
||||||
|
|
||||||
self.user.current_map.reclimb(self.step_value)
|
self.user.current_map.reclimb(self.step_value)
|
||||||
|
|
||||||
@@ -653,9 +726,9 @@ class WorldPlay:
|
|||||||
self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random(
|
self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random(
|
||||||
) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS
|
) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS
|
||||||
|
|
||||||
self.step_value += self.character_bonus_progress * self.step_times
|
self.step_value += self.character_bonus_progress
|
||||||
if self.step_value < 0:
|
if self.step_value < 0:
|
||||||
self.character_bonus_progress += self.step_value / self.step_times
|
self.character_bonus_progress += self.step_value
|
||||||
self.step_value = 0
|
self.step_value = 0
|
||||||
|
|
||||||
self.user.current_map.reclimb(self.step_value)
|
self.user.current_map.reclimb(self.step_value)
|
||||||
@@ -666,3 +739,72 @@ class WorldPlay:
|
|||||||
self.user.world_mode_locked_end_ts = int(
|
self.user.world_mode_locked_end_ts = int(
|
||||||
time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME
|
time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME
|
||||||
self.user.update_user_one_column('world_mode_locked_end_ts')
|
self.user.update_user_one_column('world_mode_locked_end_ts')
|
||||||
|
|
||||||
|
def _skill_amane(self) -> None:
|
||||||
|
'''
|
||||||
|
amane技能,起始格为限速或随机,成绩小于EX时,世界模式进度减半
|
||||||
|
偷懒在after_climb里面,需要重爬一次
|
||||||
|
'''
|
||||||
|
x: 'Step' = self.user.current_map.steps_for_climbing[0]
|
||||||
|
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
|
||||||
|
self.character_bonus_progress = -self.step_value / 2
|
||||||
|
self.step_value = self.step_value / 2
|
||||||
|
self.user.current_map.reclimb(self.step_value)
|
||||||
|
|
||||||
|
def _ilith_awakened_skill(self) -> None:
|
||||||
|
'''
|
||||||
|
ilith 觉醒技能,曲目通关时步数+6,wiki 说是 prog 值+6
|
||||||
|
'''
|
||||||
|
if self.user_play.health > 0:
|
||||||
|
self.prog_skill_increase = 6
|
||||||
|
|
||||||
|
def _skill_mika(self) -> None:
|
||||||
|
'''
|
||||||
|
mika 技能,通关特定曲目能力值翻倍
|
||||||
|
'''
|
||||||
|
if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0:
|
||||||
|
self.over_skill_increase = self.character_used.overdrive.get_value(
|
||||||
|
self.character_used.level)
|
||||||
|
self.prog_skill_increase = self.character_used.prog.get_value(
|
||||||
|
self.character_used.level)
|
||||||
|
|
||||||
|
def _skill_mithra(self) -> None:
|
||||||
|
'''
|
||||||
|
mithra 技能,每 150 combo 增加世界模式进度+1
|
||||||
|
'''
|
||||||
|
if self.user_play.combo_interval_bonus:
|
||||||
|
self.character_bonus_progress = self.user_play.combo_interval_bonus
|
||||||
|
self.step_value += self.character_bonus_progress
|
||||||
|
self.user.current_map.reclimb(self.step_value)
|
||||||
|
|
||||||
|
def _skill_ilith_ivy(self) -> None:
|
||||||
|
'''
|
||||||
|
ilith & ivy 技能,根据 skill_cytusii_flag 来增加三个数值,最高生命每过 20 就对应数值 +10
|
||||||
|
'''
|
||||||
|
if not self.user_play.skill_cytusii_flag:
|
||||||
|
return
|
||||||
|
x = self.user_play.skill_cytusii_flag[:
|
||||||
|
self.user_play.highest_health // 20]
|
||||||
|
self.over_skill_increase = x.count('2') * 10
|
||||||
|
self.prog_skill_increase = x.count('1') * 10
|
||||||
|
|
||||||
|
def _skill_hikari_vanessa(self) -> None:
|
||||||
|
'''
|
||||||
|
hikari & vanessa 技能,根据 skill_cytusii_flag 来减少三个数值,最高生命每过 20 就对应数值 -10
|
||||||
|
'''
|
||||||
|
if not self.user_play.skill_cytusii_flag:
|
||||||
|
return
|
||||||
|
x = self.user_play.skill_cytusii_flag[:5 -
|
||||||
|
self.user_play.lowest_health // 20]
|
||||||
|
self.over_skill_increase = -x.count('2') * 10
|
||||||
|
self.prog_skill_increase = -x.count('1') * 10
|
||||||
|
|
||||||
|
def _skill_maya(self) -> None:
|
||||||
|
'''
|
||||||
|
maya 技能,skill_flag 为 1 时,世界模式进度翻倍
|
||||||
|
'''
|
||||||
|
if self.character_used.skill_flag:
|
||||||
|
self.character_bonus_progress = self.step_value
|
||||||
|
self.step_value += self.character_bonus_progress
|
||||||
|
self.user.current_map.reclimb(self.step_value)
|
||||||
|
self.character_used.change_skill_state()
|
||||||
|
|||||||
@@ -1,605 +1,15 @@
|
|||||||
import sqlite3
|
import os
|
||||||
import time
|
import sys
|
||||||
import json
|
from importlib import import_module
|
||||||
|
|
||||||
# 数据库初始化文件,删掉arcaea_database.db文件后运行即可,谨慎使用
|
# 数据库初始化文件,删掉arcaea_database.db文件后运行即可,谨慎使用
|
||||||
|
|
||||||
ARCAEA_SERVER_VERSION = 'v2.10.0'
|
|
||||||
|
|
||||||
|
def main(core_path: str = '../', db_path: str = './arcaea_database.db', init_folder_path: str = './init'):
|
||||||
def main(path='./'):
|
sys.path.append(os.path.join(sys.path[0], core_path))
|
||||||
conn = sqlite3.connect(path+'arcaea_database.db')
|
sys.path.append(os.path.join(sys.path[0], core_path, './core/'))
|
||||||
c = conn.cursor()
|
init = import_module('init').DatabaseInit(db_path, init_folder_path)
|
||||||
c.execute('''create table if not exists config(id text primary key,
|
init.init()
|
||||||
value text
|
|
||||||
);''')
|
|
||||||
c.execute('''insert into config values("version", :a);''',
|
|
||||||
{'a': ARCAEA_SERVER_VERSION})
|
|
||||||
c.execute('''create table if not exists user(user_id int primary key,
|
|
||||||
name text unique,
|
|
||||||
password text not null,
|
|
||||||
join_date char(20),
|
|
||||||
user_code char(10),
|
|
||||||
rating_ptt int,
|
|
||||||
character_id int,
|
|
||||||
is_skill_sealed int,
|
|
||||||
is_char_uncapped int,
|
|
||||||
is_char_uncapped_override int,
|
|
||||||
is_hide_rating int,
|
|
||||||
song_id text,
|
|
||||||
difficulty int,
|
|
||||||
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,
|
|
||||||
rating real,
|
|
||||||
favorite_character int,
|
|
||||||
max_stamina_notification_enabled int,
|
|
||||||
current_map text,
|
|
||||||
ticket int,
|
|
||||||
prog_boost int,
|
|
||||||
email text,
|
|
||||||
world_rank_score int,
|
|
||||||
ban_flag text,
|
|
||||||
next_fragstam_ts int,
|
|
||||||
max_stamina_ts int,
|
|
||||||
stamina int,
|
|
||||||
world_mode_locked_end_ts int
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists login(access_token text,
|
|
||||||
user_id int,
|
|
||||||
login_time int,
|
|
||||||
login_ip text,
|
|
||||||
login_device text,
|
|
||||||
primary key(access_token, user_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists friend(user_id_me int,
|
|
||||||
user_id_other int,
|
|
||||||
primary key (user_id_me, user_id_other)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists best_score(user_id int,
|
|
||||||
song_id text,
|
|
||||||
difficulty int,
|
|
||||||
score int,
|
|
||||||
shiny_perfect_count int,
|
|
||||||
perfect_count int,
|
|
||||||
near_count int,
|
|
||||||
miss_count int,
|
|
||||||
health int,
|
|
||||||
modifier int,
|
|
||||||
time_played int,
|
|
||||||
best_clear_type int,
|
|
||||||
clear_type int,
|
|
||||||
rating real,
|
|
||||||
primary key(user_id, song_id, difficulty)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_char(user_id int,
|
|
||||||
character_id int,
|
|
||||||
level int,
|
|
||||||
exp real,
|
|
||||||
is_uncapped int,
|
|
||||||
is_uncapped_override int,
|
|
||||||
primary key(user_id, character_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_char_full(user_id int,
|
|
||||||
character_id int,
|
|
||||||
level int,
|
|
||||||
exp real,
|
|
||||||
is_uncapped int,
|
|
||||||
is_uncapped_override int,
|
|
||||||
primary key(user_id, character_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists character(character_id int primary key,
|
|
||||||
name text,
|
|
||||||
max_level int,
|
|
||||||
frag1 real,
|
|
||||||
prog1 real,
|
|
||||||
overdrive1 real,
|
|
||||||
frag20 real,
|
|
||||||
prog20 real,
|
|
||||||
overdrive20 real,
|
|
||||||
frag30 real,
|
|
||||||
prog30 real,
|
|
||||||
overdrive30 real,
|
|
||||||
skill_id text,
|
|
||||||
skill_unlock_level int,
|
|
||||||
skill_requires_uncap int,
|
|
||||||
skill_id_uncap text,
|
|
||||||
char_type int,
|
|
||||||
is_uncapped int
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists char_item(character_id int,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(character_id, item_id, type)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists recent30(user_id int primary key,
|
|
||||||
r0 real,
|
|
||||||
song_id0 text,
|
|
||||||
r1 real,
|
|
||||||
song_id1 text,
|
|
||||||
r2 real,
|
|
||||||
song_id2 text,
|
|
||||||
r3 real,
|
|
||||||
song_id3 text,
|
|
||||||
r4 real,
|
|
||||||
song_id4 text,
|
|
||||||
r5 real,
|
|
||||||
song_id5 text,
|
|
||||||
r6 real,
|
|
||||||
song_id6 text,
|
|
||||||
r7 real,
|
|
||||||
song_id7 text,
|
|
||||||
r8 real,
|
|
||||||
song_id8 text,
|
|
||||||
r9 real,
|
|
||||||
song_id9 text,
|
|
||||||
r10 real,
|
|
||||||
song_id10 text,
|
|
||||||
r11 real,
|
|
||||||
song_id11 text,
|
|
||||||
r12 real,
|
|
||||||
song_id12 text,
|
|
||||||
r13 real,
|
|
||||||
song_id13 text,
|
|
||||||
r14 real,
|
|
||||||
song_id14 text,
|
|
||||||
r15 real,
|
|
||||||
song_id15 text,
|
|
||||||
r16 real,
|
|
||||||
song_id16 text,
|
|
||||||
r17 real,
|
|
||||||
song_id17 text,
|
|
||||||
r18 real,
|
|
||||||
song_id18 text,
|
|
||||||
r19 real,
|
|
||||||
song_id19 text,
|
|
||||||
r20 real,
|
|
||||||
song_id20 text,
|
|
||||||
r21 real,
|
|
||||||
song_id21 text,
|
|
||||||
r22 real,
|
|
||||||
song_id22 text,
|
|
||||||
r23 real,
|
|
||||||
song_id23 text,
|
|
||||||
r24 real,
|
|
||||||
song_id24 text,
|
|
||||||
r25 real,
|
|
||||||
song_id25 text,
|
|
||||||
r26 real,
|
|
||||||
song_id26 text,
|
|
||||||
r27 real,
|
|
||||||
song_id27 text,
|
|
||||||
r28 real,
|
|
||||||
song_id28 text,
|
|
||||||
r29 real,
|
|
||||||
song_id29 text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_world(user_id int,
|
|
||||||
map_id text,
|
|
||||||
curr_position int,
|
|
||||||
curr_capture real,
|
|
||||||
is_locked int,
|
|
||||||
primary key(user_id, map_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists songplay_token(token text primary key,
|
|
||||||
user_id int,
|
|
||||||
song_id text,
|
|
||||||
difficulty int,
|
|
||||||
course_id text,
|
|
||||||
course_state int,
|
|
||||||
course_score int,
|
|
||||||
course_clear_type int,
|
|
||||||
stamina_multiply int,
|
|
||||||
fragment_multiply int,
|
|
||||||
prog_boost_multiply int
|
|
||||||
);''')
|
|
||||||
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)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_download(user_id int,
|
|
||||||
time int,
|
|
||||||
token text,
|
|
||||||
primary key(user_id, time, token)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists item(item_id text,
|
|
||||||
type text,
|
|
||||||
is_available int,
|
|
||||||
_id text,
|
|
||||||
primary key(item_id, type)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_item(user_id int,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(user_id, item_id, type)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists purchase(purchase_name text primary key,
|
|
||||||
price int,
|
|
||||||
orig_price int,
|
|
||||||
discount_from int,
|
|
||||||
discount_to int,
|
|
||||||
discount_reason text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists purchase_item(purchase_name text,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(purchase_name, item_id, type)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_save(user_id int primary key,
|
|
||||||
scores_data text,
|
|
||||||
clearlamps_data text,
|
|
||||||
clearedsongs_data text,
|
|
||||||
unlocklist_data text,
|
|
||||||
installid_data text,
|
|
||||||
devicemodelname_data text,
|
|
||||||
story_data text,
|
|
||||||
createdAt int,
|
|
||||||
finalestate_data text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists present(present_id text primary key,
|
|
||||||
expire_ts int,
|
|
||||||
description text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_present(user_id int,
|
|
||||||
present_id text,
|
|
||||||
primary key(user_id, present_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists present_item(present_id text,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(present_id, item_id, type)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists chart(song_id text primary key,
|
|
||||||
name text,
|
|
||||||
rating_pst int,
|
|
||||||
rating_prs int,
|
|
||||||
rating_ftr int,
|
|
||||||
rating_byn int
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists redeem(code text primary key,
|
|
||||||
type int
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_redeem(user_id int,
|
|
||||||
code text,
|
|
||||||
primary key(user_id, code)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists redeem_item(code text,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(code, item_id, type)
|
|
||||||
);''')
|
|
||||||
|
|
||||||
c.execute('''create table if not exists role(role_id int primary key,
|
|
||||||
role_name text,
|
|
||||||
caption text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_role(user_id int,
|
|
||||||
role_id int,
|
|
||||||
primary key(user_id, role_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists power(power_id int primary key,
|
|
||||||
power_name text,
|
|
||||||
caption text
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists role_power(role_id int,
|
|
||||||
power_id int,
|
|
||||||
primary key(role_id, power_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists api_login(user_id int,
|
|
||||||
token text,
|
|
||||||
login_time int,
|
|
||||||
login_ip text,
|
|
||||||
primary key(user_id, token)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists course(course_id text primary key,
|
|
||||||
course_name text,
|
|
||||||
dan_name text,
|
|
||||||
style int,
|
|
||||||
gauge_requirement text,
|
|
||||||
flag_as_hidden_when_requirements_not_met int,
|
|
||||||
can_start int
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists user_course(user_id int,
|
|
||||||
course_id text,
|
|
||||||
high_score int,
|
|
||||||
best_clear_type int,
|
|
||||||
primary key(user_id, course_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists course_chart(course_id text,
|
|
||||||
song_id text,
|
|
||||||
difficulty int,
|
|
||||||
flag_as_hidden int,
|
|
||||||
song_index int,
|
|
||||||
primary key(course_id, song_index)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists course_requirement(course_id text,
|
|
||||||
required_id text,
|
|
||||||
primary key(course_id, required_id)
|
|
||||||
);''')
|
|
||||||
c.execute('''create table if not exists course_item(course_id text,
|
|
||||||
item_id text,
|
|
||||||
type text,
|
|
||||||
amount int,
|
|
||||||
primary key(course_id, item_id, type)
|
|
||||||
);''')
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
'''create index best_score_1 on best_score (song_id, difficulty);''') # 排名查询优化
|
|
||||||
c.execute(
|
|
||||||
'''create index download_token_1 on download_token (song_id, file_name);''') # 下载token判断优化
|
|
||||||
|
|
||||||
# 搭档初始化
|
|
||||||
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']
|
|
||||||
|
|
||||||
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_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
|
||||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
|
||||||
|
|
||||||
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0]
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
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, 34, 85]
|
|
||||||
|
|
||||||
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, 34, 86]
|
|
||||||
|
|
||||||
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, 84, 50]
|
|
||||||
|
|
||||||
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, 34, 85]
|
|
||||||
|
|
||||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 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, 34, 86]
|
|
||||||
|
|
||||||
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, 84, 50]
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
char_core = {
|
|
||||||
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
|
||||||
1: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_desolate', 'amount': 25}],
|
|
||||||
2: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_crimson', 'amount': 25}],
|
|
||||||
4: [{'core_id': 'core_ambivalent', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
|
||||||
13: [{'core_id': 'core_scarlet', 'amount': 30}],
|
|
||||||
21: [{'core_id': 'core_scarlet', 'amount': 30}],
|
|
||||||
26: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
27: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
28: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
29: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
36: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
42: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
43: [{'core_id': 'core_chunithm', 'amount': 15}],
|
|
||||||
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
|
|
||||||
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
|
||||||
19: [{'core_id': 'core_colorful', 'amount': 30}]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in range(0, 58):
|
|
||||||
skill_requires_uncap = 1 if i == 2 else 0
|
|
||||||
|
|
||||||
if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43, 11, 12, 19, 5]:
|
|
||||||
sql = '''insert into character values(?,?,30,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1)'''
|
|
||||||
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
|
|
||||||
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
|
|
||||||
else:
|
|
||||||
sql = '''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)'''
|
|
||||||
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
|
|
||||||
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
|
|
||||||
|
|
||||||
c.execute('''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)''', (99,
|
|
||||||
'shirahime', 38, 33, 28, 66, 58, 50, 66, 58, 50, 'frags_preferred_song', 0, 0, '', 0))
|
|
||||||
|
|
||||||
for i in char_core:
|
|
||||||
for j in char_core[i]:
|
|
||||||
c.execute('''insert into char_item values(?,?,'core',?)''',
|
|
||||||
(i, j['core_id'], j['amount']))
|
|
||||||
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']
|
|
||||||
|
|
||||||
for i in cores:
|
|
||||||
c.execute('''insert into item values(?,"core",1,'')''', (i,))
|
|
||||||
|
|
||||||
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']
|
|
||||||
for i in world_songs:
|
|
||||||
c.execute('''insert into item values(?,"world_song",1,'')''', (i,))
|
|
||||||
|
|
||||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
|
||||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
|
|
||||||
for i in world_unlocks:
|
|
||||||
c.execute('''insert into item values(?,"world_unlock",1,'')''', (i,))
|
|
||||||
|
|
||||||
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
|
||||||
for i in course_banners:
|
|
||||||
c.execute('''insert into item values(?,"course_banner",1,'')''', (i,))
|
|
||||||
|
|
||||||
c.execute('''insert into item values(?,?,?,?)''',
|
|
||||||
('fragment', 'fragment', 1, ''))
|
|
||||||
c.execute('''insert into item values(?,?,?,?)''',
|
|
||||||
('memory', 'memory', 1, ''))
|
|
||||||
c.execute('''insert into item values(?,?,?,?)''',
|
|
||||||
('anni5tix', 'anni5tix', 1, ''))
|
|
||||||
|
|
||||||
def insert_items(c, items):
|
|
||||||
# 物品数据导入
|
|
||||||
for i in items:
|
|
||||||
if 'discount_from' not in i:
|
|
||||||
discount_from = -1
|
|
||||||
else:
|
|
||||||
discount_from = i['discount_from']
|
|
||||||
if 'discount_to' not in i:
|
|
||||||
discount_to = -1
|
|
||||||
else:
|
|
||||||
discount_to = i['discount_to']
|
|
||||||
if 'discount_reason' not in i:
|
|
||||||
discount_reason = ''
|
|
||||||
else:
|
|
||||||
discount_reason = i['discount_reason']
|
|
||||||
c.execute('''insert into purchase values(?,?,?,?,?,?)''',
|
|
||||||
(i['name'], i['price'], i['orig_price'], discount_from, discount_to, discount_reason))
|
|
||||||
for j in i['items']:
|
|
||||||
if "_id" not in j:
|
|
||||||
_id = ''
|
|
||||||
else:
|
|
||||||
_id = j['_id']
|
|
||||||
c.execute(
|
|
||||||
'''select exists(select * from item where item_id=?)''', (j['id'],))
|
|
||||||
if c.fetchone() == (0,):
|
|
||||||
c.execute('''insert into item values(?,?,?,?)''',
|
|
||||||
(j['id'], j['type'], j['is_available'], _id))
|
|
||||||
if 'amount' in j:
|
|
||||||
amount = j['amount']
|
|
||||||
else:
|
|
||||||
amount = 1
|
|
||||||
c.execute('''insert into purchase_item values(?,?,?,?)''',
|
|
||||||
(i['name'], j['id'], j['type'], amount))
|
|
||||||
|
|
||||||
# item初始化
|
|
||||||
try:
|
|
||||||
f = open(path+'singles.json', 'r')
|
|
||||||
singles = json.load(f)
|
|
||||||
f.close()
|
|
||||||
insert_items(c, singles)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = open(path+'packs.json', 'r')
|
|
||||||
packs = json.load(f)
|
|
||||||
f.close()
|
|
||||||
insert_items(c, packs)
|
|
||||||
c.execute('''select exists(select * from item where item_id='epilogue')''')
|
|
||||||
if c.fetchone() == (0,):
|
|
||||||
c.execute('''insert into item values('epilogue','pack',1,'')''')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# course初始化
|
|
||||||
try:
|
|
||||||
f = open(path+'courses.json', 'r', encoding="utf-8")
|
|
||||||
courses = json.load(f)
|
|
||||||
f.close()
|
|
||||||
except:
|
|
||||||
courses = []
|
|
||||||
|
|
||||||
if courses:
|
|
||||||
if path == './':
|
|
||||||
# 有什么好办法吗
|
|
||||||
import sys
|
|
||||||
sys.path.append('..')
|
|
||||||
from core.course import Course
|
|
||||||
for i in courses:
|
|
||||||
x = Course(c).from_dict(i)
|
|
||||||
x.insert_all()
|
|
||||||
|
|
||||||
# api权限与权限组初始化
|
|
||||||
role = ['admin', 'user', 'selecter']
|
|
||||||
role_caption = ['管理员', '用户', '查询接口']
|
|
||||||
|
|
||||||
power = ['select', 'select_me', 'change', 'change_me', 'grant',
|
|
||||||
'grant_inf', 'select_song_rank', 'select_song_info']
|
|
||||||
power_caption = ['总体查询权限', '自我查询权限', '总体修改权限',
|
|
||||||
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限']
|
|
||||||
|
|
||||||
role_power = {'0': [0, 1, 3, 5, 6, 7],
|
|
||||||
'1': [1, 3, 6, 7],
|
|
||||||
'2': [0]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in range(0, len(role)):
|
|
||||||
c.execute('''insert into role values(:a,:b,:c)''', {
|
|
||||||
'a': i, 'b': role[i], 'c': role_caption[i]})
|
|
||||||
for i in range(0, len(power)):
|
|
||||||
c.execute('''insert into power values(:a,:b,:c)''', {
|
|
||||||
'a': i, 'b': power[i], 'c': power_caption[i]})
|
|
||||||
for i in role_power:
|
|
||||||
for j in role_power[i]:
|
|
||||||
c.execute('''insert into role_power values(:a,:b)''',
|
|
||||||
{'a': int(i), 'b': j})
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
def arc_register(name: str):
|
|
||||||
def build_user_code(c):
|
|
||||||
return '123456789'
|
|
||||||
|
|
||||||
def build_user_id(c):
|
|
||||||
return 2000000
|
|
||||||
|
|
||||||
def insert_user_char(c, user_id):
|
|
||||||
# 为用户添加可用角色
|
|
||||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
|
||||||
(user_id, 0, 30, 25000, 1, 0))
|
|
||||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
|
||||||
(user_id, 1, 30, 25000, 1, 0))
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
'''select character_id, max_level, is_uncapped from character''')
|
|
||||||
x = c.fetchall()
|
|
||||||
if x:
|
|
||||||
for i in x:
|
|
||||||
exp = 25000 if i[1] == 30 else 10000
|
|
||||||
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
|
|
||||||
(user_id, i[0], i[1], exp, i[2], 0))
|
|
||||||
|
|
||||||
conn = sqlite3.connect(path+'arcaea_database.db')
|
|
||||||
c = conn.cursor()
|
|
||||||
hash_pwd = '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb'
|
|
||||||
c.execute(
|
|
||||||
'''select exists(select * from user where name = :name)''', {'name': name})
|
|
||||||
if c.fetchone() == (0,):
|
|
||||||
user_code = build_user_code(c)
|
|
||||||
user_id = build_user_id(c)
|
|
||||||
now = int(time.time() * 1000)
|
|
||||||
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)
|
|
||||||
values(:user_id, :name, :password, :join_date, :user_code, 1250, 1, 0, 1, 0, 0, -1, 0, '', 114514)
|
|
||||||
''', {'user_code': user_code, 'user_id': user_id, 'join_date': now, 'name': name, 'password': hash_pwd})
|
|
||||||
c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
|
||||||
'user_id': user_id})
|
|
||||||
c.execute(
|
|
||||||
'''insert into best_score values(2000000,'vexaria',3,10000000,100,0,0,0,100,0,1599667200,3,3,10.8)''')
|
|
||||||
insert_user_char(c, user_id)
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return None
|
|
||||||
|
|
||||||
arc_register('admin')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
90
latest version/database/init/arc_data.py
Normal file
90
latest version/database/init/arc_data.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
class InitData:
|
||||||
|
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
|
||||||
|
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', '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']
|
||||||
|
|
||||||
|
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', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin']
|
||||||
|
|
||||||
|
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
||||||
|
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap']
|
||||||
|
|
||||||
|
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, 0, 0]
|
||||||
|
|
||||||
|
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32,
|
||||||
|
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60]
|
||||||
|
|
||||||
|
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, 33, 58, 31, 50, 50]
|
||||||
|
|
||||||
|
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, 33, 58, 31, 50, 34]
|
||||||
|
|
||||||
|
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, 58, 51, 40, 50, 70]
|
||||||
|
|
||||||
|
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, 58, 96, 47, 50, 80]
|
||||||
|
|
||||||
|
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, 59.5, 58, 96, 47, 50, 54]
|
||||||
|
|
||||||
|
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, 58, 51, 40, 50, 80]
|
||||||
|
|
||||||
|
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, 58, 96, 47, 50, 90]
|
||||||
|
|
||||||
|
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, 59.5, 58, 96, 47, 50, 64]
|
||||||
|
|
||||||
|
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, 2, 0, 0, 2]
|
||||||
|
|
||||||
|
char_core = {
|
||||||
|
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||||
|
1: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_desolate', 'amount': 25}],
|
||||||
|
2: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_crimson', 'amount': 25}],
|
||||||
|
4: [{'core_id': 'core_ambivalent', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||||
|
13: [{'core_id': 'core_scarlet', 'amount': 30}],
|
||||||
|
21: [{'core_id': 'core_scarlet', 'amount': 30}],
|
||||||
|
26: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
27: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
28: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
29: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
36: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
42: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
43: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
|
||||||
|
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||||
|
19: [{'core_id': 'core_colorful', 'amount': 30}],
|
||||||
|
10: [{'core_id': 'core_umbral', 'amount': 30}],
|
||||||
|
66: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||||
|
5: [{'core_id': 'core_hollow', 'amount': 0}],
|
||||||
|
73: [{'core_id': 'core_wacca', 'amount': 15}]
|
||||||
|
}
|
||||||
|
|
||||||
|
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_wacca']
|
||||||
|
|
||||||
|
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', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn']
|
||||||
|
|
||||||
|
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||||
|
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
|
||||||
|
|
||||||
|
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
||||||
|
|
||||||
|
role = ['system', 'admin', 'user', 'selecter']
|
||||||
|
role_caption = ['系统', '管理员', '用户', '查询接口']
|
||||||
|
|
||||||
|
power = ['system', 'select', 'select_me', 'change', 'change_me', 'grant',
|
||||||
|
'grant_inf', 'select_song_rank', 'select_song_info', 'select_song_rank_top']
|
||||||
|
power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限',
|
||||||
|
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限', '歌曲排行榜有限查询权限']
|
||||||
|
|
||||||
|
role_power = {'system': power,
|
||||||
|
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank_top', 'select_song_info'],
|
||||||
|
'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'],
|
||||||
|
'selector': ['select']
|
||||||
|
}
|
||||||
30
latest version/database/init/log_tables.sql
Normal file
30
latest version/database/init/log_tables.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
create table if not exists cache(key text primary key,
|
||||||
|
value text,
|
||||||
|
expire_time int
|
||||||
|
);
|
||||||
|
create table if not exists user_score(user_id int,
|
||||||
|
song_id text,
|
||||||
|
difficulty int,
|
||||||
|
time_played int,
|
||||||
|
score int,
|
||||||
|
shiny_perfect_count int,
|
||||||
|
perfect_count int,
|
||||||
|
near_count int,
|
||||||
|
miss_count int,
|
||||||
|
health int,
|
||||||
|
modifier int,
|
||||||
|
clear_type int,
|
||||||
|
rating real,
|
||||||
|
primary key(user_id, song_id, difficulty, time_played)
|
||||||
|
);
|
||||||
|
create table if not exists user_rating(user_id int,
|
||||||
|
time int,
|
||||||
|
rating_ptt real,
|
||||||
|
primary key(user_id, time)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists user_score_1 on user_score (song_id, difficulty);
|
||||||
|
create index if not exists user_score_2 on user_score (time_played);
|
||||||
|
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA default_cache_size = 4000;
|
||||||
@@ -327,11 +327,10 @@
|
|||||||
"is_available": true
|
"is_available": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"price": 0,
|
"price": 700,
|
||||||
"orig_price": 700,
|
"orig_price": 700,
|
||||||
"discount_from": 1646784000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1647388799000,
|
"discount_to": 1647388799000
|
||||||
"discount_reason": "extend"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "alice",
|
"name": "alice",
|
||||||
@@ -595,6 +594,11 @@
|
|||||||
"id": "finale",
|
"id": "finale",
|
||||||
"is_available": true
|
"is_available": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "epilogue",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "core",
|
"type": "core",
|
||||||
"amount": 5,
|
"amount": 5,
|
||||||
@@ -622,5 +626,169 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 500,
|
"orig_price": 500,
|
||||||
"price": 500
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "extend_2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "extend_2",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 7,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 700,
|
||||||
|
"price": 700,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maimai_append_1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "maimai_append_1",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 4,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 400,
|
||||||
|
"price": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "chunithm_append_2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "chunithm_append_2",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "yourvoiceso",
|
"name": "yourvoiceso",
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "crosssoul",
|
"name": "crosssoul",
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "impurebird",
|
"name": "impurebird",
|
||||||
@@ -88,8 +88,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "auxesia",
|
"name": "auxesia",
|
||||||
@@ -128,8 +128,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "yozakurafubuki",
|
"name": "yozakurafubuki",
|
||||||
@@ -148,8 +148,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "surrender",
|
"name": "surrender",
|
||||||
@@ -168,8 +168,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "metallicpunisher",
|
"name": "metallicpunisher",
|
||||||
@@ -224,8 +224,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "callmyname",
|
"name": "callmyname",
|
||||||
@@ -244,8 +244,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "fallensquare",
|
"name": "fallensquare",
|
||||||
@@ -264,8 +264,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dropdead",
|
"name": "dropdead",
|
||||||
@@ -304,8 +304,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "astraltale",
|
"name": "astraltale",
|
||||||
@@ -324,8 +324,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phantasia",
|
"name": "phantasia",
|
||||||
@@ -380,8 +380,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dottodot",
|
"name": "dottodot",
|
||||||
@@ -400,8 +400,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "dreadnought",
|
"name": "dreadnought",
|
||||||
@@ -420,8 +420,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mirzam",
|
"name": "mirzam",
|
||||||
@@ -440,8 +440,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "heavenlycaress",
|
"name": "heavenlycaress",
|
||||||
@@ -460,8 +460,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament",
|
"name": "filament",
|
||||||
@@ -480,8 +480,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "avantraze",
|
"name": "avantraze",
|
||||||
@@ -518,8 +518,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "saikyostronger",
|
"name": "saikyostronger",
|
||||||
@@ -556,8 +556,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "einherjar",
|
"name": "einherjar",
|
||||||
@@ -576,8 +576,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laqryma",
|
"name": "laqryma",
|
||||||
@@ -596,8 +596,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "amygdata",
|
"name": "amygdata",
|
||||||
@@ -616,8 +616,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "altale",
|
"name": "altale",
|
||||||
@@ -636,8 +636,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "feelssoright",
|
"name": "feelssoright",
|
||||||
@@ -656,8 +656,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "scarletcage",
|
"name": "scarletcage",
|
||||||
@@ -712,8 +712,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "badtek",
|
"name": "badtek",
|
||||||
@@ -732,8 +732,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maliciousmischance",
|
"name": "maliciousmischance",
|
||||||
@@ -788,8 +788,8 @@
|
|||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"discount_from": 1615248000000,
|
"discount_from": 1646784000000,
|
||||||
"discount_to": 1615852799000
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "xeraphinite",
|
"name": "xeraphinite",
|
||||||
@@ -807,7 +807,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "xanatos",
|
"name": "xanatos",
|
||||||
@@ -825,7 +827,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"price": 100,
|
"price": 100,
|
||||||
"orig_price": 100
|
"orig_price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "attraqtia",
|
"name": "attraqtia",
|
||||||
@@ -843,7 +847,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gimmedablood",
|
"name": "gimmedablood",
|
||||||
@@ -879,7 +885,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "theultimacy",
|
"name": "theultimacy",
|
||||||
@@ -897,7 +905,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "rekkaresonanc",
|
"name": "rekkaresonanc",
|
||||||
@@ -945,7 +955,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "eveninginscarlet",
|
"name": "eveninginscarlet",
|
||||||
@@ -981,7 +993,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "goldenslaughterer",
|
"name": "goldenslaughterer",
|
||||||
@@ -999,7 +1013,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "redolentshape",
|
"name": "redolentshape",
|
||||||
@@ -1017,7 +1033,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100,
|
||||||
|
"discount_from": 1646784000000,
|
||||||
|
"discount_to": 1647388799000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "summerfireworks",
|
"name": "summerfireworks",
|
||||||
@@ -1056,29 +1074,11 @@
|
|||||||
"price": 100
|
"price": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "internetoverdose",
|
"name": "kissinglucifer",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "single",
|
"type": "single",
|
||||||
"id": "internetoverdose",
|
"id": "kissinglucifer",
|
||||||
"is_available": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "core",
|
|
||||||
"amount": 1,
|
|
||||||
"id": "core_generic",
|
|
||||||
"is_available": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"orig_price": 100,
|
|
||||||
"price": 100
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "macromod",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "single",
|
|
||||||
"id": "macromod",
|
|
||||||
"is_available": true
|
"is_available": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1110,11 +1110,11 @@
|
|||||||
"price": 100
|
"price": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kissinglucifer",
|
"name": "macromod",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "single",
|
"type": "single",
|
||||||
"id": "kissinglucifer",
|
"id": "macromod",
|
||||||
"is_available": true
|
"is_available": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1128,11 +1128,11 @@
|
|||||||
"price": 100
|
"price": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "headbonkache",
|
"name": "internetoverdose",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "single",
|
"type": "single",
|
||||||
"id": "headbonkache",
|
"id": "internetoverdose",
|
||||||
"is_available": true
|
"is_available": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1163,6 +1163,24 @@
|
|||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "headbonkache",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "headbonkache",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "picopicotranslation",
|
"name": "picopicotranslation",
|
||||||
"items": [
|
"items": [
|
||||||
@@ -1216,5 +1234,329 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 100,
|
"orig_price": 100,
|
||||||
"price": 100
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cocorocosmetic",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "cocorocosmetic",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "freemyself",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "freemyself",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "capella",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "capella",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nullapophenia",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "nullapophenia",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hiirogekka",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "hiirogekka",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "manicjeer",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "manicjeer",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "crimsonthrone",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "crimsonthrone",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "primitivelights",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "primitivelights",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "thesurvivor",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "thesurvivor",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "newyorkbackraise",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "newyorkbackraise",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lostintheabyss",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "lostintheabyss",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "galacticlove",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "galacticlove",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 100,
|
||||||
|
"price": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tothemilkyway",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "single",
|
||||||
|
"id": "tothemilkyway",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 1,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
317
latest version/database/init/tables.sql
Normal file
317
latest version/database/init/tables.sql
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
create table if not exists config(id text primary key, value text);
|
||||||
|
create table if not exists user(user_id int primary key,
|
||||||
|
name text unique,
|
||||||
|
password text,
|
||||||
|
join_date char(20),
|
||||||
|
user_code char(10),
|
||||||
|
rating_ptt int,
|
||||||
|
character_id int,
|
||||||
|
is_skill_sealed int,
|
||||||
|
is_char_uncapped int,
|
||||||
|
is_char_uncapped_override int,
|
||||||
|
is_hide_rating int,
|
||||||
|
song_id text,
|
||||||
|
difficulty int,
|
||||||
|
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,
|
||||||
|
rating real,
|
||||||
|
favorite_character int,
|
||||||
|
max_stamina_notification_enabled int,
|
||||||
|
current_map text,
|
||||||
|
ticket int,
|
||||||
|
prog_boost int,
|
||||||
|
email text,
|
||||||
|
world_rank_score int,
|
||||||
|
ban_flag text,
|
||||||
|
next_fragstam_ts int,
|
||||||
|
max_stamina_ts int,
|
||||||
|
stamina int,
|
||||||
|
world_mode_locked_end_ts int,
|
||||||
|
beyond_boost_gauge real default 0
|
||||||
|
);
|
||||||
|
create table if not exists login(access_token text,
|
||||||
|
user_id int,
|
||||||
|
login_time int,
|
||||||
|
login_ip text,
|
||||||
|
login_device text,
|
||||||
|
primary key(access_token, user_id)
|
||||||
|
);
|
||||||
|
create table if not exists friend(user_id_me int,
|
||||||
|
user_id_other int,
|
||||||
|
primary key (user_id_me, user_id_other)
|
||||||
|
);
|
||||||
|
create table if not exists best_score(user_id int,
|
||||||
|
song_id text,
|
||||||
|
difficulty int,
|
||||||
|
score int,
|
||||||
|
shiny_perfect_count int,
|
||||||
|
perfect_count int,
|
||||||
|
near_count int,
|
||||||
|
miss_count int,
|
||||||
|
health int,
|
||||||
|
modifier int,
|
||||||
|
time_played int,
|
||||||
|
best_clear_type int,
|
||||||
|
clear_type int,
|
||||||
|
rating real,
|
||||||
|
primary key(user_id, song_id, difficulty)
|
||||||
|
);
|
||||||
|
create table if not exists user_char(user_id int,
|
||||||
|
character_id int,
|
||||||
|
level int,
|
||||||
|
exp real,
|
||||||
|
is_uncapped int,
|
||||||
|
is_uncapped_override int,
|
||||||
|
skill_flag int,
|
||||||
|
primary key(user_id, character_id)
|
||||||
|
);
|
||||||
|
create table if not exists user_char_full(user_id int,
|
||||||
|
character_id int,
|
||||||
|
level int,
|
||||||
|
exp real,
|
||||||
|
is_uncapped int,
|
||||||
|
is_uncapped_override int,
|
||||||
|
skill_flag int,
|
||||||
|
primary key(user_id, character_id)
|
||||||
|
);
|
||||||
|
create table if not exists character(character_id int primary key,
|
||||||
|
name text,
|
||||||
|
max_level int,
|
||||||
|
frag1 real,
|
||||||
|
prog1 real,
|
||||||
|
overdrive1 real,
|
||||||
|
frag20 real,
|
||||||
|
prog20 real,
|
||||||
|
overdrive20 real,
|
||||||
|
frag30 real,
|
||||||
|
prog30 real,
|
||||||
|
overdrive30 real,
|
||||||
|
skill_id text,
|
||||||
|
skill_unlock_level int,
|
||||||
|
skill_requires_uncap int,
|
||||||
|
skill_id_uncap text,
|
||||||
|
char_type int,
|
||||||
|
is_uncapped int
|
||||||
|
);
|
||||||
|
create table if not exists char_item(character_id int,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(character_id, item_id, type)
|
||||||
|
);
|
||||||
|
create table if not exists recent30(user_id int primary key,
|
||||||
|
r0 real,
|
||||||
|
song_id0 text,
|
||||||
|
r1 real,
|
||||||
|
song_id1 text,
|
||||||
|
r2 real,
|
||||||
|
song_id2 text,
|
||||||
|
r3 real,
|
||||||
|
song_id3 text,
|
||||||
|
r4 real,
|
||||||
|
song_id4 text,
|
||||||
|
r5 real,
|
||||||
|
song_id5 text,
|
||||||
|
r6 real,
|
||||||
|
song_id6 text,
|
||||||
|
r7 real,
|
||||||
|
song_id7 text,
|
||||||
|
r8 real,
|
||||||
|
song_id8 text,
|
||||||
|
r9 real,
|
||||||
|
song_id9 text,
|
||||||
|
r10 real,
|
||||||
|
song_id10 text,
|
||||||
|
r11 real,
|
||||||
|
song_id11 text,
|
||||||
|
r12 real,
|
||||||
|
song_id12 text,
|
||||||
|
r13 real,
|
||||||
|
song_id13 text,
|
||||||
|
r14 real,
|
||||||
|
song_id14 text,
|
||||||
|
r15 real,
|
||||||
|
song_id15 text,
|
||||||
|
r16 real,
|
||||||
|
song_id16 text,
|
||||||
|
r17 real,
|
||||||
|
song_id17 text,
|
||||||
|
r18 real,
|
||||||
|
song_id18 text,
|
||||||
|
r19 real,
|
||||||
|
song_id19 text,
|
||||||
|
r20 real,
|
||||||
|
song_id20 text,
|
||||||
|
r21 real,
|
||||||
|
song_id21 text,
|
||||||
|
r22 real,
|
||||||
|
song_id22 text,
|
||||||
|
r23 real,
|
||||||
|
song_id23 text,
|
||||||
|
r24 real,
|
||||||
|
song_id24 text,
|
||||||
|
r25 real,
|
||||||
|
song_id25 text,
|
||||||
|
r26 real,
|
||||||
|
song_id26 text,
|
||||||
|
r27 real,
|
||||||
|
song_id27 text,
|
||||||
|
r28 real,
|
||||||
|
song_id28 text,
|
||||||
|
r29 real,
|
||||||
|
song_id29 text
|
||||||
|
);
|
||||||
|
create table if not exists user_world(user_id int,
|
||||||
|
map_id text,
|
||||||
|
curr_position int,
|
||||||
|
curr_capture real,
|
||||||
|
is_locked int,
|
||||||
|
primary key(user_id, map_id)
|
||||||
|
);
|
||||||
|
create table if not exists songplay_token(token text primary key,
|
||||||
|
user_id int,
|
||||||
|
song_id text,
|
||||||
|
difficulty int,
|
||||||
|
course_id text,
|
||||||
|
course_state int,
|
||||||
|
course_score int,
|
||||||
|
course_clear_type int,
|
||||||
|
stamina_multiply int,
|
||||||
|
fragment_multiply int,
|
||||||
|
prog_boost_multiply int,
|
||||||
|
beyond_boost_gauge_usage int,
|
||||||
|
skill_cytusii_flag text
|
||||||
|
);
|
||||||
|
create table if not exists item(item_id text,
|
||||||
|
type text,
|
||||||
|
is_available int,
|
||||||
|
primary key(item_id, type)
|
||||||
|
);
|
||||||
|
create table if not exists user_item(user_id int,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(user_id, item_id, type)
|
||||||
|
);
|
||||||
|
create table if not exists purchase(purchase_name text primary key,
|
||||||
|
price int,
|
||||||
|
orig_price int,
|
||||||
|
discount_from int,
|
||||||
|
discount_to int,
|
||||||
|
discount_reason text
|
||||||
|
);
|
||||||
|
create table if not exists purchase_item(purchase_name text,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(purchase_name, item_id, type)
|
||||||
|
);
|
||||||
|
create table if not exists user_save(user_id int primary key,
|
||||||
|
scores_data text,
|
||||||
|
clearlamps_data text,
|
||||||
|
clearedsongs_data text,
|
||||||
|
unlocklist_data text,
|
||||||
|
installid_data text,
|
||||||
|
devicemodelname_data text,
|
||||||
|
story_data text,
|
||||||
|
createdAt int,
|
||||||
|
finalestate_data text
|
||||||
|
);
|
||||||
|
create table if not exists present(present_id text primary key,
|
||||||
|
expire_ts int,
|
||||||
|
description text
|
||||||
|
);
|
||||||
|
create table if not exists user_present(user_id int,
|
||||||
|
present_id text,
|
||||||
|
primary key(user_id, present_id)
|
||||||
|
);
|
||||||
|
create table if not exists present_item(present_id text,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(present_id, item_id, type)
|
||||||
|
);
|
||||||
|
create table if not exists chart(song_id text primary key,
|
||||||
|
name text,
|
||||||
|
rating_pst int,
|
||||||
|
rating_prs int,
|
||||||
|
rating_ftr int,
|
||||||
|
rating_byn int
|
||||||
|
);
|
||||||
|
create table if not exists redeem(code text primary key,
|
||||||
|
type int
|
||||||
|
);
|
||||||
|
create table if not exists user_redeem(user_id int,
|
||||||
|
code text,
|
||||||
|
primary key(user_id, code)
|
||||||
|
);
|
||||||
|
create table if not exists redeem_item(code text,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(code, item_id, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists role(role_id text primary key,
|
||||||
|
caption text
|
||||||
|
);
|
||||||
|
create table if not exists user_role(user_id int,
|
||||||
|
role_id text,
|
||||||
|
primary key(user_id, role_id)
|
||||||
|
);
|
||||||
|
create table if not exists power(power_id text primary key,
|
||||||
|
caption text
|
||||||
|
);
|
||||||
|
create table if not exists role_power(role_id text,
|
||||||
|
power_id text,
|
||||||
|
primary key(role_id, power_id)
|
||||||
|
);
|
||||||
|
create table if not exists api_login(user_id int,
|
||||||
|
token text,
|
||||||
|
login_time int,
|
||||||
|
login_ip text,
|
||||||
|
primary key(user_id, token)
|
||||||
|
);
|
||||||
|
create table if not exists course(course_id text primary key,
|
||||||
|
course_name text,
|
||||||
|
dan_name text,
|
||||||
|
style int,
|
||||||
|
gauge_requirement text,
|
||||||
|
flag_as_hidden_when_requirements_not_met int,
|
||||||
|
can_start int
|
||||||
|
);
|
||||||
|
create table if not exists user_course(user_id int,
|
||||||
|
course_id text,
|
||||||
|
high_score int,
|
||||||
|
best_clear_type int,
|
||||||
|
primary key(user_id, course_id)
|
||||||
|
);
|
||||||
|
create table if not exists course_chart(course_id text,
|
||||||
|
song_id text,
|
||||||
|
difficulty int,
|
||||||
|
flag_as_hidden int,
|
||||||
|
song_index int,
|
||||||
|
primary key(course_id, song_index)
|
||||||
|
);
|
||||||
|
create table if not exists course_requirement(course_id text,
|
||||||
|
required_id text,
|
||||||
|
primary key(course_id, required_id)
|
||||||
|
);
|
||||||
|
create table if not exists course_item(course_id text,
|
||||||
|
item_id text,
|
||||||
|
type text,
|
||||||
|
amount int,
|
||||||
|
primary key(course_id, item_id, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists best_score_1 on best_score (song_id, difficulty);
|
||||||
|
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA default_cache_size = 8000;
|
||||||
@@ -2,9 +2,24 @@
|
|||||||
"songs": [
|
"songs": [
|
||||||
{
|
{
|
||||||
"id": "dement",
|
"id": "dement",
|
||||||
|
"set": "base",
|
||||||
|
"purchase": "",
|
||||||
"difficulties": [
|
"difficulties": [
|
||||||
{
|
{
|
||||||
"ratingClass": 3
|
"ratingClass": 0,
|
||||||
|
"rating": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 1,
|
||||||
|
"rating": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 2,
|
||||||
|
"rating": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingClass": 3,
|
||||||
|
"rating": 9
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,22 @@ 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
|
||||||
|
|
||||||
|
TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||||
|
|
||||||
TIME_LIMIT = 3600000
|
TIME_LIMIT = 3600000
|
||||||
|
|
||||||
COMMAND_INTERVAL = 1000000
|
COMMAND_INTERVAL = 1000000
|
||||||
|
|||||||
@@ -1,257 +1,107 @@
|
|||||||
import base64
|
# import binascii
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
import time
|
from json import dumps, loads
|
||||||
from os import urandom
|
|
||||||
|
|
||||||
# import binascii
|
|
||||||
from .aes import decrypt, encrypt
|
from .aes import decrypt, encrypt
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .udp_class import Player, Room, bi
|
from .store import Store, TCPRouter, clear_player, clear_room
|
||||||
|
from .udp_class import bi
|
||||||
from .udp_parser import CommandParser
|
from .udp_parser import CommandParser
|
||||||
|
|
||||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
|
||||||
link_play_data = {}
|
|
||||||
room_id_dict = {} # 'room_id': Room
|
|
||||||
room_code_dict = {} # 'room_code': Room
|
|
||||||
player_dict = {} # 'player_id' : Player
|
|
||||||
clean_timer = 0
|
|
||||||
lock = threading.RLock()
|
|
||||||
|
|
||||||
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||||
level=logging.INFO)
|
level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def random_room_code():
|
|
||||||
# 随机生成房间号
|
|
||||||
re = ''
|
|
||||||
for _ in range(4):
|
|
||||||
re += chr(random.randint(65, 90))
|
|
||||||
for _ in range(2):
|
|
||||||
re += str(random.randint(0, 9))
|
|
||||||
|
|
||||||
return re
|
|
||||||
|
|
||||||
|
|
||||||
def unique_random(dataset, length=8, random_func=None):
|
|
||||||
'''无重复随机,且默认非0'''
|
|
||||||
if random_func is None:
|
|
||||||
x = bi(urandom(length))
|
|
||||||
while x in dataset or x == 0:
|
|
||||||
x = bi(urandom(length))
|
|
||||||
else:
|
|
||||||
x = random_func()
|
|
||||||
while x in dataset:
|
|
||||||
x = random_func()
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
def clear_player(token):
|
|
||||||
# 清除玩家信息和token
|
|
||||||
del player_dict[link_play_data[token]['player_id']]
|
|
||||||
del link_play_data[token]
|
|
||||||
|
|
||||||
|
|
||||||
def clear_room(room):
|
|
||||||
# 清除房间信息
|
|
||||||
room_id = room.room_id
|
|
||||||
room_code = room.room_code
|
|
||||||
del room_id_dict[room_id]
|
|
||||||
del room_code_dict[room_code]
|
|
||||||
del room
|
|
||||||
|
|
||||||
|
|
||||||
def memory_clean(now):
|
|
||||||
# 内存清理
|
|
||||||
with lock:
|
|
||||||
clean_room_list = []
|
|
||||||
clean_player_list = []
|
|
||||||
for token in link_play_data:
|
|
||||||
room = link_play_data[token]['room']
|
|
||||||
if now - room.timestamp >= Config.TIME_LIMIT:
|
|
||||||
clean_room_list.append(room.room_id)
|
|
||||||
|
|
||||||
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
|
|
||||||
clean_player_list.append(token)
|
|
||||||
|
|
||||||
for room_id in room_id_dict:
|
|
||||||
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
|
|
||||||
clean_room_list.append(room_id)
|
|
||||||
|
|
||||||
for room_id in clean_room_list:
|
|
||||||
if room_id in room_id_dict:
|
|
||||||
clear_room(room_id_dict[room_id])
|
|
||||||
|
|
||||||
for token in clean_player_list:
|
|
||||||
clear_player(token)
|
|
||||||
|
|
||||||
|
|
||||||
class UDP_handler(socketserver.BaseRequestHandler):
|
class UDP_handler(socketserver.BaseRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
client_msg, server = self.request
|
client_msg, server = self.request
|
||||||
|
# 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:32]
|
||||||
ciphertext = client_msg[32:]
|
ciphertext = client_msg[32:]
|
||||||
if int.from_bytes(token, byteorder='little') in link_play_data:
|
if bi(token) not in Store.link_play_data:
|
||||||
user = link_play_data[bi(token)]
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
user = Store.link_play_data[bi(token)]
|
||||||
|
|
||||||
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
return None
|
return None
|
||||||
# print(binascii.b2a_hex(plaintext))
|
|
||||||
|
# if Config.DEBUG:
|
||||||
|
# logging.info(
|
||||||
|
# f'UDP-From-{self.client_address[0]}-{binascii.b2a_hex(plaintext)}')
|
||||||
|
|
||||||
commands = CommandParser(
|
commands = CommandParser(
|
||||||
user['room'], user['player_index']).get_commands(plaintext)
|
user['room'], user['player_index']).get_commands(plaintext)
|
||||||
|
|
||||||
if user['room'].players[user['player_index']].player_id == 0:
|
if user['room'].players[user['player_index']].player_id == 0:
|
||||||
clear_player(bi(token))
|
clear_player(bi(token))
|
||||||
temp = []
|
if user['room'].player_num == 0:
|
||||||
for i in commands:
|
clear_room(user['room'])
|
||||||
if i[:3] == b'\x06\x16\x12':
|
commands = [i for i in commands if i[2] == 0x12]
|
||||||
temp.append(i)
|
|
||||||
commands = temp
|
|
||||||
# 处理不能正确被踢的问题
|
# 处理不能正确被踢的问题
|
||||||
|
|
||||||
for i in commands:
|
for i in commands:
|
||||||
iv, ciphertext, tag = encrypt(user['key'], i, b'')
|
iv, ciphertext, tag = encrypt(user['key'], i, b'')
|
||||||
# print(binascii.b2a_hex(i))
|
# if Config.DEBUG:
|
||||||
|
# logging.info(
|
||||||
|
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
||||||
|
|
||||||
server.sendto(token + iv + tag[:12] +
|
server.sendto(token + iv + tag[:12] +
|
||||||
ciphertext, self.client_address)
|
ciphertext, self.client_address)
|
||||||
|
|
||||||
|
|
||||||
class TCP_handler(socketserver.StreamRequestHandler):
|
AUTH_LEN = len(Config.AUTHENTICATION)
|
||||||
def handle(self):
|
TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16]
|
||||||
self.data = self.rfile.readline().strip()
|
|
||||||
|
|
||||||
message = self.data.decode('utf-8')
|
|
||||||
# print(message)
|
class TCP_handler(socketserver.StreamRequestHandler):
|
||||||
data = message.split('|')
|
|
||||||
if data[0] != Config.AUTHENTICATION:
|
def handle(self):
|
||||||
|
try:
|
||||||
|
if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
|
||||||
self.wfile.write(b'No authentication')
|
self.wfile.write(b'No authentication')
|
||||||
logging.warning('TCP-%s-No authentication' %
|
logging.warning(
|
||||||
self.client_address[0])
|
f'TCP-{self.client_address[0]}-No authentication')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
global clean_timer
|
cipher_len = int.from_bytes(self.rfile.read(8), byteorder='little')
|
||||||
now = round(time.time() * 1000)
|
if cipher_len > Config.TCP_MAX_LENGTH:
|
||||||
if now - clean_timer >= Config.TIME_LIMIT:
|
self.wfile.write(b'Body too long')
|
||||||
logging.info('Start cleaning memory...')
|
logging.warning(f'TCP-{self.client_address[0]}-Body too long')
|
||||||
clean_timer = now
|
return None
|
||||||
memory_clean(now)
|
|
||||||
|
|
||||||
self.wfile.write(data_swap(data[1:]).encode('utf-8'))
|
iv = self.rfile.read(12)
|
||||||
|
tag = self.rfile.read(12)
|
||||||
|
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
|
||||||
|
|
||||||
def data_swap(data: list) -> str:
|
if Config.DEBUG:
|
||||||
# data: list[str] = [command, ...]
|
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
|
||||||
if data[0] == '1':
|
|
||||||
# 开房
|
|
||||||
# data = ['1', name, song_unlock, ]
|
|
||||||
# song_unlock: base64 str
|
|
||||||
name = data[1]
|
|
||||||
song_unlock = base64.b64decode(data[2])
|
|
||||||
|
|
||||||
key = urandom(16)
|
r = TCPRouter(data).handle()
|
||||||
with lock:
|
try:
|
||||||
room_id = unique_random(room_id_dict)
|
r = dumps(r)
|
||||||
|
if Config.DEBUG:
|
||||||
room = Room()
|
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
|
||||||
room.room_id = room_id
|
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
|
||||||
room_id_dict[room_id] = room
|
r = len(ciphertext).to_bytes(8, byteorder='little') + \
|
||||||
|
iv + tag[:12] + ciphertext
|
||||||
player_id = unique_random(player_dict, 3)
|
except Exception as e:
|
||||||
player = Player()
|
logging.error(e)
|
||||||
player.player_id = player_id
|
return None
|
||||||
player.set_player_name(name)
|
self.wfile.write(r)
|
||||||
player_dict[player_id] = player
|
|
||||||
|
|
||||||
player.song_unlock = song_unlock
|
|
||||||
room.song_unlock = song_unlock
|
|
||||||
room.host_id = player_id
|
|
||||||
room.players[0] = player
|
|
||||||
room.player_num = 1
|
|
||||||
|
|
||||||
room_code = unique_random(
|
|
||||||
room_code_dict, random_func=random_room_code)
|
|
||||||
room.room_code = room_code
|
|
||||||
room_code_dict[room_code] = room
|
|
||||||
|
|
||||||
token = room_id
|
|
||||||
player.token = token
|
|
||||||
|
|
||||||
link_play_data[token] = {'key': key,
|
|
||||||
'room': room,
|
|
||||||
'player_index': 0,
|
|
||||||
'player_id': player_id}
|
|
||||||
|
|
||||||
logging.info('TCP-Create room `%s` by player `%s`' % (room_code, name))
|
|
||||||
return '|'.join([str(x) for x in (0, room_code, room_id, token, base64.b64encode(key).decode('utf-8'), player_id)])
|
|
||||||
|
|
||||||
elif data[0] == '2':
|
|
||||||
# 入房
|
|
||||||
# data = ['2', name, song_unlock, room_code]
|
|
||||||
# song_unlock: base64 str
|
|
||||||
room_code = data[3].upper()
|
|
||||||
with lock:
|
|
||||||
if room_code not in room_code_dict:
|
|
||||||
# 房间号错误
|
|
||||||
return '1202'
|
|
||||||
room = room_code_dict[room_code]
|
|
||||||
|
|
||||||
if room.player_num == 4:
|
|
||||||
# 满人
|
|
||||||
return '1201'
|
|
||||||
elif room.state != 2:
|
|
||||||
# 无法加入
|
|
||||||
return '1205'
|
|
||||||
|
|
||||||
name = data[1]
|
|
||||||
song_unlock = base64.b64decode(data[2])
|
|
||||||
|
|
||||||
key = urandom(16)
|
|
||||||
with lock:
|
|
||||||
token = unique_random(link_play_data)
|
|
||||||
player_id = unique_random(player_dict, 3)
|
|
||||||
|
|
||||||
player = Player()
|
|
||||||
player.player_id = player_id
|
|
||||||
player.set_player_name(name)
|
|
||||||
player.token = token
|
|
||||||
player_dict[player_id] = player
|
|
||||||
player.song_unlock = song_unlock
|
|
||||||
room.update_song_unlock()
|
|
||||||
for i in range(4):
|
|
||||||
if room.players[i].player_id == 0:
|
|
||||||
room.players[i] = player
|
|
||||||
player_index = i
|
|
||||||
break
|
|
||||||
link_play_data[token] = {'key': key,
|
|
||||||
'room': room,
|
|
||||||
'player_index': player_index,
|
|
||||||
'player_id': player_id}
|
|
||||||
|
|
||||||
logging.info('TCP-Player `%s` joins room `%s`' % (name, room_code))
|
|
||||||
return '|'.join([str(x) for x in (0, room_code, room.room_id, token, base64.b64encode(key).decode('utf-8'), player_id, base64.b64encode(room.song_unlock).decode('utf-8'))])
|
|
||||||
|
|
||||||
elif data[0] == '3':
|
|
||||||
# 房间信息更新
|
|
||||||
# data = ['3', token]
|
|
||||||
token = int(data[1])
|
|
||||||
with lock:
|
|
||||||
if token in link_play_data:
|
|
||||||
r = link_play_data[token]
|
|
||||||
logging.info('TCP-Room `%s` info update' % room_code)
|
|
||||||
return '|'.join([str(x) for x in (0, r['room'].room_code, r['room'].room_id, base64.b64encode(r['key']).decode('utf-8'), r['room'].players[r['player_index']].player_id, base64.b64encode(r['room'].song_unlock).decode('utf-8'))])
|
|
||||||
else:
|
|
||||||
return '108'
|
|
||||||
|
|
||||||
|
|
||||||
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):
|
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):
|
||||||
|
|||||||
302
latest version/linkplay_server/store.py
Normal file
302
latest version/linkplay_server/store.py
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import logging
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from os import urandom
|
||||||
|
from random import randint
|
||||||
|
from threading import RLock
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .udp_class import Player, Room, bi
|
||||||
|
|
||||||
|
|
||||||
|
class Store:
|
||||||
|
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||||
|
link_play_data = {}
|
||||||
|
room_id_dict = {} # 'room_id': Room
|
||||||
|
room_code_dict = {} # 'room_code': Room
|
||||||
|
player_dict = {} # 'player_id' : Player
|
||||||
|
|
||||||
|
lock = RLock()
|
||||||
|
|
||||||
|
|
||||||
|
def random_room_code():
|
||||||
|
re = ''
|
||||||
|
for _ in range(4):
|
||||||
|
re += chr(randint(65, 90))
|
||||||
|
for _ in range(2):
|
||||||
|
re += str(randint(0, 9))
|
||||||
|
return re
|
||||||
|
|
||||||
|
|
||||||
|
def unique_random(dataset, length=8, random_func=None):
|
||||||
|
'''无重复随机,且默认非0,没处理可能的死循环'''
|
||||||
|
if random_func is None:
|
||||||
|
x = bi(urandom(length))
|
||||||
|
while x in dataset or x == 0:
|
||||||
|
x = bi(urandom(length))
|
||||||
|
else:
|
||||||
|
x = random_func()
|
||||||
|
while x in dataset:
|
||||||
|
x = random_func()
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def clear_player(token):
|
||||||
|
# 清除玩家信息和token
|
||||||
|
player_id = Store.link_play_data[token]['player_id']
|
||||||
|
logging.info(f'Clean player `{Store.player_dict[player_id].name}`')
|
||||||
|
del Store.player_dict[player_id]
|
||||||
|
del Store.link_play_data[token]
|
||||||
|
|
||||||
|
|
||||||
|
def clear_room(room):
|
||||||
|
# 清除房间信息
|
||||||
|
room_id = room.room_id
|
||||||
|
room_code = room.room_code
|
||||||
|
logging.info(f'Clean room `{room_code}`')
|
||||||
|
del Store.room_id_dict[room_id]
|
||||||
|
del Store.room_code_dict[room_code]
|
||||||
|
del room
|
||||||
|
|
||||||
|
|
||||||
|
def memory_clean(now):
|
||||||
|
# 内存清理,应对玩家不正常退出
|
||||||
|
with Store.lock:
|
||||||
|
clean_room_list = []
|
||||||
|
clean_player_list = []
|
||||||
|
for token, v in Store.link_play_data.items():
|
||||||
|
room = v['room']
|
||||||
|
if now - room.timestamp >= Config.TIME_LIMIT:
|
||||||
|
clean_room_list.append(room.room_id)
|
||||||
|
|
||||||
|
if now - room.players[v['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
|
||||||
|
clean_player_list.append(token)
|
||||||
|
|
||||||
|
for room_id, v in Store.room_id_dict.items():
|
||||||
|
if now - v.timestamp >= Config.TIME_LIMIT:
|
||||||
|
clean_room_list.append(room_id)
|
||||||
|
|
||||||
|
for room_id in clean_room_list:
|
||||||
|
if room_id in Store.room_id_dict:
|
||||||
|
clear_room(Store.room_id_dict[room_id])
|
||||||
|
|
||||||
|
for token in clean_player_list:
|
||||||
|
clear_player(token)
|
||||||
|
|
||||||
|
|
||||||
|
class TCPRouter:
|
||||||
|
clean_timer = 0
|
||||||
|
router = {
|
||||||
|
'debug',
|
||||||
|
'create_room',
|
||||||
|
'join_room',
|
||||||
|
'update_room',
|
||||||
|
'get_rooms',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, raw_data: 'dict | list'):
|
||||||
|
self.raw_data = raw_data # data: dict {endpoint: str, data: dict}
|
||||||
|
self.data = raw_data['data']
|
||||||
|
self.endpoint = raw_data['endpoint']
|
||||||
|
|
||||||
|
def debug(self) -> dict:
|
||||||
|
if Config.DEBUG:
|
||||||
|
return {'result': eval(self.data['code'])}
|
||||||
|
return {'hello_world': 'ok'}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_check():
|
||||||
|
now = round(time() * 1000)
|
||||||
|
if now - TCPRouter.clean_timer >= Config.TIME_LIMIT:
|
||||||
|
logging.info('Start cleaning memory...')
|
||||||
|
TCPRouter.clean_timer = now
|
||||||
|
memory_clean(now)
|
||||||
|
|
||||||
|
def handle(self) -> dict:
|
||||||
|
self.clean_check()
|
||||||
|
if self.endpoint not in self.router:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
r = getattr(self, self.endpoint)()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
return 999
|
||||||
|
if isinstance(r, int):
|
||||||
|
return {'code': r}
|
||||||
|
return {
|
||||||
|
'code': 0,
|
||||||
|
'data': r
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_player(name: str) -> Player:
|
||||||
|
player_id = unique_random(Store.player_dict, 3)
|
||||||
|
player = Player()
|
||||||
|
player.player_id = player_id
|
||||||
|
player.set_player_name(name)
|
||||||
|
|
||||||
|
Store.player_dict[player_id] = player
|
||||||
|
|
||||||
|
return player
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_room() -> Room:
|
||||||
|
room_id = unique_random(Store.room_id_dict)
|
||||||
|
room = Room()
|
||||||
|
room.room_id = room_id
|
||||||
|
room.timestamp = round(time() * 1000)
|
||||||
|
Store.room_id_dict[room_id] = room
|
||||||
|
|
||||||
|
room_code = unique_random(
|
||||||
|
Store.room_code_dict, random_func=random_room_code)
|
||||||
|
room.room_code = room_code
|
||||||
|
Store.room_code_dict[room_code] = room
|
||||||
|
|
||||||
|
return room
|
||||||
|
|
||||||
|
def create_room(self) -> dict:
|
||||||
|
# 开房
|
||||||
|
# data = ['1', name, song_unlock, ]
|
||||||
|
# song_unlock: base64 str
|
||||||
|
name = self.data['name']
|
||||||
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
|
||||||
|
key = urandom(16)
|
||||||
|
with Store.lock:
|
||||||
|
room = self.generate_room()
|
||||||
|
player = self.generate_player(name)
|
||||||
|
|
||||||
|
player.song_unlock = song_unlock
|
||||||
|
room.song_unlock = song_unlock
|
||||||
|
room.host_id = player.player_id
|
||||||
|
room.players[0] = player
|
||||||
|
|
||||||
|
token = room.room_id
|
||||||
|
player.token = token
|
||||||
|
|
||||||
|
Store.link_play_data[token] = {
|
||||||
|
'key': key,
|
||||||
|
'room': room,
|
||||||
|
'player_index': 0,
|
||||||
|
'player_id': player.player_id
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`')
|
||||||
|
return {
|
||||||
|
'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) -> 'dict | int':
|
||||||
|
# 入房
|
||||||
|
# data = ['2', name, song_unlock, room_code]
|
||||||
|
# song_unlock: base64 str
|
||||||
|
room_code = self.data['room_code'].upper()
|
||||||
|
key = urandom(16)
|
||||||
|
name = self.data['name']
|
||||||
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
|
||||||
|
with Store.lock:
|
||||||
|
if room_code not in Store.room_code_dict:
|
||||||
|
# 房间号错误 / 房间不存在
|
||||||
|
return 1202
|
||||||
|
room: Room = Store.room_code_dict[room_code]
|
||||||
|
|
||||||
|
player_num = room.player_num
|
||||||
|
if player_num == 4:
|
||||||
|
# 满人
|
||||||
|
return 1201
|
||||||
|
if player_num == 0:
|
||||||
|
# 房间不存在
|
||||||
|
return 1202
|
||||||
|
if room.state != 2:
|
||||||
|
# 无法加入
|
||||||
|
return 1205
|
||||||
|
|
||||||
|
token = unique_random(Store.link_play_data)
|
||||||
|
|
||||||
|
player = self.generate_player(name)
|
||||||
|
player.token = token
|
||||||
|
player.song_unlock = song_unlock
|
||||||
|
room.update_song_unlock()
|
||||||
|
for i in range(4):
|
||||||
|
if room.players[i].player_id == 0:
|
||||||
|
room.players[i] = player
|
||||||
|
player_index = i
|
||||||
|
break
|
||||||
|
Store.link_play_data[token] = {
|
||||||
|
'key': key,
|
||||||
|
'room': room,
|
||||||
|
'player_index': player_index,
|
||||||
|
'player_id': player.player_id
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f'TCP-Player `{name}` joins room `{room_code}`')
|
||||||
|
return {
|
||||||
|
'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) -> dict:
|
||||||
|
# 房间信息更新
|
||||||
|
# data = ['3', token]
|
||||||
|
token = int(self.data['token'])
|
||||||
|
with Store.lock:
|
||||||
|
if token not in Store.link_play_data:
|
||||||
|
return 108
|
||||||
|
r = Store.link_play_data[token]
|
||||||
|
room = r['room']
|
||||||
|
logging.info(f'TCP-Room `{room.room_code}` info update')
|
||||||
|
return {
|
||||||
|
'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
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import logging
|
||||||
|
from time import time
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +45,31 @@ class Player:
|
|||||||
|
|
||||||
self.start_command_num = 0
|
self.start_command_num = 0
|
||||||
|
|
||||||
|
@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,
|
||||||
|
'last_song': {
|
||||||
|
'difficulty': self.last_difficulty,
|
||||||
|
'score': self.last_score,
|
||||||
|
'cleartype': self.last_cleartype,
|
||||||
|
},
|
||||||
|
'song': {
|
||||||
|
'difficulty': self.difficulty,
|
||||||
|
'score': self.score,
|
||||||
|
'cleartype': self.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:
|
||||||
@@ -61,11 +89,10 @@ class Room:
|
|||||||
self.song_idx = 0xffff
|
self.song_idx = 0xffff
|
||||||
self.last_song_idx = 0xffff
|
self.last_song_idx = 0xffff
|
||||||
|
|
||||||
self.song_unlock = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||||
|
|
||||||
self.host_id = 0
|
self.host_id = 0
|
||||||
self.players = [Player(), Player(), Player(), Player()]
|
self.players = [Player(), Player(), Player(), Player()]
|
||||||
self.player_num = 0
|
|
||||||
|
|
||||||
self.interval = 1000
|
self.interval = 1000
|
||||||
self.times = 100
|
self.times = 100
|
||||||
@@ -73,7 +100,59 @@ class Room:
|
|||||||
self.round_switch = 0
|
self.round_switch = 0
|
||||||
|
|
||||||
self.command_queue = []
|
self.command_queue = []
|
||||||
self.command_queue_length = 0
|
|
||||||
|
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['player_id'] == self.host_id
|
||||||
|
return {
|
||||||
|
'room_id': self.room_id,
|
||||||
|
'room_code': self.room_code,
|
||||||
|
'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_switch': self.round_switch == 1,
|
||||||
|
'last_timestamp': self.timestamp,
|
||||||
|
'is_enterable': self.is_enterable,
|
||||||
|
'is_playing': self.is_playing,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_enterable(self) -> bool:
|
||||||
|
return 0 < self.player_num < 4 and self.state == 2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_playing(self) -> bool:
|
||||||
|
return self.state in (4, 5, 6, 7)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def command_queue_length(self) -> int:
|
||||||
|
return len(self.command_queue)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player_num(self) -> int:
|
||||||
|
self.check_player_online()
|
||||||
|
return sum(i.player_id != 0 for i in self.players)
|
||||||
|
|
||||||
|
def check_player_online(self, now: int = None):
|
||||||
|
# 检测玩家是否被自动踢出房间 / 离线判断
|
||||||
|
now = round(time() * 1000000) if now is None else now
|
||||||
|
flag = False
|
||||||
|
player_index_list = []
|
||||||
|
for i, x in enumerate(self.players):
|
||||||
|
if x.player_id == 0 or x.last_timestamp == 0:
|
||||||
|
continue
|
||||||
|
if now - x.last_timestamp >= Config.PLAYER_TIMEOUT:
|
||||||
|
self.delete_player(i)
|
||||||
|
flag = True
|
||||||
|
player_index_list.append(i)
|
||||||
|
elif x.online == 1 and now - x.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
|
||||||
|
x.online = 0
|
||||||
|
player_index_list.append(i)
|
||||||
|
|
||||||
|
return flag, player_index_list
|
||||||
|
|
||||||
def get_players_info(self):
|
def get_players_info(self):
|
||||||
# 获取所有玩家信息
|
# 获取所有玩家信息
|
||||||
@@ -106,17 +185,23 @@ class Room:
|
|||||||
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):
|
||||||
# 删除某个玩家
|
# 删除某个玩家
|
||||||
self.player_num -= 1
|
player = self.players[player_index]
|
||||||
if self.players[player_index].player_id == self.host_id:
|
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()
|
||||||
self.update_song_unlock()
|
self.update_song_unlock()
|
||||||
@@ -138,7 +223,6 @@ class Room:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_finish(self):
|
def is_finish(self):
|
||||||
@@ -178,3 +262,10 @@ class Room:
|
|||||||
|
|
||||||
for i in max_score_i:
|
for i in max_score_i:
|
||||||
self.players[i].best_player_flag = 1
|
self.players[i].best_player_flag = 1
|
||||||
|
|
||||||
|
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}` - Score: {i.last_score} Cleartype: {i.last_cleartype} Difficulty: {i.last_difficulty}')
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .udp_class import Room, bi
|
from .udp_class import Room, bi
|
||||||
@@ -6,31 +7,24 @@ from .udp_sender import CommandSender
|
|||||||
|
|
||||||
|
|
||||||
class CommandParser:
|
class CommandParser:
|
||||||
|
route = [None, 'command_01', 'command_02', 'command_03', 'command_04', 'command_05',
|
||||||
|
'command_06', 'command_07', 'command_08', 'command_09', 'command_0a', 'command_0b']
|
||||||
|
|
||||||
def __init__(self, room: Room, player_index: int = 0) -> None:
|
def __init__(self, room: Room, player_index: int = 0) -> None:
|
||||||
self.room = room
|
self.room = room
|
||||||
self.player_index = player_index
|
self.player_index = player_index
|
||||||
|
self.s = CommandSender(self.room)
|
||||||
|
self.command: bytes = None
|
||||||
|
|
||||||
def get_commands(self, command):
|
def get_commands(self, command: bytes):
|
||||||
self.command = command
|
self.command = command
|
||||||
l = {b'\x06\x16\x01': self.command_01,
|
r = getattr(self, self.route[self.command[2]])()
|
||||||
b'\x06\x16\x02': self.command_02,
|
|
||||||
b'\x06\x16\x03': self.command_03,
|
|
||||||
b'\x06\x16\x04': self.command_04,
|
|
||||||
b'\x06\x16\x05': self.command_05,
|
|
||||||
b'\x06\x16\x06': self.command_06,
|
|
||||||
b'\x06\x16\x07': self.command_07,
|
|
||||||
b'\x06\x16\x08': self.command_08,
|
|
||||||
b'\x06\x16\x09': self.command_09,
|
|
||||||
b'\x06\x16\x0a': self.command_0a,
|
|
||||||
b'\x06\x16\x0b': self.command_0b
|
|
||||||
}
|
|
||||||
r = l[command[:3]]()
|
|
||||||
|
|
||||||
re = []
|
re = []
|
||||||
|
|
||||||
flag_13 = False
|
flag_13 = False
|
||||||
for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length):
|
for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length):
|
||||||
if self.room.command_queue[i][:3] == b'\x06\x16\x13':
|
if self.room.command_queue[i][2] == 0x13:
|
||||||
if flag_13:
|
if flag_13:
|
||||||
break
|
break
|
||||||
flag_13 = True
|
flag_13 = True
|
||||||
@@ -51,17 +45,14 @@ 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}`')
|
||||||
|
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
self.room.command_queue.append(self.s.command_10())
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_10())
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_02(self):
|
def command_02(self):
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
song_idx = bi(self.command[24:26])
|
song_idx = bi(self.command[24:26])
|
||||||
|
|
||||||
flag = 2
|
flag = 2
|
||||||
@@ -69,17 +60,14 @@ class CommandParser:
|
|||||||
flag = 0
|
flag = 0
|
||||||
self.room.state = 3
|
self.room.state = 3
|
||||||
self.room.song_idx = song_idx
|
self.room.song_idx = song_idx
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_11())
|
||||||
self.room.command_queue.append(x.command_11())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
|
|
||||||
return [x.command_0d(flag)]
|
return [self.s.command_0d(flag)]
|
||||||
|
|
||||||
def command_03(self):
|
def command_03(self):
|
||||||
# 尝试进入结算
|
# 尝试进入结算
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
player = self.room.players[self.player_index]
|
player = self.room.players[self.player_index]
|
||||||
player.score = bi(self.command[24:28])
|
player.score = bi(self.command[24:28])
|
||||||
player.cleartype = self.command[28]
|
player.cleartype = self.command[28]
|
||||||
@@ -89,20 +77,15 @@ class CommandParser:
|
|||||||
player.last_timestamp -= Config.COMMAND_INTERVAL
|
player.last_timestamp -= Config.COMMAND_INTERVAL
|
||||||
self.room.last_song_idx = self.room.song_idx
|
self.room.last_song_idx = self.room.song_idx
|
||||||
|
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
self.room.command_queue.append(x.command_12(self.player_index))
|
|
||||||
|
|
||||||
if self.room.is_finish():
|
if self.room.is_finish():
|
||||||
self.room.make_finish()
|
self.room.make_finish()
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_04(self):
|
def command_04(self):
|
||||||
# 踢人
|
# 踢人
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
player_id = bi(self.command[24:32])
|
player_id = bi(self.command[24:32])
|
||||||
flag = 2
|
flag = 2
|
||||||
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
|
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
|
||||||
@@ -110,51 +93,38 @@ class CommandParser:
|
|||||||
if self.room.players[i].player_id == player_id:
|
if self.room.players[i].player_id == player_id:
|
||||||
flag = 1
|
flag = 1
|
||||||
self.room.delete_player(i)
|
self.room.delete_player(i)
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
self.room.command_queue.append(x.command_12(i))
|
|
||||||
self.room.update_song_unlock()
|
self.room.update_song_unlock()
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_14())
|
||||||
self.room.command_queue.append(x.command_14())
|
|
||||||
break
|
break
|
||||||
|
|
||||||
return [x.command_0d(flag)]
|
return [self.s.command_0d(flag)]
|
||||||
|
|
||||||
def command_05(self):
|
def command_05(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def command_06(self):
|
def command_06(self):
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
self.room.state = 1
|
self.room.state = 1
|
||||||
self.room.song_idx = 0xffff
|
self.room.song_idx = 0xffff
|
||||||
|
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_07(self):
|
def command_07(self):
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
self.room.players[self.player_index].song_unlock = self.command[24:536]
|
self.room.players[self.player_index].song_unlock = self.command[24:536]
|
||||||
self.room.update_song_unlock()
|
self.room.update_song_unlock()
|
||||||
|
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_14())
|
||||||
self.room.command_queue.append(x.command_14())
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_08(self):
|
def command_08(self):
|
||||||
self.room.round_switch = bi(self.command[24:25])
|
self.room.round_switch = bi(self.command[24:25])
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_09(self):
|
def command_09(self):
|
||||||
re = []
|
re = []
|
||||||
x = CommandSender(self.room)
|
self.s.random_code = self.command[16:24]
|
||||||
x.random_code = self.command[16:24]
|
|
||||||
player = self.room.players[self.player_index]
|
player = self.room.players[self.player_index]
|
||||||
|
|
||||||
if bi(self.command[12:16]) == 0:
|
if bi(self.command[12:16]) == 0:
|
||||||
@@ -162,29 +132,17 @@ class CommandParser:
|
|||||||
self.room.state = 1
|
self.room.state = 1
|
||||||
self.room.update_song_unlock()
|
self.room.update_song_unlock()
|
||||||
player.start_command_num = self.room.command_queue_length
|
player.start_command_num = self.room.command_queue_length
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_15())
|
||||||
self.room.command_queue.append(x.command_15())
|
|
||||||
else:
|
else:
|
||||||
if x.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
||||||
re.append(x.command_0c())
|
re.append(self.s.command_0c())
|
||||||
player.last_timestamp = x.timestamp
|
player.last_timestamp = self.s.timestamp
|
||||||
|
|
||||||
flag_13 = False
|
|
||||||
# 离线判断
|
# 离线判断
|
||||||
for i in range(4):
|
flag_13, player_index_list = self.room.check_player_online(
|
||||||
if i != self.player_index:
|
self.s.timestamp)
|
||||||
t = self.room.players[i]
|
for i in player_index_list:
|
||||||
if t.player_id != 0:
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
if t.last_timestamp != 0:
|
|
||||||
if t.online == 1 and x.timestamp - t.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
|
|
||||||
t.online = 0
|
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_12(i))
|
|
||||||
elif t.online == 0 and x.timestamp - t.last_timestamp >= Config.PLAYER_TIMEOUT:
|
|
||||||
self.room.delete_player(i)
|
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_12(i))
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
flag_11 = False
|
flag_11 = False
|
||||||
flag_12 = False
|
flag_12 = False
|
||||||
@@ -234,7 +192,9 @@ class CommandParser:
|
|||||||
# 将换房主时间提前到此刻
|
# 将换房主时间提前到此刻
|
||||||
self.room.make_round()
|
self.room.make_round()
|
||||||
|
|
||||||
if self.room.state == 4 or self.room.state == 5 or self.room.state == 6:
|
logging.info(f'Room `{self.room.room_code}` starts playing')
|
||||||
|
|
||||||
|
if self.room.state in (4, 5, 6):
|
||||||
timestamp = round(time.time() * 1000)
|
timestamp = round(time.time() * 1000)
|
||||||
self.room.countdown -= timestamp - self.room.timestamp
|
self.room.countdown -= timestamp - self.room.timestamp
|
||||||
self.room.timestamp = timestamp
|
self.room.timestamp = timestamp
|
||||||
@@ -263,10 +223,9 @@ class CommandParser:
|
|||||||
self.room.countdown = 0xffffffff
|
self.room.countdown = 0xffffffff
|
||||||
flag_13 = True
|
flag_13 = True
|
||||||
|
|
||||||
if self.room.countdown <= 0:
|
self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0
|
||||||
self.room.countdown = 0
|
|
||||||
|
|
||||||
if self.room.state == 7 or self.room.state == 8:
|
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:
|
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_timer = player.timer
|
||||||
player.last_score = player.score
|
player.last_score = player.score
|
||||||
@@ -276,7 +235,7 @@ class CommandParser:
|
|||||||
if player.timer != 0 or self.room.state != 8:
|
if player.timer != 0 or self.room.state != 8:
|
||||||
for i in self.room.players:
|
for i in self.room.players:
|
||||||
i.extra_command_queue.append(
|
i.extra_command_queue.append(
|
||||||
x.command_0e(self.player_index))
|
self.s.command_0e(self.player_index))
|
||||||
|
|
||||||
if self.room.is_ready(8, 1):
|
if self.room.is_ready(8, 1):
|
||||||
flag_13 = True
|
flag_13 = True
|
||||||
@@ -293,14 +252,12 @@ class CommandParser:
|
|||||||
flag_13 = True
|
flag_13 = True
|
||||||
|
|
||||||
if flag_11:
|
if flag_11:
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_11())
|
||||||
self.room.command_queue.append(x.command_11())
|
|
||||||
if flag_12:
|
if flag_12:
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(
|
||||||
self.room.command_queue.append(x.command_12(self.player_index))
|
self.s.command_12(self.player_index))
|
||||||
if flag_13:
|
if flag_13:
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
|
|
||||||
return re
|
return re
|
||||||
|
|
||||||
@@ -308,28 +265,19 @@ class CommandParser:
|
|||||||
# 退出房间
|
# 退出房间
|
||||||
self.room.delete_player(self.player_index)
|
self.room.delete_player(self.player_index)
|
||||||
|
|
||||||
x = CommandSender(self.room)
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_12(self.player_index))
|
|
||||||
|
|
||||||
if self.room.state == 3 or self.room.state == 2:
|
if self.room.state in (2, 3):
|
||||||
self.room.state = 1
|
self.room.state = 1
|
||||||
self.room.song_idx = 0xffff
|
self.room.song_idx = 0xffff
|
||||||
# self.room.command_queue_length += 1
|
# self.room.command_queue.append(self.s.command_11())
|
||||||
# self.room.command_queue.append(x.command_11())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue_length += 1
|
self.room.command_queue.append(self.s.command_14())
|
||||||
self.room.command_queue.append(x.command_13())
|
|
||||||
self.room.command_queue_length += 1
|
|
||||||
self.room.command_queue.append(x.command_14())
|
|
||||||
return None
|
|
||||||
|
|
||||||
def command_0b(self):
|
def command_0b(self):
|
||||||
# 推荐歌曲
|
# 推荐歌曲
|
||||||
song_idx = bi(self.command[16:18])
|
song_idx = bi(self.command[16:18])
|
||||||
x = CommandSender(self.room)
|
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
if self.player_index != i and self.room.players[i].online == 1:
|
if self.player_index != i and self.room.players[i].online == 1:
|
||||||
self.room.players[i].extra_command_queue.append(
|
self.room.players[i].extra_command_queue.append(
|
||||||
x.command_0f(self.player_index, song_idx))
|
self.s.command_0f(self.player_index, song_idx))
|
||||||
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -1,46 +1,64 @@
|
|||||||
import time
|
from time import time
|
||||||
|
|
||||||
from .udp_class import Room, b
|
from .udp_class import Room, b
|
||||||
|
|
||||||
|
|
||||||
class CommandSender:
|
class CommandSender:
|
||||||
def __init__(self, room: Room = Room()) -> None:
|
|
||||||
|
PROTOCOL_NAME = b'\x06\x16'
|
||||||
|
PROTOCOL_VERSION = b'\x09'
|
||||||
|
|
||||||
|
def __init__(self, room: Room = None) -> None:
|
||||||
self.room = room
|
self.room = room
|
||||||
self.timestamp = round(time.time() * 1000000)
|
self.timestamp = round(time() * 1000000)
|
||||||
|
|
||||||
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
|
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def command_encode(t: tuple):
|
||||||
|
r = b''.join(t)
|
||||||
|
x = 16 - len(r) % 16
|
||||||
|
return r + b(x) * x
|
||||||
|
|
||||||
|
def command_prefix(self, command: bytes):
|
||||||
|
length = self.room.command_queue_length
|
||||||
|
if command >= b'\x10':
|
||||||
|
length += 1
|
||||||
|
|
||||||
|
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
||||||
|
|
||||||
def command_0c(self):
|
def command_0c(self):
|
||||||
return b'\x06\x16\x0c\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b'\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'
|
return self.command_encode((*self.command_prefix(b'\x0c'), self.random_code, b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8)))
|
||||||
|
|
||||||
def command_0d(self, code: int):
|
def command_0d(self, code: int):
|
||||||
return b'\x06\x16\x0d\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(code) + b'\x07\x07\x07\x07\x07\x07\x07'
|
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
|
||||||
|
|
||||||
def command_0e(self, player_index: int):
|
def command_0e(self, player_index: int):
|
||||||
# 分数广播
|
# 分数广播
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
return b'\x06\x16\x0e\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + b(player.player_id, 8) + b(player.character_id) + b(player.is_uncapped) + b(player.difficulty) + b(player.score, 4) + b(player.timer, 4) + b(player.cleartype) + b(player.player_state) + b(player.download_percent) + b'\x01' + b(player.last_score, 4) + b(player.last_timer, 4) + b(player.online)
|
return self.command_encode((*self.command_prefix(b'\x0e'), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b'\x01', b(player.last_score, 4), b(player.last_timer, 4), b(player.online)))
|
||||||
|
|
||||||
def command_0f(self, player_index: int, song_idx: int):
|
def command_0f(self, player_index: int, song_idx: int):
|
||||||
# 歌曲推荐
|
# 歌曲推荐
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
return b'\x06\x16\x0f\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + b(player.player_id, 8) + b(song_idx, 2) + b'\x06\x06\x06\x06\x06\x06'
|
return self.command_encode((*self.command_prefix(b'\x0f'), b(player.player_id, 8), b(song_idx, 2)))
|
||||||
|
|
||||||
def command_10(self):
|
def command_10(self):
|
||||||
# 房主宣告
|
# 房主宣告
|
||||||
return b'\x06\x16\x10\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.host_id, 8)
|
return self.command_encode((*self.command_prefix(b'\x10'), self.random_code, b(self.room.host_id, 8)))
|
||||||
|
|
||||||
def command_11(self):
|
def command_11(self):
|
||||||
return b'\x06\x16\x11\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + self.room.get_players_info() + b'\x08\x08\x08\x08\x08\x08\x08\x08'
|
return self.command_encode((*self.command_prefix(b'\x11'), self.random_code, self.room.get_players_info()))
|
||||||
|
|
||||||
def command_12(self, player_index: int):
|
def command_12(self, player_index: int):
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
return b'\x06\x16\x12\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(player_index) + b(player.player_id, 8) + b(player.character_id) + b(player.is_uncapped) + b(player.difficulty) + b(player.score, 4) + b(player.timer, 4) + b(player.cleartype) + b(player.player_state) + b(player.download_percent) + b(player.online)
|
return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b(player.online)))
|
||||||
|
|
||||||
def command_13(self):
|
def command_13(self):
|
||||||
return b'\x06\x16\x13\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + b(self.room.host_id, 8) + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b(self.room.song_idx, 2) + b(self.room.interval, 2) + b(self.room.times, 7) + self.room.get_player_last_score() + b(self.room.last_song_idx, 2) + b(self.room.round_switch, 1) + b'\x01'
|
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
|
||||||
|
|
||||||
def command_14(self):
|
def command_14(self):
|
||||||
return b'\x06\x16\x14\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.random_code + self.room.song_unlock + b'\x08\x08\x08\x08\x08\x08\x08\x08'
|
return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
|
||||||
|
|
||||||
def command_15(self):
|
def command_15(self):
|
||||||
return b'\x06\x16\x15\x09' + b(self.room.room_id, 8) + b(self.room.command_queue_length, 4) + self.room.get_players_info() + self.room.song_unlock + b(self.room.host_id, 8) + b(self.room.state) + b(self.room.countdown, 4) + b(self.timestamp, 8) + b(self.room.song_idx, 2) + b(self.room.interval, 2) + b(self.room.times, 7) + self.room.get_player_last_score() + b(self.room.last_song_idx, 2) + b(self.room.round_switch, 1) + b'\x09\x09\x09\x09\x09\x09\x09\x09\x09'
|
return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from core.config_manager import Config, ConfigManager
|
||||||
|
|
||||||
|
if os.path.exists('config.py') or os.path.exists('config'):
|
||||||
|
# 导入用户自定义配置
|
||||||
|
ConfigManager.load(import_module('config').Config)
|
||||||
|
|
||||||
|
if Config.DEPLOY_MODE == 'gevent':
|
||||||
|
# 异步
|
||||||
|
from gevent import monkey
|
||||||
|
monkey.patch_all()
|
||||||
|
|
||||||
|
|
||||||
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, set_start_method
|
||||||
@@ -10,33 +24,38 @@ from flask import Flask, make_response, request, send_from_directory
|
|||||||
|
|
||||||
import api
|
import api
|
||||||
import server
|
import server
|
||||||
import server.init
|
|
||||||
import web.index
|
import web.index
|
||||||
import web.login
|
import web.login
|
||||||
|
# import webapi
|
||||||
from core.constant import Constant
|
from core.constant import Constant
|
||||||
from core.download import (UserDownload, get_only_3_song_ids,
|
from core.download import UserDownload
|
||||||
initialize_songfile)
|
from core.error import ArcError, NoAccess, RateLimit
|
||||||
from core.error import ArcError
|
from core.init import FileChecker
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from server.func import error_return
|
from server.func import error_return
|
||||||
from setting import Config
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# from werkzeug.middleware.proxy_fix import ProxyFix
|
if Config.USE_PROXY_FIX:
|
||||||
# app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
# 代理修复
|
||||||
# from flask_cors import CORS
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
# CORS(app, supports_credentials=True)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
||||||
|
if Config.USE_CORS:
|
||||||
|
# 服务端跨域
|
||||||
|
from flask_cors import CORS
|
||||||
|
CORS(app, supports_credentials=True)
|
||||||
|
|
||||||
|
|
||||||
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
|
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
|
||||||
|
|
||||||
|
|
||||||
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
|
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
|
||||||
app.config['SESSION_TYPE'] = 'filesystem'
|
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)
|
app.register_blueprint(server.bp)
|
||||||
|
# app.register_blueprint(webapi.bp)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -55,22 +74,23 @@ def favicon():
|
|||||||
|
|
||||||
@app.route('/download/<path:file_path>', methods=['GET']) # 下载
|
@app.route('/download/<path:file_path>', methods=['GET']) # 下载
|
||||||
def download(file_path):
|
def download(file_path):
|
||||||
with Connect() as c:
|
with Connect(in_memory=True) as c:
|
||||||
try:
|
try:
|
||||||
x = UserDownload(c)
|
x = UserDownload(c)
|
||||||
x.token = request.args.get('t')
|
x.token = request.args.get('t')
|
||||||
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 ArcError(
|
raise RateLimit('You have reached the download limit.', 903)
|
||||||
'You have reached the download limit.', 903, status=403)
|
|
||||||
if not x.is_valid:
|
if not x.is_valid:
|
||||||
raise ArcError('Expired token.', status=403)
|
raise NoAccess('Expired token.')
|
||||||
x.insert_user_download()
|
x.download_hit()
|
||||||
# response = make_response()
|
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||||
# response.headers['Content-Type'] = 'application/octet-stream'
|
# nginx X-Accel-Redirect
|
||||||
# response.headers['X-Accel-Redirect'] = '/nginx_download/' + file_path
|
response = make_response()
|
||||||
# return response
|
response.headers['Content-Type'] = 'application/octet-stream'
|
||||||
|
response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path
|
||||||
|
return response
|
||||||
return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
if Config.ALLOW_WARNING_LOG:
|
if Config.ALLOW_WARNING_LOG:
|
||||||
@@ -79,10 +99,35 @@ def download(file_path):
|
|||||||
return error_return()
|
return error_return()
|
||||||
|
|
||||||
|
|
||||||
|
if Config.DEPLOY_MODE == 'waitress':
|
||||||
|
# 给waitress加个日志
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
app.logger.info(
|
||||||
|
f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}')
|
||||||
|
return response
|
||||||
|
|
||||||
|
# @app.before_request
|
||||||
|
# def before_request():
|
||||||
|
# print(request.path)
|
||||||
|
# print(request.headers)
|
||||||
|
# print(request.data)
|
||||||
|
|
||||||
|
|
||||||
def tcp_server_run():
|
def tcp_server_run():
|
||||||
if False:
|
if Config.DEPLOY_MODE == 'gevent':
|
||||||
|
# 异步 gevent WSGI server
|
||||||
|
host_port = (Config.HOST, Config.PORT)
|
||||||
|
app.logger.info('Running gevent WSGI server... (%s:%s)' % host_port)
|
||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
WSGIServer(("127.0.0.1", 5000), app).serve_forever()
|
WSGIServer(host_port, app, log=app.logger).serve_forever()
|
||||||
|
elif Config.DEPLOY_MODE == 'waitress':
|
||||||
|
# waitress WSGI server
|
||||||
|
import logging
|
||||||
|
from waitress import serve
|
||||||
|
logger = logging.getLogger('waitress')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
serve(app, host=Config.HOST, port=Config.PORT)
|
||||||
else:
|
else:
|
||||||
if Config.SSL_CERT and Config.SSL_KEY:
|
if Config.SSL_CERT and Config.SSL_KEY:
|
||||||
app.run(Config.HOST, Config.PORT, ssl_context=(
|
app.run(Config.HOST, Config.PORT, ssl_context=(
|
||||||
@@ -91,6 +136,18 @@ def tcp_server_run():
|
|||||||
app.run(Config.HOST, Config.PORT)
|
app.run(Config.HOST, Config.PORT)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_log_file_dict(level: str, filename: str) -> dict:
|
||||||
|
return {
|
||||||
|
"class": "logging.handlers.RotatingFileHandler",
|
||||||
|
"maxBytes": 1024 * 1024,
|
||||||
|
"backupCount": 1,
|
||||||
|
"encoding": "utf-8",
|
||||||
|
"level": level,
|
||||||
|
"formatter": "default",
|
||||||
|
"filename": filename
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
log_dict = {
|
log_dict = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
@@ -104,15 +161,7 @@ def main():
|
|||||||
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||||
'formatter': 'default'
|
'formatter': 'default'
|
||||||
},
|
},
|
||||||
"error_file": {
|
"error_file": generate_log_file_dict('ERROR', './log/error.log')
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
|
||||||
"maxBytes": 1024 * 1024,
|
|
||||||
"backupCount": 1,
|
|
||||||
"encoding": "utf-8",
|
|
||||||
"level": "ERROR",
|
|
||||||
"formatter": "default",
|
|
||||||
"filename": "./log/error.log"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'formatters': {
|
'formatters': {
|
||||||
'default': {
|
'default': {
|
||||||
@@ -122,51 +171,30 @@ def main():
|
|||||||
}
|
}
|
||||||
if Config.ALLOW_INFO_LOG:
|
if Config.ALLOW_INFO_LOG:
|
||||||
log_dict['root']['handlers'].append('info_file')
|
log_dict['root']['handlers'].append('info_file')
|
||||||
log_dict['handlers']['info_file'] = {
|
log_dict['handlers']['info_file'] = generate_log_file_dict(
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
'INFO', './log/info.log')
|
||||||
"maxBytes": 1024 * 1024,
|
|
||||||
"backupCount": 1,
|
|
||||||
"encoding": "utf-8",
|
|
||||||
"level": "INFO",
|
|
||||||
"formatter": "default",
|
|
||||||
"filename": "./log/info.log"
|
|
||||||
}
|
|
||||||
if Config.ALLOW_WARNING_LOG:
|
if Config.ALLOW_WARNING_LOG:
|
||||||
log_dict['root']['handlers'].append('warning_file')
|
log_dict['root']['handlers'].append('warning_file')
|
||||||
log_dict['handlers']['warning_file'] = {
|
log_dict['handlers']['warning_file'] = generate_log_file_dict(
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
'WARNING', './log/warning.log')
|
||||||
"maxBytes": 1024 * 1024,
|
|
||||||
"backupCount": 1,
|
|
||||||
"encoding": "utf-8",
|
|
||||||
"level": "WARNING",
|
|
||||||
"formatter": "default",
|
|
||||||
"filename": "./log/warning.log"
|
|
||||||
}
|
|
||||||
|
|
||||||
dictConfig(log_dict)
|
dictConfig(log_dict)
|
||||||
|
|
||||||
if not server.init.check_before_run(app):
|
Connect.logger = app.logger
|
||||||
app.logger.error('Something wrong. The server will not run.')
|
if not FileChecker(app.logger).check_before_run():
|
||||||
|
app.logger.error('Some errors occurred. The server will not run.')
|
||||||
input('Press ENTER key to exit.')
|
input('Press ENTER key to exit.')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
app.logger.info("Start to initialize song data...")
|
|
||||||
try:
|
|
||||||
initialize_songfile()
|
|
||||||
get_only_3_song_ids()
|
|
||||||
app.logger.info('Complete!')
|
|
||||||
except:
|
|
||||||
app.logger.warning('Initialization error!')
|
|
||||||
|
|
||||||
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
||||||
from linkplay_server import link_play
|
from linkplay_server import link_play
|
||||||
process = [Process(target=link_play, args=(
|
process = [Process(target=link_play, args=(
|
||||||
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
||||||
[p.start() for p in process]
|
[p.start() for p in process]
|
||||||
app.logger.info("Link Play UDP server is running on " +
|
app.logger.info(
|
||||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_UDP_PORT) + " ...")
|
f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...")
|
||||||
app.logger.info("Link Play TCP server is running on " +
|
app.logger.info(
|
||||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_TCP_PORT) + " ...")
|
f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...")
|
||||||
tcp_server_run()
|
tcp_server_run()
|
||||||
[p.join() for p in process]
|
[p.join() for p in process]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
flask>=2.0.2
|
flask>=2.0.2
|
||||||
cryptography>=35.0.0
|
cryptography>=3.0.0
|
||||||
|
limits>=2.7.0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
cd /d %~dp0
|
cd /d %~dp0
|
||||||
:: Develop server
|
:: Develop server
|
||||||
python -B main.py
|
python -B main.py
|
||||||
|
pause
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from setting 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)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
from core.sql import Connect
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_rating(defnum, score):
|
|
||||||
# 计算rating
|
|
||||||
if score >= 10000000:
|
|
||||||
ptt = defnum + 2
|
|
||||||
elif score < 9800000:
|
|
||||||
ptt = defnum + (score-9500000) / 300000
|
|
||||||
if ptt < 0 and defnum != -10:
|
|
||||||
ptt = 0
|
|
||||||
else:
|
|
||||||
ptt = defnum + 1 + (score-9800000) / 200000
|
|
||||||
|
|
||||||
return ptt
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_all_score_rating():
|
|
||||||
# 刷新所有best成绩的rating
|
|
||||||
error = 'Unknown error.'
|
|
||||||
|
|
||||||
with Connect() as c:
|
|
||||||
c.execute(
|
|
||||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
|
||||||
x = c.fetchall()
|
|
||||||
|
|
||||||
if x:
|
|
||||||
song_list = [i[0] for i in x]
|
|
||||||
with Connect() as c:
|
|
||||||
c.execute('''update best_score set rating=0 where song_id not in ({0})'''.format(
|
|
||||||
','.join(['?']*len(song_list))), tuple(song_list))
|
|
||||||
for i in x:
|
|
||||||
for j in range(0, 4):
|
|
||||||
defnum = -10 # 没在库里的全部当做定数-10
|
|
||||||
if i is not None:
|
|
||||||
defnum = float(i[j+1]) / 10
|
|
||||||
if defnum <= 0:
|
|
||||||
defnum = -10 # 缺少难度的当做定数-10
|
|
||||||
|
|
||||||
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
|
|
||||||
'a': i[0], 'b': j})
|
|
||||||
y = c.fetchall()
|
|
||||||
if y:
|
|
||||||
for k in y:
|
|
||||||
ptt = calculate_rating(defnum, k[1])
|
|
||||||
if ptt < 0:
|
|
||||||
ptt = 0
|
|
||||||
|
|
||||||
c.execute('''update best_score set rating=:a where user_id=:b and song_id=:c and difficulty=:d''', {
|
|
||||||
'a': ptt, 'b': k[0], 'c': i[0], 'd': j})
|
|
||||||
error = None
|
|
||||||
|
|
||||||
else:
|
|
||||||
error = 'No song data.'
|
|
||||||
|
|
||||||
return error
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import base64
|
import base64
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, g, jsonify, request
|
||||||
|
|
||||||
from core.error import ArcError, NoAccess
|
from core.error import ArcError, NoAccess
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserAuth, UserLogin
|
from core.user import UserAuth, UserLogin
|
||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from setting import Config
|
|
||||||
|
|
||||||
from .func import arc_try, error_return
|
from .func import arc_try, error_return, header_check
|
||||||
|
|
||||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
|
|
||||||
@@ -16,10 +16,9 @@ bp = Blueprint('auth', __name__, url_prefix='/auth')
|
|||||||
@arc_try
|
@arc_try
|
||||||
def login():
|
def login():
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
if 'AppVersion' in headers: # 版本检查
|
e = header_check(request)
|
||||||
if Config.ALLOW_APPVERSION:
|
if e is not None:
|
||||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
raise e
|
||||||
raise NoAccess('Wrong app version.', 1203)
|
|
||||||
|
|
||||||
request.form['grant_type']
|
request.form['grant_type']
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
@@ -33,32 +32,42 @@ def login():
|
|||||||
|
|
||||||
user = UserLogin(c)
|
user = UserLogin(c)
|
||||||
user.login(name, password, device_id, request.remote_addr)
|
user.login(name, password, device_id, request.remote_addr)
|
||||||
|
current_app.logger.info(f'User `{user.user_id}` log in')
|
||||||
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})
|
||||||
|
|
||||||
|
|
||||||
def auth_required(request):
|
@bp.route('/verify', methods=['POST']) # 邮箱验证进度查询
|
||||||
|
@arc_try
|
||||||
|
def email_verify():
|
||||||
|
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def auth_required(req):
|
||||||
# arcaea登录验证,写成了修饰器
|
# arcaea登录验证,写成了修饰器
|
||||||
def decorator(view):
|
def decorator(view):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def wrapped_view(*args, **kwargs):
|
def wrapped_view(*args, **kwargs):
|
||||||
|
|
||||||
headers = request.headers
|
headers = req.headers
|
||||||
|
|
||||||
if 'AppVersion' in headers: # 版本检查
|
e = header_check(req)
|
||||||
if Config.ALLOW_APPVERSION:
|
if e is not None:
|
||||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
current_app.logger.warning(
|
||||||
return error_return(NoAccess('Wrong app version.', 1203))
|
f' - {e.error_code}|{e.api_error_code}: {e}')
|
||||||
|
return error_return(e)
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
try:
|
try:
|
||||||
user = UserAuth(c)
|
user = UserAuth(c)
|
||||||
user.token = headers['Authorization'][7:]
|
token = headers.get('Authorization')
|
||||||
return view(user.token_get_id(), *args, **kwargs)
|
if not token:
|
||||||
|
raise NoAccess('No token.', -4)
|
||||||
|
user.token = token[7:]
|
||||||
|
user_id = user.token_get_id()
|
||||||
|
g.user = user
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
return error_return(e)
|
return error_return(e)
|
||||||
|
return view(user_id, *args, **kwargs)
|
||||||
return error_return()
|
|
||||||
|
|
||||||
return wrapped_view
|
return wrapped_view
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
from core.constant import Constant
|
from core.constant import Constant
|
||||||
from core.course import UserCourseList
|
from core.course import UserCourseList
|
||||||
from core.error import ArcError
|
|
||||||
from core.item import ItemCore
|
from core.item import ItemCore
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .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 course_me(user_id):
|
|||||||
user = UserOnline(c, user_id)
|
user = UserOnline(c, user_id)
|
||||||
core = ItemCore(c)
|
core = ItemCore(c)
|
||||||
core.item_id = 'core_course_skip_purchase'
|
core.item_id = 'core_course_skip_purchase'
|
||||||
core.select(user)
|
core.select_user_item(user)
|
||||||
x = UserCourseList(c, user)
|
x = UserCourseList(c, user)
|
||||||
x.select_all()
|
x.select_all()
|
||||||
return success_return({
|
return success_return({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline, code_get_id
|
from core.user import UserOnline, code_get_id
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from core.error import ArcError
|
from flask import current_app, g, jsonify
|
||||||
from flask import current_app, jsonify
|
|
||||||
from setting import Config
|
from core.config_manager import Config
|
||||||
|
from core.error import ArcError, NoAccess
|
||||||
|
|
||||||
|
has_arc_hash = False
|
||||||
|
try:
|
||||||
|
from core.arc_crypto import ArcHashChecker # type: ignore
|
||||||
|
has_arc_hash = True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
default_error = ArcError('Unknown Error', status=500)
|
default_error = ArcError('Unknown Error', status=500)
|
||||||
|
|
||||||
@@ -32,6 +40,8 @@ def error_return(e: ArcError = default_error): # 错误返回
|
|||||||
# 124 你今天不能再使用这个IP地址创建新的账号
|
# 124 你今天不能再使用这个IP地址创建新的账号
|
||||||
# 150 非常抱歉您已被限制使用此功能
|
# 150 非常抱歉您已被限制使用此功能
|
||||||
# 151 目前无法使用此功能
|
# 151 目前无法使用此功能
|
||||||
|
# 160 账户未邮箱认证,请检查邮箱
|
||||||
|
# 161 账户认证过期,请重新注册
|
||||||
# 401 用户不存在
|
# 401 用户不存在
|
||||||
# 403 无法连接至服务器
|
# 403 无法连接至服务器
|
||||||
# 501 502 -6 此物品目前无法获取
|
# 501 502 -6 此物品目前无法获取
|
||||||
@@ -43,6 +53,7 @@ def error_return(e: ArcError = default_error): # 错误返回
|
|||||||
# 604 你不能加自己为好友
|
# 604 你不能加自己为好友
|
||||||
# 903 下载量超过了限制,请24小时后重试
|
# 903 下载量超过了限制,请24小时后重试
|
||||||
# 905 请在再次使用此功能前等待24小时
|
# 905 请在再次使用此功能前等待24小时
|
||||||
|
# 910 重新请求验证邮件前需等待x分钟 extra: retry_at
|
||||||
# 1001 设备数量达到上限
|
# 1001 设备数量达到上限
|
||||||
# 1002 此设备已使用过此功能
|
# 1002 此设备已使用过此功能
|
||||||
# 1201 房间已满
|
# 1201 房间已满
|
||||||
@@ -78,11 +89,26 @@ def arc_try(view):
|
|||||||
data = view(*args, **kwargs)
|
data = view(*args, **kwargs)
|
||||||
if data is None:
|
if data is None:
|
||||||
return error_return()
|
return error_return()
|
||||||
else:
|
|
||||||
return data
|
return data
|
||||||
except ArcError as e:
|
except ArcError as e:
|
||||||
if Config.ALLOW_WARNING_LOG:
|
if Config.ALLOW_WARNING_LOG:
|
||||||
current_app.logger.warning(format_exc())
|
current_app.logger.warning(format_exc())
|
||||||
|
user = g.get("user", None)
|
||||||
|
current_app.logger.warning(
|
||||||
|
f'{user.user_id if user is not None else ""} - {e.error_code}|{e.api_error_code}: {e}')
|
||||||
return error_return(e)
|
return error_return(e)
|
||||||
|
|
||||||
return wrapped_view
|
return wrapped_view
|
||||||
|
|
||||||
|
|
||||||
|
def header_check(request) -> ArcError:
|
||||||
|
'''检查请求头是否合法'''
|
||||||
|
headers = request.headers
|
||||||
|
if Config.ALLOW_APPVERSION: # 版本检查
|
||||||
|
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||||
|
return NoAccess('Invalid app version', 1203)
|
||||||
|
|
||||||
|
if has_arc_hash and not ArcHashChecker(request).check():
|
||||||
|
return NoAccess('Invalid request')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import os
|
|
||||||
from shutil import copy, copy2
|
|
||||||
from core.sql import Connect
|
|
||||||
from database.database_initialize import main, ARCAEA_SERVER_VERSION
|
|
||||||
from web.system import update_database
|
|
||||||
|
|
||||||
|
|
||||||
def try_rename(path, new_path):
|
|
||||||
# 尝试重命名文件,并尝试避免命名冲突,返回最终路径
|
|
||||||
final_path = new_path
|
|
||||||
if os.path.exists(new_path):
|
|
||||||
i = 1
|
|
||||||
while os.path.exists(new_path + str(i)):
|
|
||||||
i += 1
|
|
||||||
os.rename(path, new_path + str(i))
|
|
||||||
final_path = new_path + str(i)
|
|
||||||
else:
|
|
||||||
os.rename(path, new_path)
|
|
||||||
|
|
||||||
return final_path
|
|
||||||
|
|
||||||
|
|
||||||
def check_before_run(app):
|
|
||||||
# 运行前检查关键文件,返回布尔值,其实是因为有人经常忘了
|
|
||||||
|
|
||||||
f = True
|
|
||||||
|
|
||||||
if not os.path.exists('setting.py'):
|
|
||||||
app.logger.warning('File `setting.py` is missing.')
|
|
||||||
f = False
|
|
||||||
|
|
||||||
if not os.path.exists('database'):
|
|
||||||
app.logger.warning('Folder `database` is missing.')
|
|
||||||
f = False
|
|
||||||
|
|
||||||
if not os.path.exists('database/songs'):
|
|
||||||
app.logger.warning('Folder `database/songs` is missing.')
|
|
||||||
f = False
|
|
||||||
|
|
||||||
if not os.path.exists('database/arcaea_database.db'):
|
|
||||||
app.logger.warning('File `database/arcaea_database.db` is missing.')
|
|
||||||
f = False
|
|
||||||
try:
|
|
||||||
app.logger.info(
|
|
||||||
'Try to new the file `database/arcaea_database.db`.')
|
|
||||||
main('./database/')
|
|
||||||
app.logger.info(
|
|
||||||
'Success to new the file `database/arcaea_database.db`.')
|
|
||||||
f = True
|
|
||||||
except:
|
|
||||||
app.logger.warning(
|
|
||||||
'Fail to new the file `database/arcaea_database.db`.')
|
|
||||||
|
|
||||||
else:
|
|
||||||
with Connect() as c:
|
|
||||||
try:
|
|
||||||
c.execute('''select value from config where id="version"''')
|
|
||||||
x = c.fetchone()
|
|
||||||
except:
|
|
||||||
x = None
|
|
||||||
|
|
||||||
# 数据库自动更新,不强求
|
|
||||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
|
||||||
app.logger.warning(
|
|
||||||
'Maybe the file `database/arcaea_database.db` is an old version.')
|
|
||||||
try:
|
|
||||||
app.logger.info(
|
|
||||||
'Try to update the file `database/arcaea_database.db`.')
|
|
||||||
|
|
||||||
path = try_rename('database/arcaea_database.db',
|
|
||||||
'database/arcaea_database.db.bak')
|
|
||||||
|
|
||||||
try:
|
|
||||||
copy2(path, 'database/arcaea_database.db')
|
|
||||||
except:
|
|
||||||
copy(path, 'database/arcaea_database.db')
|
|
||||||
|
|
||||||
if os.path.isfile("database/old_arcaea_database.db"):
|
|
||||||
os.remove('database/old_arcaea_database.db')
|
|
||||||
|
|
||||||
try_rename('database/arcaea_database.db',
|
|
||||||
'database/old_arcaea_database.db')
|
|
||||||
|
|
||||||
main('./database/')
|
|
||||||
update_database()
|
|
||||||
|
|
||||||
app.logger.info(
|
|
||||||
'Success to update the file `database/arcaea_database.db`.')
|
|
||||||
|
|
||||||
except:
|
|
||||||
app.logger.warning(
|
|
||||||
'Fail to update the file `database/arcaea_database.db`.')
|
|
||||||
|
|
||||||
return f
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
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 Player, RemoteMultiPlayer, Room
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from flask import Blueprint, request
|
|
||||||
from setting import Config
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import json
|
import json
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from core.download import DownloadList
|
from core.download import DownloadList
|
||||||
from core.error import ArcError
|
from core.error import RateLimit
|
||||||
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
|
||||||
from flask import Blueprint, jsonify, request
|
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
|
||||||
|
|
||||||
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_pack, bundle_bundle
|
from .purchase import bundle_bundle, bundle_pack
|
||||||
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
|
||||||
@@ -29,13 +30,13 @@ def game_info():
|
|||||||
@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_m:
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
x = DownloadList(c, UserOnline(c, user_id))
|
x = DownloadList(c_m, UserOnline(c, user_id))
|
||||||
x.song_ids = request.args.getlist('sid')
|
x.song_ids = request.args.getlist('sid')
|
||||||
x.url_flag = json.loads(request.args.get('url', 'true'))
|
x.url_flag = json.loads(request.args.get('url', 'true'))
|
||||||
x.clear_user_download()
|
if x.url_flag and x.is_limited:
|
||||||
if x.is_limited and x.url_flag:
|
raise RateLimit('You have reached the download limit.', 903)
|
||||||
raise ArcError('You have reached the download limit.', 903)
|
|
||||||
|
|
||||||
x.add_songs()
|
x.add_songs()
|
||||||
return success_return(x.urls)
|
return success_return(x.urls)
|
||||||
@@ -54,6 +55,11 @@ def finale_start():
|
|||||||
return success_return({})
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/finale/finale_end', methods=['POST'])
|
||||||
|
def finale_end():
|
||||||
|
return success_return({})
|
||||||
|
|
||||||
|
|
||||||
map_dict = {'/user/me': user_me,
|
map_dict = {'/user/me': user_me,
|
||||||
'/purchase/bundle/pack': bundle_pack,
|
'/purchase/bundle/pack': bundle_pack,
|
||||||
'/serve/download/me/song': download_song,
|
'/serve/download/me/song': download_song,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from core.error import ArcError
|
from flask import Blueprint, request
|
||||||
|
|
||||||
from core.present import UserPresent, UserPresentList
|
from core.present import UserPresent, UserPresentList
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, error_return, success_return
|
from .func import arc_try, success_return
|
||||||
|
|
||||||
bp = Blueprint('present', __name__, url_prefix='/present')
|
bp = Blueprint('present', __name__, url_prefix='/present')
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
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
|
||||||
from core.redeem import UserRedeem
|
from core.redeem import UserRedeem
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
from random import randint
|
||||||
from time import time
|
from time import time
|
||||||
from core.course import CoursePlay
|
|
||||||
|
|
||||||
|
from flask import Blueprint, request
|
||||||
|
|
||||||
|
from core.constant import Constant
|
||||||
|
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
|
||||||
from core.score import UserPlay
|
from core.score import UserPlay
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
@@ -24,24 +27,38 @@ def score_token():
|
|||||||
@arc_try
|
@arc_try
|
||||||
def score_token_world(user_id):
|
def score_token_world(user_id):
|
||||||
|
|
||||||
stamina_multiply = int(
|
d = request.args.get
|
||||||
request.args['stamina_multiply']) if 'stamina_multiply' in request.args else 1
|
|
||||||
fragment_multiply = int(
|
stamina_multiply = int(d('stamina_multiply', 1))
|
||||||
request.args['fragment_multiply']) if 'fragment_multiply' in request.args else 100
|
fragment_multiply = int(d('fragment_multiply', 100))
|
||||||
prog_boost_multiply = int(
|
prog_boost_multiply = int(d('prog_boost_multiply', 0))
|
||||||
request.args['prog_boost_multiply']) if 'prog_boost_multiply' in request.args else 0
|
beyond_boost_gauge_use = int(d('beyond_boost_gauge_use', 0))
|
||||||
|
skill_cytusii_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)])
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
x = UserPlay(c, UserOnline(c, user_id))
|
x = UserPlay(c, UserOnline(c, user_id))
|
||||||
x.song.set_chart(request.args['song_id'], int(
|
x.song.set_chart(request.args['song_id'], int(
|
||||||
request.args['difficulty']))
|
request.args['difficulty']))
|
||||||
x.set_play_state_for_world(stamina_multiply,
|
x.set_play_state_for_world(
|
||||||
fragment_multiply, prog_boost_multiply)
|
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_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,
|
||||||
}
|
}
|
||||||
)
|
if skill_cytusii_flag and skill_id:
|
||||||
|
r['play_parameters'] = {
|
||||||
|
skill_id: list(
|
||||||
|
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_cytusii_flag))
|
||||||
|
}
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
||||||
@@ -96,6 +113,10 @@ def song_score_post(user_id):
|
|||||||
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
|
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
|
||||||
x.beyond_gauge = int(request.form['beyond_gauge'])
|
x.beyond_gauge = int(request.form['beyond_gauge'])
|
||||||
x.submission_hash = request.form['submission_hash']
|
x.submission_hash = request.form['submission_hash']
|
||||||
|
if 'combo_interval_bonus' in request.form:
|
||||||
|
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
|
||||||
|
x.highest_health = request.form.get("highest_health", type=int)
|
||||||
|
x.lowest_health = request.form.get("lowest_health", 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,14 +1,14 @@
|
|||||||
|
from flask import Blueprint, current_app, request
|
||||||
|
|
||||||
from core.character import UserCharacter
|
from core.character import UserCharacter
|
||||||
from core.error import ArcError, NoAccess
|
from core.error import ArcError
|
||||||
from core.item import ItemCore
|
from core.item import ItemCore
|
||||||
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
|
||||||
from flask import Blueprint, request
|
|
||||||
from setting import Config
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, header_check, success_return
|
||||||
|
|
||||||
bp = Blueprint('user', __name__, url_prefix='/user')
|
bp = Blueprint('user', __name__, url_prefix='/user')
|
||||||
|
|
||||||
@@ -16,10 +16,9 @@ bp = Blueprint('user', __name__, url_prefix='/user')
|
|||||||
@bp.route('', methods=['POST']) # 注册接口
|
@bp.route('', methods=['POST']) # 注册接口
|
||||||
@arc_try
|
@arc_try
|
||||||
def register():
|
def register():
|
||||||
if 'AppVersion' in request.headers: # 版本检查
|
error = header_check(request)
|
||||||
if Config.ALLOW_APPVERSION:
|
if error is not None:
|
||||||
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
raise error
|
||||||
raise NoAccess('Wrong app version.', 1203)
|
|
||||||
|
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
new_user = UserRegister(c)
|
new_user = UserRegister(c)
|
||||||
@@ -37,6 +36,7 @@ def register():
|
|||||||
user = UserLogin(c)
|
user = UserLogin(c)
|
||||||
user.login(new_user.name, new_user.password,
|
user.login(new_user.name, new_user.password,
|
||||||
device_id, request.remote_addr)
|
device_id, request.remote_addr)
|
||||||
|
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})
|
||||||
|
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ 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 'is_hide_rating' == set_arg or 'max_stamina_notification_enabled' == set_arg:
|
if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'):
|
||||||
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())
|
||||||
|
|
||||||
@@ -165,3 +165,9 @@ def sys_set(user_id, set_arg):
|
|||||||
@arc_try
|
@arc_try
|
||||||
def user_delete(user_id):
|
def user_delete(user_id):
|
||||||
raise ArcError('Cannot delete the account.', 151, status=404)
|
raise ArcError('Cannot delete the account.', 151, status=404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
||||||
|
@arc_try
|
||||||
|
def email_resend_verify():
|
||||||
|
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
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 UserMap, get_world_all
|
||||||
from flask import Blueprint, request
|
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
from .func import arc_try, success_return
|
from .func import arc_try, success_return
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import server.arcscore
|
from core.init import FileChecker
|
||||||
from core.download import initialize_songfile, get_only_3_song_ids
|
from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
|
||||||
from core.rank import RankList
|
from core.rank import RankList
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
|
from core.user import User
|
||||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@@ -270,7 +271,8 @@ def update_database():
|
|||||||
file.save(os.path.join(UPLOAD_FOLDER, filename))
|
file.save(os.path.join(UPLOAD_FOLDER, filename))
|
||||||
flash('上传成功 Success upload.')
|
flash('上传成功 Success upload.')
|
||||||
try:
|
try:
|
||||||
web.system.update_database()
|
FileChecker.update_database(
|
||||||
|
os.path.join(UPLOAD_FOLDER, filename))
|
||||||
flash('数据更新成功 Success update data.')
|
flash('数据更新成功 Success update data.')
|
||||||
except:
|
except:
|
||||||
flash('数据更新失败 Cannot update data.')
|
flash('数据更新失败 Cannot update data.')
|
||||||
@@ -288,9 +290,7 @@ def update_database():
|
|||||||
def update_song_hash():
|
def update_song_hash():
|
||||||
# 更新数据库内谱面文件hash值
|
# 更新数据库内谱面文件hash值
|
||||||
try:
|
try:
|
||||||
get_only_3_song_ids.cache_clear()
|
RefreshSongFileCache().run()
|
||||||
get_only_3_song_ids()
|
|
||||||
initialize_songfile()
|
|
||||||
flash('数据刷新成功 Success refresh data.')
|
flash('数据刷新成功 Success refresh data.')
|
||||||
except:
|
except:
|
||||||
flash('Something error!')
|
flash('Something error!')
|
||||||
@@ -301,10 +301,7 @@ def update_song_hash():
|
|||||||
@login_required
|
@login_required
|
||||||
def update_song_rating():
|
def update_song_rating():
|
||||||
# 更新所有分数的rating
|
# 更新所有分数的rating
|
||||||
error = server.arcscore.refresh_all_score_rating()
|
RefreshAllScoreRating().run()
|
||||||
if error:
|
|
||||||
flash(error)
|
|
||||||
else:
|
|
||||||
flash('数据刷新成功 Success refresh data.')
|
flash('数据刷新成功 Success refresh data.')
|
||||||
return render_template('web/updatedatabase.html')
|
return render_template('web/updatedatabase.html')
|
||||||
|
|
||||||
@@ -425,7 +422,7 @@ def all_character():
|
|||||||
def change_character():
|
def change_character():
|
||||||
# 修改角色数据
|
# 修改角色数据
|
||||||
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard']
|
'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']
|
||||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||||
|
|
||||||
|
|
||||||
@@ -609,7 +606,7 @@ def edit_user_purchase():
|
|||||||
if 'name' not in request.form and 'user_code' not in request.form:
|
if 'name' not in request.form and 'user_code' not in request.form:
|
||||||
flag = False
|
flag = False
|
||||||
if method == '0':
|
if method == '0':
|
||||||
web.system.unlock_all_user_item(c)
|
UnlockUserItem().run()
|
||||||
else:
|
else:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''delete from user_item where type in ('pack', 'single')''')
|
'''delete from user_item where type in ('pack', 'single')''')
|
||||||
@@ -635,7 +632,9 @@ def edit_user_purchase():
|
|||||||
user_id = user_id[0]
|
user_id = user_id[0]
|
||||||
|
|
||||||
if method == '0':
|
if method == '0':
|
||||||
web.system.unlock_user_item(c, user_id)
|
x = UnlockUserItem()
|
||||||
|
x.set_params(user_id=user_id)
|
||||||
|
x.run()
|
||||||
else:
|
else:
|
||||||
c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', {
|
c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', {
|
||||||
'user_id': user_id})
|
'user_id': user_id})
|
||||||
@@ -699,7 +698,7 @@ def change_item():
|
|||||||
c.execute(
|
c.execute(
|
||||||
'''select exists(select * from item where item_id=:a and type=:b)''', {'a': item_id, 'b': item_type})
|
'''select exists(select * from item where item_id=:a and type=:b)''', {'a': item_id, 'b': item_type})
|
||||||
if c.fetchone() == (0,):
|
if c.fetchone() == (0,):
|
||||||
c.execute('''insert into item values(?,?,?,'')''',
|
c.execute('''insert into item values(?,?,?)''',
|
||||||
(item_id, item_type, is_available))
|
(item_id, item_type, is_available))
|
||||||
flash('物品添加成功 Successfully add the item.')
|
flash('物品添加成功 Successfully add the item.')
|
||||||
else:
|
else:
|
||||||
@@ -941,7 +940,7 @@ def update_user_save():
|
|||||||
# 全修改
|
# 全修改
|
||||||
if 'name' not in request.form and 'user_code' not in request.form:
|
if 'name' not in request.form and 'user_code' not in request.form:
|
||||||
flag = False
|
flag = False
|
||||||
web.system.update_all_save(c)
|
SaveUpdateScore().run()
|
||||||
flash("全部用户存档同步成功 Successfully update all users' saves.")
|
flash("全部用户存档同步成功 Successfully update all users' saves.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -961,7 +960,9 @@ def update_user_save():
|
|||||||
user_id = c.fetchone()
|
user_id = c.fetchone()
|
||||||
if user_id:
|
if user_id:
|
||||||
user_id = user_id[0]
|
user_id = user_id[0]
|
||||||
web.system.update_one_save(c, user_id)
|
user = User()
|
||||||
|
user.user_id = user_id
|
||||||
|
SaveUpdateScore(user).run()
|
||||||
flash("用户存档同步成功 Successfully update the user's saves.")
|
flash("用户存档同步成功 Successfully update the user's saves.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#import sqlite3
|
|
||||||
from flask import (Blueprint, flash, g, redirect,
|
|
||||||
render_template, request, session, url_for)
|
|
||||||
import functools
|
import functools
|
||||||
from setting import Config
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
from core.config_manager import Config
|
||||||
|
from flask import (Blueprint, flash, g, redirect, render_template, request,
|
||||||
|
session, url_for)
|
||||||
|
|
||||||
bp = Blueprint('login', __name__, url_prefix='/web')
|
bp = Blueprint('login', __name__, url_prefix='/web')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import os
|
|
||||||
from core.sql import Connect
|
|
||||||
import time
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import time
|
||||||
from random import Random
|
from random import Random
|
||||||
from setting import Config
|
|
||||||
|
from core.sql import Connect
|
||||||
|
|
||||||
|
|
||||||
def int2b(x):
|
def int2b(x):
|
||||||
@@ -25,141 +24,8 @@ def random_str(randomlength=10):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def get_table_info(c, table_name):
|
|
||||||
# 得到表结构,返回主键列表和字段名列表
|
|
||||||
pk = []
|
|
||||||
name = []
|
|
||||||
|
|
||||||
c.execute('''pragma table_info ("'''+table_name+'''")''')
|
|
||||||
x = c.fetchall()
|
|
||||||
if x:
|
|
||||||
for i in x:
|
|
||||||
name.append(i[1])
|
|
||||||
if i[5] != 0:
|
|
||||||
pk.append(i[1])
|
|
||||||
|
|
||||||
return pk, name
|
|
||||||
|
|
||||||
|
|
||||||
def get_sql_select_table(table_name, get_field, where_field=[], where_value=[]):
|
|
||||||
# sql语句拼接,select ... from ... where ...
|
|
||||||
sql = 'select '
|
|
||||||
sql_dict = {}
|
|
||||||
if len(get_field) >= 2:
|
|
||||||
sql += get_field[0]
|
|
||||||
for i in range(1, len(get_field)):
|
|
||||||
sql += ',' + get_field[i]
|
|
||||||
sql += ' from ' + table_name
|
|
||||||
elif len(get_field) == 1:
|
|
||||||
sql += get_field[0] + ' from ' + table_name
|
|
||||||
else:
|
|
||||||
sql += '* from ' + table_name
|
|
||||||
|
|
||||||
if where_field and where_value:
|
|
||||||
sql += ' where '
|
|
||||||
sql += where_field[0] + '=:' + where_field[0]
|
|
||||||
sql_dict[where_field[0]] = where_value[0]
|
|
||||||
if len(where_field) >= 1:
|
|
||||||
for i in range(1, len(where_field)):
|
|
||||||
sql_dict[where_field[i]] = where_value[i]
|
|
||||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
|
||||||
|
|
||||||
sql += ' order by rowid'
|
|
||||||
return sql, sql_dict
|
|
||||||
|
|
||||||
|
|
||||||
def get_sql_insert_table(table_name, field, value):
|
|
||||||
# sql语句拼接,insert into ...(...) values(...)
|
|
||||||
sql = 'insert into ' + table_name + '('
|
|
||||||
sql_dict = {}
|
|
||||||
sql2 = ''
|
|
||||||
if len(field) >= 2:
|
|
||||||
sql += field[0]
|
|
||||||
sql2 += ':' + field[0]
|
|
||||||
sql_dict[field[0]] = value[0]
|
|
||||||
for i in range(1, len(field)):
|
|
||||||
sql += ',' + field[i]
|
|
||||||
sql2 += ', :' + field[i]
|
|
||||||
sql_dict[field[i]] = value[i]
|
|
||||||
|
|
||||||
sql += ') values('
|
|
||||||
|
|
||||||
elif len(field) == 1:
|
|
||||||
sql += field[0] + ') values('
|
|
||||||
sql2 += ':' + field[0]
|
|
||||||
sql_dict[field[0]] = value[0]
|
|
||||||
|
|
||||||
else:
|
|
||||||
return 'error', {}
|
|
||||||
|
|
||||||
sql += sql2 + ')'
|
|
||||||
|
|
||||||
return sql, sql_dict
|
|
||||||
|
|
||||||
|
|
||||||
def get_sql_delete_table(table_name, where_field=[], where_value=[]):
|
|
||||||
# sql语句拼接,delete from ... where ...
|
|
||||||
sql = 'delete from ' + table_name
|
|
||||||
sql_dict = {}
|
|
||||||
|
|
||||||
if where_field and where_value:
|
|
||||||
sql += ' where '
|
|
||||||
sql += where_field[0] + '=:' + where_field[0]
|
|
||||||
sql_dict[where_field[0]] = where_value[0]
|
|
||||||
if len(where_field) >= 1:
|
|
||||||
for i in range(1, len(where_field)):
|
|
||||||
sql_dict[where_field[i]] = where_value[i]
|
|
||||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
|
||||||
|
|
||||||
return sql, sql_dict
|
|
||||||
|
|
||||||
|
|
||||||
def update_one_table(c1, c2, table_name):
|
|
||||||
# 从c1向c2更新数据表,c1中存在的信息不变
|
|
||||||
c1.execute(
|
|
||||||
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
|
||||||
c2.execute(
|
|
||||||
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
|
||||||
if not c1.fetchone() or not c2.fetchone():
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
db1_pk, db1_name = get_table_info(c1, table_name)
|
|
||||||
db2_pk, db2_name = get_table_info(c2, table_name)
|
|
||||||
if db1_pk != db2_pk:
|
|
||||||
return 'error'
|
|
||||||
|
|
||||||
field = []
|
|
||||||
for i in db1_name:
|
|
||||||
if i in db2_name:
|
|
||||||
field.append(i)
|
|
||||||
|
|
||||||
sql, sql_dict = get_sql_select_table(table_name, db1_pk)
|
|
||||||
c1.execute(sql)
|
|
||||||
x = c1.fetchall()
|
|
||||||
sql, sql_dict = get_sql_select_table(table_name, field)
|
|
||||||
c1.execute(sql)
|
|
||||||
y = c1.fetchall()
|
|
||||||
if x:
|
|
||||||
for i in range(0, len(x)):
|
|
||||||
sql, sql_dict = get_sql_select_table(
|
|
||||||
table_name, [], db1_pk, list(x[i]))
|
|
||||||
sql = 'select exists(' + sql + ')'
|
|
||||||
c2.execute(sql, sql_dict)
|
|
||||||
|
|
||||||
if c2.fetchone() == (1,): # 如果c2里存在,先删除
|
|
||||||
sql, sql_dict = get_sql_delete_table(
|
|
||||||
table_name, db1_pk, list(x[i]))
|
|
||||||
c2.execute(sql, sql_dict)
|
|
||||||
|
|
||||||
sql, sql_dict = get_sql_insert_table(
|
|
||||||
table_name, field, list(y[i]))
|
|
||||||
c2.execute(sql, sql_dict)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
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''')
|
||||||
@@ -170,103 +36,10 @@ 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))
|
||||||
|
|
||||||
|
|
||||||
def update_user_epilogue(c):
|
|
||||||
c.execute('''select user_id from user''')
|
|
||||||
x = c.fetchall()
|
|
||||||
for i in x:
|
|
||||||
c.execute(
|
|
||||||
'''select exists(select * from user_item where user_id=? and item_id=? and type='pack')''', (i[0], 'epilogue'))
|
|
||||||
if c.fetchone() == (0,):
|
|
||||||
c.execute('''insert into user_item values(?,?,'pack',1)''',
|
|
||||||
(i[0], 'epilogue'))
|
|
||||||
|
|
||||||
|
|
||||||
def update_database():
|
|
||||||
# 将old数据库不存在数据加入到新数据库上,并删除old数据库
|
|
||||||
# 对于arcaea_datebase.db,更新一些表,并用character数据更新user_char_full
|
|
||||||
if os.path.isfile("database/old_arcaea_database.db") and os.path.isfile("database/arcaea_database.db"):
|
|
||||||
with Connect('./database/old_arcaea_database.db') as c1:
|
|
||||||
with Connect() as c2:
|
|
||||||
|
|
||||||
update_one_table(c1, c2, 'user')
|
|
||||||
update_one_table(c1, c2, 'friend')
|
|
||||||
update_one_table(c1, c2, 'best_score')
|
|
||||||
update_one_table(c1, c2, 'recent30')
|
|
||||||
update_one_table(c1, c2, 'user_world')
|
|
||||||
update_one_table(c1, c2, 'item')
|
|
||||||
update_one_table(c1, c2, 'user_item')
|
|
||||||
update_one_table(c1, c2, 'purchase')
|
|
||||||
update_one_table(c1, c2, 'purchase_item')
|
|
||||||
update_one_table(c1, c2, 'user_save')
|
|
||||||
update_one_table(c1, c2, 'login')
|
|
||||||
update_one_table(c1, c2, 'present')
|
|
||||||
update_one_table(c1, c2, 'user_present')
|
|
||||||
update_one_table(c1, c2, 'present_item')
|
|
||||||
update_one_table(c1, c2, 'redeem')
|
|
||||||
update_one_table(c1, c2, 'user_redeem')
|
|
||||||
update_one_table(c1, c2, 'redeem_item')
|
|
||||||
update_one_table(c1, c2, 'role')
|
|
||||||
update_one_table(c1, c2, 'user_role')
|
|
||||||
update_one_table(c1, c2, 'power')
|
|
||||||
update_one_table(c1, c2, 'role_power')
|
|
||||||
update_one_table(c1, c2, 'api_login')
|
|
||||||
update_one_table(c1, c2, 'chart')
|
|
||||||
update_one_table(c1, c2, 'user_course')
|
|
||||||
update_one_table(c1, c2, 'course')
|
|
||||||
update_one_table(c1, c2, 'course_item')
|
|
||||||
update_one_table(c1, c2, 'course_chart')
|
|
||||||
update_one_table(c1, c2, 'course_requirement')
|
|
||||||
|
|
||||||
update_one_table(c1, c2, 'user_char')
|
|
||||||
|
|
||||||
if not Config.UPDATE_WITH_NEW_CHARACTER_DATA:
|
|
||||||
update_one_table(c1, c2, 'character')
|
|
||||||
|
|
||||||
update_user_char(c2) # 更新user_char_full
|
|
||||||
update_user_epilogue(c2) # 更新user的epilogue
|
|
||||||
|
|
||||||
os.remove('database/old_arcaea_database.db')
|
|
||||||
|
|
||||||
|
|
||||||
def unlock_all_user_item(c):
|
|
||||||
# 解锁所有用户购买
|
|
||||||
|
|
||||||
c.execute('''select user_id from user''')
|
|
||||||
x = c.fetchall()
|
|
||||||
c.execute('''select item_id, type from purchase_item''')
|
|
||||||
y = c.fetchall()
|
|
||||||
if x and y:
|
|
||||||
for i in x:
|
|
||||||
for j in y:
|
|
||||||
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
|
|
||||||
'a': i[0], 'b': j[0], 'c': j[1]})
|
|
||||||
if c.fetchone() == (0,) and j[1] != 'character':
|
|
||||||
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
|
|
||||||
'a': i[0], 'b': j[0], 'c': j[1]})
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def unlock_user_item(c, user_id):
|
|
||||||
# 解锁用户购买
|
|
||||||
|
|
||||||
c.execute('''select item_id, type from purchase_item''')
|
|
||||||
y = c.fetchall()
|
|
||||||
|
|
||||||
for j in y:
|
|
||||||
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
|
|
||||||
'a': user_id, 'b': j[0], 'c': j[1]})
|
|
||||||
if c.fetchone() == (0,) and j[1] != 'character':
|
|
||||||
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
|
|
||||||
'a': user_id, 'b': j[0], 'c': j[1]})
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_item():
|
def get_all_item():
|
||||||
# 所有物品数据查询
|
# 所有物品数据查询
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
@@ -324,61 +97,6 @@ def get_all_purchase():
|
|||||||
return re
|
return re
|
||||||
|
|
||||||
|
|
||||||
def update_one_save(c, user_id):
|
|
||||||
# 同步指定用户存档
|
|
||||||
# 注意,best_score表不比较,直接覆盖
|
|
||||||
return
|
|
||||||
|
|
||||||
# c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', {
|
|
||||||
# 'a': user_id})
|
|
||||||
# x = c.fetchone()
|
|
||||||
# if x:
|
|
||||||
# scores = json.loads(x[0])[""]
|
|
||||||
# clearlamps = json.loads(x[1])[""]
|
|
||||||
# clear_song_id_difficulty = []
|
|
||||||
# clear_state = []
|
|
||||||
# for i in clearlamps:
|
|
||||||
# clear_song_id_difficulty.append(i['song_id']+str(i['difficulty']))
|
|
||||||
# clear_state.append(i['clear_type'])
|
|
||||||
|
|
||||||
# for i in scores:
|
|
||||||
# rating = server.arcscore.get_one_ptt(
|
|
||||||
# i['song_id'], i['difficulty'], i['score'])
|
|
||||||
# if rating < 0:
|
|
||||||
# rating = 0
|
|
||||||
# try:
|
|
||||||
# index = clear_song_id_difficulty.index(
|
|
||||||
# i['song_id'] + str(i['difficulty']))
|
|
||||||
# except:
|
|
||||||
# index = -1
|
|
||||||
# if index != -1:
|
|
||||||
# clear_type = clear_state[index]
|
|
||||||
# else:
|
|
||||||
# clear_type = 0
|
|
||||||
# c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', {
|
|
||||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty']})
|
|
||||||
# c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', {
|
|
||||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating})
|
|
||||||
|
|
||||||
# ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT
|
|
||||||
# c.execute('''update user set rating_ptt=:a where user_id=:b''', {
|
|
||||||
# 'a': ptt, 'b': user_id})
|
|
||||||
|
|
||||||
# return
|
|
||||||
|
|
||||||
|
|
||||||
def update_all_save(c):
|
|
||||||
# 同步所有用户存档
|
|
||||||
|
|
||||||
c.execute('''select user_id from user_save''')
|
|
||||||
x = c.fetchall()
|
|
||||||
if x:
|
|
||||||
for i in x:
|
|
||||||
update_one_save(c, i[0])
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount):
|
def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount):
|
||||||
# 添加一个奖励
|
# 添加一个奖励
|
||||||
|
|
||||||
|
|||||||
121
tools/clean_logdb.py
Normal file
121
tools/clean_logdb.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from time import mktime
|
||||||
|
|
||||||
|
LOG_DATABASE_PATH = './arcaea_log.db'
|
||||||
|
|
||||||
|
|
||||||
|
class Connect():
|
||||||
|
# 数据库连接类,上下文管理
|
||||||
|
|
||||||
|
def __init__(self, file_path=LOG_DATABASE_PATH):
|
||||||
|
"""
|
||||||
|
数据库连接
|
||||||
|
接受:文件路径
|
||||||
|
返回:sqlite3连接操作对象
|
||||||
|
"""
|
||||||
|
self.file_path = file_path
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.conn = sqlite3.connect(self.file_path)
|
||||||
|
self.c = self.conn.cursor()
|
||||||
|
return self.c
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||||
|
if exc_type is not None:
|
||||||
|
print(f'exc_type: {exc_type}')
|
||||||
|
print(f'exc_val: {exc_val}')
|
||||||
|
print(f'exc_tb: {exc_tb}')
|
||||||
|
if self.conn:
|
||||||
|
self.conn.rollback()
|
||||||
|
|
||||||
|
if self.conn:
|
||||||
|
self.conn.commit()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clean_by_time(table_name: str, time_colume_name: str, colume_count: int):
|
||||||
|
with Connect() as c:
|
||||||
|
today = datetime.now()
|
||||||
|
print(f'The time now is {today}.')
|
||||||
|
day = input(
|
||||||
|
'Please input the number of days the data before which you want to delete: ')
|
||||||
|
try:
|
||||||
|
day = int(day)
|
||||||
|
except ValueError:
|
||||||
|
print('Invalid input!')
|
||||||
|
return
|
||||||
|
time = mktime(today.timetuple()) - day * 24 * 60 * 60
|
||||||
|
delete_count = c.execute(
|
||||||
|
f'select count(*) from {table_name} where {time_colume_name} < ?', (time,)).fetchone()[0]
|
||||||
|
all_count = c.execute(
|
||||||
|
f'select count(*) from {table_name}').fetchone()[0]
|
||||||
|
print(
|
||||||
|
f'Before {day} days, there are {delete_count} records to be deleted, {all_count} records in total.')
|
||||||
|
flag = input('Are you sure to delete these records? (y/n) ')
|
||||||
|
if flag == 'y' or flag == 'Y':
|
||||||
|
if delete_count >= 1000000:
|
||||||
|
print(
|
||||||
|
'It will cost a long time to delete these records, please wait patiently...')
|
||||||
|
print('Deleting...')
|
||||||
|
c.execute('PRAGMA cache_size = 32768')
|
||||||
|
c.execute('PRAGMA synchronous = OFF')
|
||||||
|
c.execute('PRAGMA temp_store = MEMORY')
|
||||||
|
if delete_count / all_count >= 0.8:
|
||||||
|
data = c.execute(
|
||||||
|
f'select * from {table_name} where {time_colume_name} > ?', (time,)).fetchall()
|
||||||
|
c.execute(f'delete from {table_name}')
|
||||||
|
c.executemany(
|
||||||
|
f'insert into {table_name} values ({",".join(["?"]*colume_count)})', data)
|
||||||
|
else:
|
||||||
|
c.execute(
|
||||||
|
f'delete from {table_name} where {time_colume_name} < ?', (time,))
|
||||||
|
c.execute('PRAGMA temp_store = DEFAULT')
|
||||||
|
print('Delete successfully!')
|
||||||
|
else:
|
||||||
|
print('Delete canceled!')
|
||||||
|
|
||||||
|
|
||||||
|
def vacuum():
|
||||||
|
print('This operation will release unused space in the database file.')
|
||||||
|
print('It will cost a long time to release unused space if the database file is so large.')
|
||||||
|
flag = input('Are you sure to release unused space? (y/n) ')
|
||||||
|
if flag == 'y' or flag == 'Y':
|
||||||
|
with Connect() as c:
|
||||||
|
print('Releasing unused space...')
|
||||||
|
c.execute('vacuum')
|
||||||
|
print('Release unused space successfully!')
|
||||||
|
else:
|
||||||
|
print('Release unused space canceled!')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(LOG_DATABASE_PATH):
|
||||||
|
print('The database file `arcaea_log.db` does not exist!')
|
||||||
|
print('-- Arcaea Server Log Database Cleaner --')
|
||||||
|
print('Note: It is more recommended to delete the database file directly.')
|
||||||
|
while True:
|
||||||
|
print('-' * 40)
|
||||||
|
print('1. clean `user_score` table')
|
||||||
|
print('2. clean `user_rating` table')
|
||||||
|
print('3. release unused space (`vacuum` command)')
|
||||||
|
print('0. exit')
|
||||||
|
choice = input('Please input your choice: ')
|
||||||
|
if choice == '1':
|
||||||
|
clean_by_time('user_score', 'time_played', 13)
|
||||||
|
elif choice == '2':
|
||||||
|
clean_by_time('user_rating', 'time', 3)
|
||||||
|
elif choice == '3':
|
||||||
|
vacuum()
|
||||||
|
elif choice == '0':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print('Invalid choice!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
input('Press `Enter` key to exit.')
|
||||||
Reference in New Issue
Block a user