mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-08 16:57:26 +08:00
Compare commits
232 Commits
v2.3
...
d11a8435b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d11a8435b3 | ||
|
|
17998ee655 | ||
|
|
39e1e52d36 | ||
|
|
52bb834a8a | ||
|
|
65f6d212fe | ||
|
|
9c27b998ef | ||
|
|
a3891a901f | ||
|
|
32b708b7de | ||
|
|
eee34bae80 | ||
|
|
029d43e3f5 | ||
|
|
05bdd42679 | ||
|
|
5b5f544a19 | ||
|
|
d05618049c | ||
|
|
2e43dfbdff | ||
|
|
7f94416189 | ||
|
|
4911511ed7 | ||
|
|
46b3b053cf | ||
|
|
6c6a13086f | ||
|
|
97b86a4381 | ||
|
|
a3bc071416 | ||
|
|
c4da67cd94 | ||
|
|
3424ad67cb | ||
|
|
ecfb360228 | ||
|
|
5a37766cb9 | ||
|
|
a23e5372fb | ||
|
|
44d7d7e490 | ||
|
|
3f5281582c | ||
|
|
a6c26dedfe | ||
|
|
6f964d7a78 | ||
|
|
f3a1a5e0a9 | ||
|
|
bd40704fa7 | ||
|
|
9c9af892bd | ||
|
|
dc1ca344e5 | ||
|
|
c52fda6f47 | ||
|
|
4c40d8e21f | ||
|
|
27567b8a5c | ||
|
|
10a816b161 | ||
|
|
c3af92f29b | ||
|
|
014531f3f1 | ||
|
|
59422f96b5 | ||
|
|
9bb7171354 | ||
|
|
c38208832b | ||
|
|
64526e3c4b | ||
|
|
dd0bc64c93 | ||
|
|
2a08c9cd14 | ||
|
|
338f6579aa | ||
|
|
9636cfcae8 | ||
|
|
5c539bdf59 | ||
|
|
efedd96908 | ||
|
|
2d498ae02b | ||
|
|
ab201c9d48 | ||
|
|
02bf565ebd | ||
|
|
a05bb4854d | ||
|
|
328be5b923 | ||
|
|
2f826e9ffc | ||
|
|
eeced74f50 | ||
|
|
8f66b90912 | ||
|
|
d65cc3bcbe | ||
|
|
e206247c09 | ||
|
|
b3bf55407f | ||
|
|
50369d1f23 | ||
|
|
9d096f480b | ||
|
|
2edf559373 | ||
|
|
746712718b | ||
|
|
de701b4d1b | ||
|
|
b6663ac0dc | ||
|
|
de1d46f96e | ||
|
|
f3c17cdde7 | ||
|
|
150686d9f8 | ||
|
|
3e93082a3c | ||
|
|
04010d9c33 | ||
|
|
1f389e4073 | ||
|
|
5788972692 | ||
|
|
f373c7b052 | ||
|
|
04c6d1a0e0 | ||
|
|
41409dee27 | ||
|
|
4527339d78 | ||
|
|
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 | ||
|
|
2a3ee0f0c7 | ||
|
|
a04df8bba6 | ||
|
|
fb110fd01c | ||
|
|
7a614eaa57 | ||
|
|
8f1c28eb5c | ||
|
|
299c8c582c | ||
|
|
6298b37ea2 | ||
|
|
b1575af0db | ||
|
|
7bf43c3e07 | ||
|
|
12cd187563 | ||
|
|
574d2c9d9f | ||
|
|
732947a3f0 | ||
|
|
27ee147989 | ||
|
|
b38dbf1a55 | ||
|
|
d852be8029 | ||
|
|
4f52df35ce | ||
|
|
cbd2198e43 | ||
|
|
cdf76fb93a | ||
|
|
5fba93524c | ||
|
|
93f4ad4999 | ||
|
|
47f05cdf1e | ||
|
|
dbfed37dda | ||
|
|
6801af197f | ||
|
|
210ba99606 | ||
|
|
0705c35be8 | ||
|
|
c9bcbb8ec4 | ||
|
|
ac0071bfd6 | ||
|
|
ef8e551261 | ||
|
|
55ab6c43cb | ||
|
|
15134173c9 | ||
|
|
9f41ef0a70 | ||
|
|
af3e91b3e8 | ||
|
|
9d746eab2d | ||
|
|
6fcca17918 | ||
|
|
9de62d3645 | ||
|
|
96fbd26342 | ||
|
|
aa77a4c903 | ||
|
|
6075bda5d8 | ||
|
|
409c2c032b | ||
|
|
e8c15f5069 | ||
|
|
35c34f25d5 | ||
|
|
4b8c5a520e | ||
|
|
1235733ddf | ||
|
|
cb6425a0d1 | ||
|
|
3a1c731e24 | ||
|
|
5de274fdf5 | ||
|
|
b60457c38b | ||
|
|
35b954e549 | ||
|
|
ceebba4664 | ||
|
|
5bc9b9a3dc | ||
|
|
d9a543bc5a | ||
|
|
4666c9c153 | ||
|
|
32bcbb0ccd | ||
|
|
34497d0638 | ||
|
|
23bf3c020f | ||
|
|
e1fc1bbcd1 | ||
|
|
355ec3557f | ||
|
|
cc4ac192e7 | ||
|
|
5068c7215c | ||
|
|
99f05cabb7 | ||
|
|
1a097bc4d0 | ||
|
|
849f4f7260 | ||
|
|
add81ee639 | ||
|
|
b927ad23f8 | ||
|
|
8bc74b36c7 | ||
|
|
7dc8bfea9a | ||
|
|
d0be49fe20 | ||
|
|
b494b93c14 | ||
|
|
a523ff5aae | ||
|
|
acbe9fc4a1 | ||
|
|
9422bf7182 | ||
|
|
53acad4ffb | ||
|
|
13048e57f4 | ||
|
|
39d3f073ba | ||
|
|
2cf6595478 | ||
|
|
7c3bc99570 | ||
|
|
168e5f0b12 | ||
|
|
9286caf900 | ||
|
|
b0647bedb6 | ||
|
|
f49a868707 | ||
|
|
28bca25092 | ||
|
|
0227d2b44a | ||
|
|
0d95b886bc | ||
|
|
9b61acd163 | ||
|
|
be134bbc6e | ||
|
|
c8d0463b9e | ||
|
|
871c2082a8 | ||
|
|
14b685850c | ||
|
|
3fbf473de2 | ||
|
|
8a3cec05b7 | ||
|
|
e3d81436d0 | ||
|
|
941a79ccc7 | ||
|
|
62902a561c | ||
|
|
1660bc5899 | ||
|
|
d577246d28 | ||
|
|
9fd626468e | ||
|
|
c51a1b4e85 | ||
|
|
0e42123417 | ||
|
|
e216e6144b | ||
|
|
b70c435a4a | ||
|
|
eb03d2be93 | ||
|
|
fcb5d264ef | ||
|
|
d617951e1f | ||
|
|
febf931f1a | ||
|
|
981961d6a1 |
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use a clear and concise title 能不能来个正常点的标题 -->
|
||||
<!-- English or Chinese is ok 中英皆可 -->
|
||||
|
||||
**Describe the bug 问题描述**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce 复现过程**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Error log 报错日志**
|
||||
<!-- Including logs, texts in CMD, some messages in some softwares... -->
|
||||
```
|
||||
[Error] Balabalabalabala...
|
||||
```
|
||||
|
||||
**Screenshots 截图**
|
||||
<!-- Well, if you have error logs, pictures are optional. It is too slow for me to open a picture. -->
|
||||
<!-- 有错误日志就别放图了,网速有多慢你懂的 -->
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment 环境**
|
||||
- Server OS: [e.g. Windows 11 Beta]
|
||||
- Client OS: [e.g. Android 10]
|
||||
- Server Version: [e.g. v2.8.1]
|
||||
- Client Version [e.g. 4.0.256c]
|
||||
- Others like python version, flask version, some proxy softwares if you used...
|
||||
|
||||
**Additional context 附加内容**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
14
.github/ISSUE_TEMPLATE/q-a.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/q-a.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Q&A
|
||||
about: Ask a question or wonder something
|
||||
title: ''
|
||||
labels: Q&A
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What you want to know 你想知道什么**
|
||||
Clearly describe your question here.
|
||||
|
||||
**Additional context 附加内容**
|
||||
Add anything other here. Your abilities or knowledge base, your contact information, some pictures...
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -1 +1,29 @@
|
||||
# log files
|
||||
*.log
|
||||
*.log.*
|
||||
|
||||
# SSL cert
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# sqlite3 database
|
||||
*.db
|
||||
*.db.bak*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# setting/config files
|
||||
latest version/config/
|
||||
latest version/config.py
|
||||
|
||||
# song data
|
||||
latest version/database/songs/
|
||||
latest version/database/bundle/
|
||||
|
||||
!latest version/database/bundle/README.md
|
||||
|
||||
# backup
|
||||
latest version/database/backup/
|
||||
|
||||
165
README.md
165
README.md
@@ -1,8 +1,10 @@
|
||||
# Arcaea-server
|
||||
一个微型的Arcaea本地服务器 A small local server for Arcaea
|
||||
|
||||
一个微型的 Arcaea 本地服务器 A small local server for Arcaea
|
||||
|
||||
## 简介 Introduction
|
||||
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||
|
||||
这是基于 Python 以及 Flask 的微型本地 Arcaea 服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||
|
||||
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
||||
|
||||
@@ -14,96 +16,165 @@ This procedure is mainly used for study and research, and shall not be used for
|
||||
> It looks stupid, but it works!
|
||||
|
||||
## 特性 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
|
||||
- 多设备登录 Multi device login
|
||||
- 多设备自动封号 Auto-ban of multiple devices
|
||||
- :warning: 多设备登录 Multi device login
|
||||
- 登录频次限制 Login rate limit
|
||||
- 注册频次限制 Register rate limit
|
||||
- :warning: 销号 Destroy account
|
||||
- 成绩上传 Score upload
|
||||
- PTT
|
||||
- 排名 Rank
|
||||
- 成绩校验 Score check
|
||||
- 成绩排名 Score rank
|
||||
- 潜力值机制 Potential
|
||||
- Best 30
|
||||
- :warning: Recent Top 10
|
||||
- 世界排名 Global rank
|
||||
- 段位系统 Course system
|
||||
- :warning: Link Play 2.0
|
||||
- 好友系统 Friends
|
||||
- 数据同步 Data synchronization
|
||||
- 爬梯 Climbing steps
|
||||
- 自定义世界模式 Customizable World Mode
|
||||
- 自定义歌曲下载 Customizable songs download
|
||||
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
|
||||
- :x: 好友位提升 Max friend number increase
|
||||
- 云端存档 Cloud save
|
||||
- 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
|
||||
- 世界模式 World mode
|
||||
- 体力系统 Stamina system
|
||||
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
|
||||
- :warning: 角色技能 Character skills
|
||||
- 歌曲下载 Songs downloading
|
||||
- :x: 加密下载 Encrypted downloading
|
||||
- 下载校验 Download check
|
||||
- 下载频次限制 Download rate limit
|
||||
- 内容捆绑包热更新 Content bundle hot update
|
||||
- 购买系统 Purchase system
|
||||
- 单曲和曲包 Single & Pack
|
||||
- :x: 捆绑包 Pack bundle
|
||||
- 折扣 Discount
|
||||
- 五周年兑换券 5-th anniversary ticket
|
||||
- 单曲兑换券 Pick ticket
|
||||
- :x: Extend 包自动降价 Extend pack automatic price reduction
|
||||
- 奖励系统 Present system
|
||||
- 兑换码系统 Redeem code system
|
||||
- 全角色立绘 All character drawings
|
||||
- 角色技能 Character skills
|
||||
- 自定义角色属性 Customizable characters attributes
|
||||
- 全剧情解锁 Unlock all the stories
|
||||
- 后台查询 Background search
|
||||
- 后台自定义信息 Customize some things in the background
|
||||
- 成绩校验 Score check
|
||||
- 下载校验 Download check
|
||||
- 新手任务 Missions
|
||||
- 角色系统 Character system
|
||||
- 数据记录 Data recording
|
||||
- 用户成绩 Users' scores
|
||||
- 用户每日潜力值 Users' daily potential
|
||||
- :wastebasket: 简单的网页管理后台 Simple web admin backend
|
||||
- :construction: API
|
||||
- 服务器日志 Server log
|
||||
|
||||
没有以下 We don't have:
|
||||
- 角色数值 Character characteristic value
|
||||
- 服务器安全性保证 Server security assurance
|
||||
|
||||
可能有问题 There may be problems:
|
||||
- Recent 30
|
||||
- 一些歌曲的解锁 Some songs' unlocking
|
||||
- 同设备多共存登录 Multiple app logins on the same device
|
||||
|
||||
## 说明 Statement
|
||||
|
||||
只是很有趣,用处探索中。
|
||||
It is just so interesting. What it can do is under exploration.
|
||||
|
||||
|
||||
## 下载 Download
|
||||
|
||||
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
|
||||
|
||||
[Arcaea](https://konmai.cn/#arcaea)
|
||||
[Arcaea-CN official](https://arcaea.lowiro.com/zh)
|
||||
|
||||
## 更新日志 Update log
|
||||
|
||||
只保留最新版本 Only keep the latest version.
|
||||
|
||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失
|
||||
>
|
||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
||||
> 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.3
|
||||
- 适用于Arcaea 3.5.2版本 For Arcaea 3.5.2
|
||||
- 更新了歌曲数据库 Update the song database.
|
||||
- 新增服务器日志 Add server log.
|
||||
- 优化了代码结构 Optimize the code structure.
|
||||
- 修复了一些Bug,提升服务器安全性 Fix some bugs and improve server security.
|
||||
### Version 2.12.1
|
||||
|
||||
> v2.12.0.1 ~ v2.12.0.10 for Arcaea 5.10.6 ~ 6.7.1
|
||||
>
|
||||
> Here are not all bug fixes and features.
|
||||
|
||||
- 适用于 Arcaea 6.7.1 版本
|
||||
For Arcaea 6.7.1
|
||||
- 添加一些新搭档和搭档的觉醒形态,并支持他们的技能
|
||||
Add some new partners, uncap some others, and add support for their skills.
|
||||
- 支持“光 (Fatalis)”的由世界模式总台阶数决定的动态搭档数值
|
||||
Add support for dynamic values of "Hikari (Fatalis)", which is depended by world mode total steps.
|
||||
- 支持新版本客户端的账户注册和登录接口
|
||||
Adapt to new account registration and login endpoints of the new version client.
|
||||
- 修复 Link Play 房间中玩家历史分数异常的问题
|
||||
Fix a bug that if players do not start the first play in one room, their scores will be wrong.
|
||||
- 修复因为歌曲 idx 数值过大导致服务端错误,致使 Link Play 模块无法工作的问题
|
||||
Fix a bug that if song's idx is too big, the server will catch the error, which makes Link Play cannot work.
|
||||
- 修复谱面难度 ETR 相关问题
|
||||
Fix some bugs about the ETR difficulty.
|
||||
|
||||
## 运行环境与依赖 Running environment and requirements
|
||||
- Windows/Linux/Mac OS/Android
|
||||
- Python 3
|
||||
- Flask module
|
||||
- Charles (optional)
|
||||
|
||||
## 环境搭建 Environment construction
|
||||
- Windows / Linux / Mac OS / Android...
|
||||
- Python >= 3.6
|
||||
- Flask >= 2.0
|
||||
- Cryptography >= 35.0.0
|
||||
- limits >= 2.7.0
|
||||
- Charles, IDA, proxy app... (optional)
|
||||
|
||||
## 子项目 Sub repositories
|
||||
|
||||
[Arcaea-Server-Wiki](https://arcaea.lost-msth.cn/Arcaea-Server/)
|
||||
: 项目文档 Project documentation
|
||||
|
||||
[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler)
|
||||
: 用于生成和解包内容捆绑包 Used to pack or unpack content bundles
|
||||
|
||||
[Arcaea-Server-Frontend](https://github.com/Lost-MSth/arcaea_server_frontend)
|
||||
: In building
|
||||
|
||||
## 旧的说明 Old wiki
|
||||
|
||||
<!--
|
||||
### 环境搭建 Environment construction
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Environment-construction)
|
||||
-->
|
||||
|
||||
### 使用说明 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)
|
||||
[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)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
|
||||
|
||||
### Q&A
|
||||
|
||||
[中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
|
||||
|
||||
## 鸣谢 Thanks
|
||||
|
||||
歌曲数据库来自 Using song database from
|
||||
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)
|
||||
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
|
||||
[ArcaeaSongDatabase Fork](https://github.com/CuSO4Deposit/ArcaeaSongDatabase)
|
||||
|
||||
> 从v2.9开始不再提供歌曲数据
|
||||
> Since v2.9, song data will not be provided.
|
||||
|
||||
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
|
||||
|
||||
## 联系方式 Contact
|
||||
|
||||
如有必要,可以联系本人 Contact me if necessary
|
||||
邮箱 Email:th84292@foxmail.com
|
||||
邮箱 Email:arcaea@lost-msth.cn
|
||||
|
||||
## 支持一下 Support me
|
||||
|
||||
生活不易。 Life is not easy.
|
||||
[支付宝 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)
|
||||
|
||||
## 使用许可 Use license
|
||||
|
||||
[MIT](LICENSE) © Lost
|
||||
|
||||
10
latest version/api/__init__.py
Normal file
10
latest version/api/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from . import (users, songs, token, system, items,
|
||||
purchases, presents, redeems, characters, multiplay)
|
||||
|
||||
bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
l = [users, songs, token, system, items, purchases,
|
||||
presents, redeems, characters, multiplay]
|
||||
for i in l:
|
||||
bp.register_blueprint(i.bp)
|
||||
123
latest version/api/api_auth.py
Normal file
123
latest version/api/api_auth.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from base64 import b64decode
|
||||
from functools import wraps
|
||||
from json import loads
|
||||
from traceback import format_exc
|
||||
|
||||
from flask import current_app
|
||||
|
||||
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 .constant import Constant
|
||||
|
||||
|
||||
def role_required(request, powers=[]):
|
||||
'''api token验证,写成了修饰器'''
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
if request.data:
|
||||
request.json # 检查请求json格式
|
||||
except:
|
||||
return error_return(PostError('Payload must be a valid json', api_error_code=-1), 400)
|
||||
|
||||
if not 'Token' in request.headers:
|
||||
return error_return(PostError('No token', api_error_code=-1), 401)
|
||||
|
||||
user = APIUser()
|
||||
with Connect() as 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(
|
||||
request.headers['Token'])
|
||||
user.select_role_and_powers()
|
||||
|
||||
if not any(user.role.has_power(y) for y in powers):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
except ArcError as e:
|
||||
return error_return(e, 401)
|
||||
|
||||
return view(user, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
|
||||
def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False):
|
||||
'''
|
||||
提取post参数,返回dict,写成了修饰器
|
||||
|
||||
parameters:
|
||||
`request`: `Request` - 当前请求
|
||||
`required_keys`: `list` - 必须的参数
|
||||
`optional_keys`: `list` - 可选的参数
|
||||
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
|
||||
'''
|
||||
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
data = {}
|
||||
if request.data:
|
||||
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:
|
||||
if key not in json_data:
|
||||
return error_return(InputError(f'Missing parameter: {key}', api_error_code=-100))
|
||||
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:
|
||||
if key in json_data:
|
||||
data[key] = json_data[key]
|
||||
|
||||
if must_change and not data:
|
||||
return error_return(InputError('No change', api_error_code=-100))
|
||||
|
||||
return view(data, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
|
||||
def api_try(view):
|
||||
'''替代try/except,记录`ArcError`为warning'''
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
data = view(*args, **kwargs)
|
||||
if data is None:
|
||||
return error_return()
|
||||
return data
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
current_app.logger.warning(format_exc())
|
||||
return error_return(e, e.status)
|
||||
|
||||
return wrapped_view
|
||||
45
latest version/api/api_code.py
Normal file
45
latest version/api/api_code.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from flask import jsonify
|
||||
|
||||
from core.error import ArcError
|
||||
|
||||
default_error = ArcError('Unknown Error')
|
||||
|
||||
|
||||
CODE_MSG = {
|
||||
0: '',
|
||||
-1: 'See status code', # 基础错误
|
||||
-2: 'No data',
|
||||
-3: 'No data or user', # 不确定是无数据还是无用户
|
||||
-4: 'Data exist',
|
||||
-100: 'Invalid post data', # 1xx数据错误
|
||||
-101: 'Invalid data type',
|
||||
-102: 'Invalid query parameter',
|
||||
-103: 'Invalid sort parameter',
|
||||
-104: 'Invalid sort order parameter',
|
||||
-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',
|
||||
-202: 'User is banned',
|
||||
-203: 'Too many login attempts',
|
||||
-210: 'Username exists',
|
||||
-211: 'Email address exists',
|
||||
-212: 'User code exists',
|
||||
-213: 'Too many register attempts',
|
||||
-999: 'Unknown error'
|
||||
}
|
||||
|
||||
|
||||
def success_return(data: dict = {}, status: int = 200, msg: str = ''):
|
||||
return jsonify({'code': 0, 'data': data, 'msg': msg}), status
|
||||
|
||||
|
||||
def error_return(e: 'ArcError' = default_error, status: int = 200):
|
||||
return jsonify({'code': e.api_error_code, 'data': {} if e.extra_data is None else e.extra_data, 'msg': CODE_MSG[e.api_error_code] if e.message is None else e.message}), status
|
||||
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())
|
||||
7
latest version/api/constant.py
Normal file
7
latest version/api/constant.py
Normal file
@@ -0,0 +1,7 @@
|
||||
class Constant:
|
||||
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])
|
||||
115
latest version/api/songs.py
Normal file
115
latest version/api/songs.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.error import DataExist, InputError, NoData
|
||||
from core.rank import RankList
|
||||
from core.song import Song
|
||||
from core.sql import Connect, Query, Sql
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import success_return
|
||||
from .constant import Constant
|
||||
|
||||
bp = Blueprint('songs', __name__, url_prefix='/songs')
|
||||
|
||||
|
||||
@bp.route('/<string:song_id>', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_song_info'])
|
||||
@api_try
|
||||
def songs_song_get(user, song_id):
|
||||
'''查询歌曲信息'''
|
||||
with Connect() as c:
|
||||
s = Song(c, song_id).select()
|
||||
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'])
|
||||
@role_required(request, ['select', 'select_song_info'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def songs_get(data, user):
|
||||
'''查询全歌曲信息'''
|
||||
A = ['song_id', 'name']
|
||||
B = ['song_id', 'name', 'rating_pst',
|
||||
'rating_prs', 'rating_ftr', 'rating_byn', 'rating_etr']
|
||||
with Connect() as c:
|
||||
query = Query(A, A, B).from_dict(data)
|
||||
x = Sql(c).select('chart', query=query)
|
||||
r = []
|
||||
for i in x:
|
||||
r.append(Song(c).from_list(i))
|
||||
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
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, 4]:
|
||||
raise InputError('Difficulty must be 0, 1, 2, 3 or 4')
|
||||
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()
|
||||
55
latest version/api/token.py
Normal file
55
latest version/api/token.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from base64 import b64decode
|
||||
|
||||
from flask import Blueprint, current_app, request
|
||||
|
||||
from core.api_user import APIUser
|
||||
from core.error import PostError
|
||||
from core.sql import Connect
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import success_return
|
||||
|
||||
bp = Blueprint('token', __name__, url_prefix='/token')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@request_json_handle(request, required_keys=['auth'])
|
||||
@api_try
|
||||
def token_post(data):
|
||||
'''
|
||||
登录,获取token
|
||||
|
||||
{'auth': base64('<user_id>:<password>')}
|
||||
'''
|
||||
try:
|
||||
auth_decode = bytes.decode(b64decode(data['auth']))
|
||||
except Exception as e:
|
||||
raise PostError(api_error_code=-100) from e
|
||||
if ':' not in auth_decode:
|
||||
raise PostError(api_error_code=-100)
|
||||
name, password = auth_decode.split(':', 1)
|
||||
|
||||
with Connect() as c:
|
||||
user = APIUser(c)
|
||||
user.login(name, password, request.remote_addr)
|
||||
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'])
|
||||
@role_required(request, ['select_me', 'select'])
|
||||
@api_try
|
||||
def token_get(user):
|
||||
'''判断登录有效性'''
|
||||
return success_return()
|
||||
|
||||
|
||||
@bp.route('', methods=['DELETE'])
|
||||
@role_required(request, ['change_me', 'select_me', 'select'])
|
||||
@api_try
|
||||
def token_delete(user):
|
||||
'''登出'''
|
||||
with Connect() as c:
|
||||
user.c = c
|
||||
user.logout()
|
||||
return success_return()
|
||||
233
latest version/api/users.py
Normal file
233
latest version/api/users.py
Normal file
@@ -0,0 +1,233 @@
|
||||
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.score import Potential, UserScoreList
|
||||
from core.sql import Connect, Query, Sql
|
||||
from core.user import UserChanger, UserInfo, UserRegister
|
||||
from core.util import get_today_timestamp
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import error_return, success_return
|
||||
from .constant import Constant
|
||||
|
||||
bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, ['name', 'password', 'email'])
|
||||
@api_try
|
||||
def users_post(data, user):
|
||||
'''注册一个用户'''
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
new_user.set_name(data['name'])
|
||||
new_user.set_password(data['password'])
|
||||
new_user.set_email(data['email'])
|
||||
new_user.register()
|
||||
return success_return({'user_id': new_user.user_id, 'user_code': new_user.user_code})
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@role_required(request, ['select'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def users_get(data, user):
|
||||
'''查询全用户信息'''
|
||||
A = ['user_id', 'name', 'user_code']
|
||||
B = ['user_id', 'name', 'user_code', 'join_date',
|
||||
'rating_ptt', 'time_played', 'ticket', 'world_rank_score']
|
||||
with Connect() as c:
|
||||
query = Query(A, A, B).from_dict(data)
|
||||
x = Sql(c).select('user', query=query)
|
||||
r = []
|
||||
for i in x:
|
||||
r.append(UserInfo(c).from_list(i))
|
||||
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
return success_return([{
|
||||
'user_id': x.user_id,
|
||||
'name': x.name,
|
||||
'join_date': x.join_date,
|
||||
'user_code': x.user_code,
|
||||
'rating_ptt': x.rating_ptt,
|
||||
'character_id': x.character.character_id,
|
||||
'is_char_uncapped': x.character.is_uncapped,
|
||||
'is_char_uncapped_override': x.character.is_uncapped_override,
|
||||
'is_hide_rating': x.is_hide_rating,
|
||||
'ticket': x.ticket
|
||||
} for x in r])
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_get(user, user_id):
|
||||
'''查询用户信息'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要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)
|
||||
|
||||
with Connect() as c:
|
||||
u = UserInfo(c, user_id)
|
||||
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', 'custom_banner'], 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
|
||||
if 'custom_banner' in data:
|
||||
if not isinstance(data['custom_banner'], str):
|
||||
raise InputError('Value `custom_banner` must be str')
|
||||
u.custom_banner = data['custom_banner']
|
||||
r['custom_banner'] = u.custom_banner
|
||||
u.update_columns(d=r)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/b30', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_b30_get(user, user_id):
|
||||
'''查询用户b30'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要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)
|
||||
|
||||
with Connect() as c:
|
||||
x = UserScoreList(c, UserInfo(c, user_id))
|
||||
x.query.limit = 30
|
||||
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()
|
||||
r = x.to_dict_list()
|
||||
rating_sum = sum(i.rating for i in x.scores)
|
||||
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/best', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def users_user_best_get(data, user, user_id):
|
||||
'''查询用户所有best成绩'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要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)
|
||||
|
||||
with Connect() as c:
|
||||
x = UserScoreList(c, UserInfo(c, user_id))
|
||||
x.query.from_dict(data)
|
||||
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()
|
||||
return success_return({'user_id': user_id, 'data': r})
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/r30', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_r30_get(user, user_id):
|
||||
'''查询用户r30'''
|
||||
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要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)
|
||||
|
||||
with Connect() as c:
|
||||
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()})
|
||||
|
||||
|
||||
@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]})
|
||||
259
latest version/config.example.py
Normal file
259
latest version/config.example.py
Normal file
@@ -0,0 +1,259 @@
|
||||
class Config():
|
||||
'''
|
||||
This is the example setting file.
|
||||
The user's setting file's name is `config.py`.
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
主机的地址和端口号
|
||||
Host and port of your server
|
||||
'''
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 80
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
游戏API地址前缀
|
||||
Game API's URL prefix
|
||||
'''
|
||||
GAME_API_PREFIX = '/join/21'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
允许使用的游戏版本,若为空,则默认全部允许
|
||||
Allowed game versions
|
||||
If it is blank, all are allowed.
|
||||
'''
|
||||
ALLOW_APPVERSION = ['3.12.6', '3.12.6c',
|
||||
'4.0.256', '4.0.256c', '4.1.0', '4.1.0c']
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
联机功能相关设置,请确保与Link Play服务器端的设置一致
|
||||
Setting of your link play server
|
||||
Please ensure that the settings on the side of Link Play server are consistent.
|
||||
'''
|
||||
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: 是否同时在本地启动Link Play服务器
|
||||
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: If it is `True`, the link play server will run with the main server locally at the same time.
|
||||
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
|
||||
# LINKPLAY_HOST: 对主服务器来说的Link Play服务器的地址
|
||||
# LINKPLAY_HOST: The address of the linkplay server based on the main server. If it is blank, the link play feature will be disabled.
|
||||
LINKPLAY_HOST = '0.0.0.0'
|
||||
LINKPLAY_UDP_PORT = 10900
|
||||
LINKPLAY_TCP_PORT = 10901
|
||||
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
|
||||
# LINKPLAY_DISPLAY_HOST: 对客户端来说的Link Play服务器地址,如果为空,则自动获取
|
||||
# LINKPLAY_DISPLAY_HOST: The address of the linkplay server based on the client. If it is blank, the host of link play server for the client will be obtained automatically.
|
||||
LINKPLAY_DISPLAY_HOST = ''
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
SSL证书路径
|
||||
留空则使用HTTP
|
||||
SSL certificate path
|
||||
If left blank, use HTTP.
|
||||
'''
|
||||
SSL_CERT = '' # *.pem
|
||||
SSL_KEY = '' # *.key
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
愚人节模式开关
|
||||
Switch of April Fool's Day
|
||||
'''
|
||||
IS_APRILFOOLS = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
世界排名的最大显示数量
|
||||
The largest number of global rank
|
||||
'''
|
||||
WORLD_RANK_MAX = 200
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
世界模式当前活动图设置
|
||||
Current available maps in world mode
|
||||
'''
|
||||
AVAILABLE_MAP = [] # Ex. ['test', 'test2']
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的用户名和密码
|
||||
Username and password of web background management page
|
||||
'''
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'admin'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的session秘钥,如果不知道是什么,请不要修改
|
||||
Session key of web background management page
|
||||
If you don't know what it is, please don't modify it.
|
||||
'''
|
||||
SECRET_KEY = '1145141919810'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
API接口完全控制权限Token,留空则不使用
|
||||
API interface full control permission Token
|
||||
If you don't want to use it, leave it blank.
|
||||
'''
|
||||
API_TOKEN = ''
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
歌曲下载地址前缀,留空则自动获取
|
||||
Song download address prefix
|
||||
If left blank, it will be obtained automatically.
|
||||
'''
|
||||
DOWNLOAD_LINK_PREFIX = '' # http://***.com/download/
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
玩家歌曲下载的24小时次数限制,每个文件算一次
|
||||
Player's song download limit times in 24 hours, once per file
|
||||
'''
|
||||
DOWNLOAD_TIMES_LIMIT = 3000
|
||||
'''
|
||||
歌曲下载链接的有效时长,单位:秒
|
||||
Effective duration of song download link, unit: seconds
|
||||
'''
|
||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Arcaea登录的最大允许设备数量,最小值为1
|
||||
The maximum number of devices allowed to log in Arcaea, minimum: 1
|
||||
'''
|
||||
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||
'''
|
||||
是否允许同设备多应用共存登录
|
||||
请注意,这个选项设置为True时,下一个选项将自动变为False
|
||||
If logging in from multiple applications on the same device is allowed
|
||||
Note that when this option is set to True, the next option automatically becomes False
|
||||
'''
|
||||
ALLOW_LOGIN_SAME_DEVICE = False
|
||||
'''
|
||||
24小时内登陆设备数超过最大允许设备数量时,是否自动封号(1天、3天、7天、15天、31天)
|
||||
When the number of login devices exceeds the maximum number of devices allowed to log in Arcaea within 24 hours, whether the account will be automatically banned (1 day, 3 days, 7 days, 15 days, 31 days)
|
||||
'''
|
||||
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否记录详细的服务器日志
|
||||
If recording detailed server logs is enabled
|
||||
'''
|
||||
ALLOW_INFO_LOG = False
|
||||
ALLOW_WARNING_LOG = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
用户注册时的默认记忆源点数量
|
||||
The default amount of memories at the time of user registration
|
||||
'''
|
||||
DEFAULT_MEMORIES = 0
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
数据库更新时,是否采用最新的角色数据,如果你想采用最新的官方角色数据
|
||||
注意:如果是,旧的数据将丢失;如果否,某些角色的数据变动将无法同步
|
||||
If using the latest character data when updating database. If you want to only keep newest official character data, please set it `True`.
|
||||
Note: If `True`, the old data will be lost; If `False`, the data changes of some characters will not be synchronized.
|
||||
'''
|
||||
UPDATE_WITH_NEW_CHARACTER_DATA = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁搭档
|
||||
If unlocking all partners is enabled
|
||||
'''
|
||||
CHARACTER_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁世界歌曲
|
||||
If unlocking all world songs is enabled
|
||||
'''
|
||||
WORLD_SONG_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁世界场景
|
||||
If unlocking all world sceneries is enabled
|
||||
'''
|
||||
WORLD_SCENERY_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否强制使用全解锁云端存档
|
||||
If forcing full unlocked cloud save is enabled
|
||||
请注意,当前对于最终结局的判定为固定在`Testify`解锁之前
|
||||
Please note that the current setting of the finale state is before the unlock of `Testify`
|
||||
'''
|
||||
SAVE_FULL_UNLOCK = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
0
latest version/core/__init__.py
Normal file
0
latest version/core/__init__.py
Normal file
152
latest version/core/api_user.py
Normal file
152
latest version/core/api_user.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
from time import time
|
||||
|
||||
from .config_manager import Config
|
||||
from .error import NoAccess, NoData, RateLimit, UserBan
|
||||
from .limiter import ArcLimiter
|
||||
from .user import UserOnline
|
||||
|
||||
|
||||
class Power:
|
||||
def __init__(self, c=None):
|
||||
self.c = c
|
||||
self.power_id: str = None
|
||||
self.caption: str = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict, c=None) -> 'Power':
|
||||
p = cls(c)
|
||||
p.power_id = d['power_id']
|
||||
p.caption = d['caption']
|
||||
return p
|
||||
|
||||
|
||||
class Role:
|
||||
def __init__(self, c=None):
|
||||
self.c = c
|
||||
self.role_id: str = None
|
||||
self.caption: str = None
|
||||
|
||||
self.powers: list = None
|
||||
|
||||
def has_power(self, power_id: str) -> bool:
|
||||
'''判断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:
|
||||
if i.power_id in anti_power_ids:
|
||||
return False
|
||||
for j, k in enumerate(power_ids):
|
||||
if i.power_id == k:
|
||||
flags[j] = True
|
||||
return all(flags)
|
||||
|
||||
def select_from_id(self, role_id: int = None) -> 'Role':
|
||||
'''用role_id查询role'''
|
||||
if role_id is not None:
|
||||
self.role_id = role_id
|
||||
self.c.execute('''select caption from role where role_id = :a''',
|
||||
{'a': self.role_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(
|
||||
f'The role `{self.role_id}` does not exist.', api_error_code=-200)
|
||||
self.caption = x[0]
|
||||
return self
|
||||
|
||||
def select_powers(self) -> None:
|
||||
'''查询role的全部powers'''
|
||||
self.powers = []
|
||||
self.c.execute('''select * from power where power_id in (select power_id from role_power where role_id=:a)''', {
|
||||
'a': self.role_id})
|
||||
x = self.c.fetchall()
|
||||
for i in x:
|
||||
self.powers.append(Power.from_dict(
|
||||
{'power_id': i[0], 'caption': i[1]}, self.c))
|
||||
|
||||
|
||||
class APIUser(UserOnline):
|
||||
limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login')
|
||||
|
||||
def __init__(self, c=None, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
self.api_token: str = None
|
||||
self.role: 'Role' = 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:
|
||||
'''查询user的role'''
|
||||
self.c.execute('''select role_id from user_role where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
self.role = Role(self.c)
|
||||
if x is None:
|
||||
# 默认role为user
|
||||
self.role.role_id = 'user'
|
||||
else:
|
||||
self.role.role_id = x[0]
|
||||
|
||||
def select_role_and_powers(self) -> None:
|
||||
'''查询user的role,以及role的powers'''
|
||||
self.select_role()
|
||||
self.role.select_powers()
|
||||
|
||||
def select_user_id_from_api_token(self, api_token: str = None) -> None:
|
||||
if api_token is not None:
|
||||
self.api_token = api_token
|
||||
self.c.execute('''select user_id from api_login where token = :token''', {
|
||||
'token': self.api_token})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoAccess('No token', api_error_code=-1)
|
||||
self.user_id = x[0]
|
||||
|
||||
def logout(self) -> None:
|
||||
self.c.execute(
|
||||
'''delete from api_login where user_id=?''', (self.user_id,))
|
||||
|
||||
def login(self, name: str = None, password: str = None, ip: str = None) -> None:
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if password is not None:
|
||||
self.password = password
|
||||
if ip is not None:
|
||||
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''', {
|
||||
'a': self.name})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(
|
||||
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
|
||||
if x[1] == '':
|
||||
raise UserBan(f'The user `{x[0]}` is banned.')
|
||||
if self.hash_pwd != x[1]:
|
||||
raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
|
||||
api_error_code=-201, status=401)
|
||||
|
||||
self.user_id = x[0]
|
||||
now = int(time() * 1000)
|
||||
self.api_token = sha256(
|
||||
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
|
||||
|
||||
self.logout()
|
||||
self.c.execute('''insert into api_login values(?,?,?,?)''',
|
||||
(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)
|
||||
254
latest version/core/bundle.py
Normal file
254
latest version/core/bundle.py
Normal file
@@ -0,0 +1,254 @@
|
||||
import json
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from time import time
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import NoAccess, NoData, RateLimit
|
||||
from .limiter import ArcLimiter
|
||||
|
||||
|
||||
class ContentBundle:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.version: str = None
|
||||
self.prev_version: str = None
|
||||
self.app_version: str = None
|
||||
self.uuid: str = None
|
||||
|
||||
self.json_size: int = None
|
||||
self.bundle_size: int = None
|
||||
self.json_path: str = None # relative path
|
||||
self.bundle_path: str = None # relative path
|
||||
|
||||
self.json_url: str = None
|
||||
self.bundle_url: str = None
|
||||
|
||||
@staticmethod
|
||||
def parse_version(version: str) -> tuple:
|
||||
try:
|
||||
r = tuple(map(int, version.split('.')))
|
||||
except AttributeError:
|
||||
r = (0, 0, 0)
|
||||
return r
|
||||
|
||||
@property
|
||||
def version_tuple(self) -> tuple:
|
||||
return self.parse_version(self.version)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data: dict) -> 'ContentBundle':
|
||||
x = cls()
|
||||
x.version = json_data['versionNumber']
|
||||
x.prev_version = json_data['previousVersionNumber']
|
||||
x.app_version = json_data['applicationVersionNumber']
|
||||
x.uuid = json_data['uuid']
|
||||
if x.prev_version is None:
|
||||
x.prev_version = '0.0.0'
|
||||
return x
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = {
|
||||
'contentBundleVersion': self.version,
|
||||
'appVersion': self.app_version,
|
||||
'jsonSize': self.json_size,
|
||||
'bundleSize': self.bundle_size,
|
||||
}
|
||||
if self.json_url and self.bundle_url:
|
||||
r['jsonUrl'] = self.json_url
|
||||
r['bundleUrl'] = self.bundle_url
|
||||
return r
|
||||
|
||||
def calculate_size(self) -> None:
|
||||
self.json_size = os.path.getsize(os.path.join(
|
||||
Constant.CONTENT_BUNDLE_FOLDER_PATH, self.json_path))
|
||||
self.bundle_size = os.path.getsize(os.path.join(
|
||||
Constant.CONTENT_BUNDLE_FOLDER_PATH, self.bundle_path))
|
||||
|
||||
|
||||
class BundleParser:
|
||||
|
||||
# {app_version: [ List[ContentBundle] ]}
|
||||
bundles: 'dict[str, list[ContentBundle]]' = {}
|
||||
# {app_version: max bundle version}
|
||||
max_bundle_version: 'dict[str, str]' = {}
|
||||
|
||||
# {bundle version: [next versions]} 宽搜索引
|
||||
next_versions: 'dict[str, list[str]]' = {}
|
||||
# {(bver, b prev version): ContentBundle} 正向索引
|
||||
version_tuple_bundles: 'dict[tuple[str, str], ContentBundle]' = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
if not self.bundles:
|
||||
self.parse()
|
||||
|
||||
def re_init(self) -> None:
|
||||
self.bundles.clear()
|
||||
self.max_bundle_version.clear()
|
||||
self.next_versions.clear()
|
||||
self.version_tuple_bundles.clear()
|
||||
self.get_bundles.cache_clear()
|
||||
self.parse()
|
||||
|
||||
def parse(self) -> None:
|
||||
for root, dirs, files in os.walk(Constant.CONTENT_BUNDLE_FOLDER_PATH):
|
||||
for file in files:
|
||||
if not file.endswith('.json'):
|
||||
continue
|
||||
|
||||
json_path = os.path.join(root, file)
|
||||
bundle_path = os.path.join(root, f'{file[:-5]}.cb')
|
||||
|
||||
with open(json_path, 'rb') as f:
|
||||
data = json.load(f)
|
||||
|
||||
x = ContentBundle.from_json(data)
|
||||
|
||||
x.json_path = os.path.relpath(
|
||||
json_path, Constant.CONTENT_BUNDLE_FOLDER_PATH)
|
||||
x.bundle_path = os.path.relpath(
|
||||
bundle_path, Constant.CONTENT_BUNDLE_FOLDER_PATH)
|
||||
|
||||
x.json_path = x.json_path.replace('\\', '/')
|
||||
x.bundle_path = x.bundle_path.replace('\\', '/')
|
||||
|
||||
if not os.path.isfile(bundle_path):
|
||||
raise FileNotFoundError(
|
||||
f'Bundle file not found: {bundle_path}')
|
||||
x.calculate_size()
|
||||
|
||||
self.bundles.setdefault(x.app_version, []).append(x)
|
||||
|
||||
self.version_tuple_bundles[(x.version, x.prev_version)] = x
|
||||
self.next_versions.setdefault(
|
||||
x.prev_version, []).append(x.version)
|
||||
|
||||
# sort by version
|
||||
for k, v in self.bundles.items():
|
||||
v.sort(key=lambda x: x.version_tuple)
|
||||
self.max_bundle_version[k] = v[-1].version
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=128)
|
||||
def get_bundles(app_ver: str, b_ver: str) -> 'list[ContentBundle]':
|
||||
if Config.BUNDLE_STRICT_MODE:
|
||||
return BundleParser.bundles.get(app_ver, [])
|
||||
|
||||
k = b_ver if b_ver else '0.0.0'
|
||||
|
||||
target_version = BundleParser.max_bundle_version.get(app_ver, '0.0.0')
|
||||
if k == target_version:
|
||||
return []
|
||||
|
||||
# BFS
|
||||
q = [[k]]
|
||||
ans = None
|
||||
while True:
|
||||
qq = []
|
||||
for x in q:
|
||||
if x[-1] == target_version:
|
||||
ans = x
|
||||
break
|
||||
for y in BundleParser.next_versions.get(x[-1], []):
|
||||
if y in x:
|
||||
continue
|
||||
qq.append(x + [y])
|
||||
|
||||
if ans is not None or not qq:
|
||||
break
|
||||
q = qq
|
||||
|
||||
if not ans:
|
||||
raise NoData(
|
||||
f'No bundles found for app version: {app_ver}, bundle version: {b_ver}', status=404)
|
||||
|
||||
r = []
|
||||
for i in range(1, len(ans)):
|
||||
r.append(BundleParser.version_tuple_bundles[(ans[i], ans[i-1])])
|
||||
|
||||
return r
|
||||
|
||||
|
||||
class BundleDownload:
|
||||
|
||||
limiter = ArcLimiter(
|
||||
Constant.BUNDLE_DOWNLOAD_TIMES_LIMIT, 'bundle_download')
|
||||
|
||||
def __init__(self, c_m=None):
|
||||
self.c_m = c_m
|
||||
|
||||
self.client_app_version = None
|
||||
self.client_bundle_version = None
|
||||
self.device_id = None
|
||||
|
||||
def set_client_info(self, app_version: str, bundle_version: str, device_id: str = None) -> None:
|
||||
self.client_app_version = app_version
|
||||
self.client_bundle_version = bundle_version
|
||||
self.device_id = device_id
|
||||
|
||||
def get_bundle_list(self) -> list:
|
||||
bundles: 'list[ContentBundle]' = BundleParser.get_bundles(
|
||||
self.client_app_version, self.client_bundle_version)
|
||||
|
||||
if not bundles:
|
||||
return []
|
||||
|
||||
now = time()
|
||||
|
||||
if Constant.BUNDLE_DOWNLOAD_LINK_PREFIX:
|
||||
prefix = Constant.BUNDLE_DOWNLOAD_LINK_PREFIX
|
||||
if prefix[-1] != '/':
|
||||
prefix += '/'
|
||||
|
||||
def url_func(x): return f'{prefix}{x}'
|
||||
else:
|
||||
def url_func(x): return url_for(
|
||||
'bundle_download', token=x, _external=True)
|
||||
|
||||
sql_list = []
|
||||
r = []
|
||||
for x in bundles:
|
||||
if x.version_tuple <= ContentBundle.parse_version(self.client_bundle_version):
|
||||
continue
|
||||
t1 = os.urandom(64).hex()
|
||||
t2 = os.urandom(64).hex()
|
||||
|
||||
x.json_url = url_func(t1)
|
||||
x.bundle_url = url_func(t2)
|
||||
|
||||
sql_list.append((t1, x.json_path, now, self.device_id))
|
||||
sql_list.append((t2, x.bundle_path, now, self.device_id))
|
||||
r.append(x.to_dict())
|
||||
|
||||
if not sql_list:
|
||||
return []
|
||||
|
||||
self.clear_expired_token()
|
||||
|
||||
self.c_m.executemany(
|
||||
'''insert into bundle_download_token values (?, ?, ?, ?)''', sql_list)
|
||||
|
||||
return r
|
||||
|
||||
def get_path_by_token(self, token: str, ip: str) -> str:
|
||||
r = self.c_m.execute(
|
||||
'''select file_path, time, device_id from bundle_download_token where token = ?''', (token,)).fetchone()
|
||||
if not r:
|
||||
raise NoAccess('Invalid token.', status=403)
|
||||
file_path, create_time, device_id = r
|
||||
|
||||
if time() - create_time > Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT:
|
||||
raise NoAccess('Expired token.', status=403)
|
||||
|
||||
if file_path.endswith('.cb') and not self.limiter.hit(ip):
|
||||
raise RateLimit(
|
||||
f'Too many content bundle downloads, IP: {ip}, DeviceID: {device_id}', status=429)
|
||||
|
||||
return file_path
|
||||
|
||||
def clear_expired_token(self) -> None:
|
||||
self.c_m.execute(
|
||||
'''delete from bundle_download_token where time < ?''', (int(time() - Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT),))
|
||||
489
latest version/core/character.py
Normal file
489
latest version/core/character.py
Normal file
@@ -0,0 +1,489 @@
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||
from .item import CollectionItemMixin, ItemCore
|
||||
from .sql import UserKVTable
|
||||
|
||||
|
||||
class Level:
|
||||
mid_level = 20
|
||||
min_level = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.max_level: int = None
|
||||
self.level: int = None
|
||||
self.exp: float = None
|
||||
|
||||
@property
|
||||
def level_exp(self):
|
||||
return Constant.LEVEL_STEPS[self.level]
|
||||
|
||||
def add_exp(self, exp_addition: float):
|
||||
# 添加经验计算
|
||||
|
||||
exp = self.exp + exp_addition
|
||||
|
||||
if exp >= Constant.LEVEL_STEPS[self.max_level]:
|
||||
self.exp = Constant.LEVEL_STEPS[self.max_level]
|
||||
self.level = self.max_level
|
||||
return None
|
||||
|
||||
a = []
|
||||
b = []
|
||||
for k, v in Constant.LEVEL_STEPS.items():
|
||||
a.append(k)
|
||||
b.append(v)
|
||||
|
||||
if exp < b[0]: # 向下溢出,是异常状态,不该被try捕获,不然数据库无法回滚
|
||||
raise ValueError('EXP value error.')
|
||||
|
||||
i = len(a) - 1
|
||||
while exp < b[i]:
|
||||
i -= 1
|
||||
|
||||
self.exp = exp
|
||||
self.level = a[i]
|
||||
|
||||
|
||||
class Skill:
|
||||
def __init__(self) -> None:
|
||||
self.skill_id: str = None
|
||||
self.skill_id_uncap: str = None
|
||||
self.skill_unlock_level: int = None
|
||||
self.skill_requires_uncap: bool = None
|
||||
|
||||
|
||||
class CharacterValue:
|
||||
def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
|
||||
self.set_parameter(start, mid, end)
|
||||
self.addition: float = 0
|
||||
|
||||
@staticmethod
|
||||
def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
|
||||
# by Lost-MSth
|
||||
# 4/6859 = 0.00058317539
|
||||
if level <= 10:
|
||||
return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1
|
||||
return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
|
||||
|
||||
# @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
|
||||
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
|
||||
# 计算21~30级搭档数值,返回浮点数
|
||||
return (level - lva) * (statb - stata) / (lvb - lva) + stata
|
||||
|
||||
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
|
||||
self.start: float = start
|
||||
self.mid: float = mid
|
||||
self.end: float = end
|
||||
|
||||
def get_value(self, level: Level):
|
||||
if level.min_level <= level.level <= level.mid_level:
|
||||
return self._calc_char_value_20_math(level.level, self.start, self.mid) + self.addition
|
||||
if level.mid_level < level.level <= level.max_level:
|
||||
return self._calc_char_value_30(level.level, self.mid, self.end) + self.addition
|
||||
return 0
|
||||
|
||||
|
||||
class Character(CollectionItemMixin):
|
||||
database_table_name = None
|
||||
|
||||
collection_item_const = {
|
||||
'name': 'character',
|
||||
'table_name': 'char_item',
|
||||
'table_primary_key': 'character_id',
|
||||
'id_name': 'character_id',
|
||||
'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.level = Level()
|
||||
self.frag = CharacterValue()
|
||||
self.prog = CharacterValue()
|
||||
self.overdrive = CharacterValue()
|
||||
self.uncap_cores: list = []
|
||||
self.voice: list = None
|
||||
|
||||
@property
|
||||
def frag_value(self) -> float:
|
||||
return self.frag.get_value(self.level)
|
||||
|
||||
@property
|
||||
def prog_value(self) -> float:
|
||||
return self.prog.get_value(self.level)
|
||||
|
||||
@property
|
||||
def overdrive_value(self) -> float:
|
||||
return self.overdrive.get_value(self.level)
|
||||
|
||||
@property
|
||||
def skill_id_displayed(self) -> str:
|
||||
return None
|
||||
|
||||
def uncap_cores_to_dict(self):
|
||||
return [x.to_dict(character_format=True) for x in self.uncap_cores]
|
||||
|
||||
@property
|
||||
def is_uncapped_displayed(self) -> bool:
|
||||
'''对外显示的uncap状态'''
|
||||
return False if self.is_uncapped_override else self.is_uncapped
|
||||
|
||||
@property
|
||||
def is_base_character(self) -> bool:
|
||||
# 应该是只有对立这样
|
||||
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):
|
||||
'''
|
||||
用户角色类
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||
|
||||
def __init__(self, c, character_id=None, user=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.character_id = character_id
|
||||
self.user = user
|
||||
|
||||
self.skill_flag: bool = None
|
||||
|
||||
self.fatalis_is_limited: bool = False
|
||||
|
||||
@property
|
||||
def skill_id_displayed(self) -> str:
|
||||
'''对外显示的技能id'''
|
||||
if self.is_uncapped_displayed and self.skill.skill_id_uncap:
|
||||
return self.skill.skill_id_uncap
|
||||
if self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
|
||||
return self.skill.skill_id
|
||||
return None
|
||||
|
||||
@property
|
||||
def skill_state(self) -> str:
|
||||
if self.skill_id_displayed == 'skill_maya':
|
||||
return 'add_random' if self.skill_flag else 'remove_random'
|
||||
return None
|
||||
|
||||
def select_character_uncap_condition(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取此角色的觉醒信息
|
||||
if user:
|
||||
self.user = user
|
||||
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})
|
||||
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.is_uncapped = False
|
||||
self.is_uncapped_override = False
|
||||
# raise NoData('The character of the user does not exist.')
|
||||
else:
|
||||
self.is_uncapped = x[0] == 1
|
||||
self.is_uncapped_override = x[1] == 1
|
||||
|
||||
def select_character_info(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取所给用户此角色信息
|
||||
if user:
|
||||
self.user = user
|
||||
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))
|
||||
|
||||
y = self.c.fetchone()
|
||||
if y is None:
|
||||
raise NoData('The character of the user does not exist.')
|
||||
|
||||
self.name = y[8]
|
||||
self.char_type = y[23]
|
||||
self.is_uncapped = y[4] == 1
|
||||
self.is_uncapped_override = y[5] == 1
|
||||
self.level.level = y[2]
|
||||
self.level.exp = y[3]
|
||||
self.level.max_level = y[9]
|
||||
self.frag.set_parameter(y[10], y[13], y[16])
|
||||
self.prog.set_parameter(y[11], y[14], y[17])
|
||||
self.overdrive.set_parameter(y[12], y[15], y[18])
|
||||
self.skill.skill_id = y[19]
|
||||
self.skill.skill_id_uncap = y[22]
|
||||
self.skill.skill_unlock_level = y[20]
|
||||
self.skill.skill_requires_uncap = y[21] == 1
|
||||
|
||||
self.skill_flag = y[6] == 1
|
||||
|
||||
if self.character_id in (21, 46):
|
||||
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
|
||||
if self.character_id == 55:
|
||||
# fatalis 提升数值
|
||||
# prog & overdrive += 世界模式中完成的所有非无限地图的台阶数之和 / 30
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
addition = Constant.FATALIS_MAX_VALUE
|
||||
self.fatalis_is_limited = True
|
||||
else:
|
||||
kvd = UserKVTable(self.c, self.user.user_id, 'world')
|
||||
steps = kvd['total_step_count'] or 0
|
||||
addition = steps / 30
|
||||
if addition >= Constant.FATALIS_MAX_VALUE:
|
||||
addition = Constant.FATALIS_MAX_VALUE
|
||||
self.fatalis_is_limited = True
|
||||
self.prog.addition = addition
|
||||
self.overdrive.addition = addition
|
||||
|
||||
self.select_character_core()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.char_type is None:
|
||||
self.select_character_info(self.user)
|
||||
r = {'base_character': self.is_base_character,
|
||||
"is_uncapped_override": self.is_uncapped_override,
|
||||
"is_uncapped": self.is_uncapped,
|
||||
"uncap_cores": self.uncap_cores_to_dict(),
|
||||
"char_type": self.char_type,
|
||||
"skill_id_uncap": self.skill.skill_id_uncap,
|
||||
"skill_requires_uncap": self.skill.skill_requires_uncap,
|
||||
"skill_unlock_level": self.skill.skill_unlock_level,
|
||||
"skill_id": self.skill.skill_id,
|
||||
"overdrive": self.overdrive.get_value(self.level),
|
||||
"prog": self.prog.get_value(self.level),
|
||||
"frag": self.frag.get_value(self.level),
|
||||
"level_exp": self.level.level_exp,
|
||||
"exp": self.level.exp,
|
||||
"level": self.level.level,
|
||||
"name": self.name,
|
||||
"character_id": self.character_id
|
||||
}
|
||||
if self.voice:
|
||||
r['voice'] = self.voice
|
||||
if self.character_id == 55:
|
||||
r['fatalis_is_limited'] = self.fatalis_is_limited
|
||||
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
|
||||
r['base_character_id'] = 1
|
||||
|
||||
x = self.skill_state
|
||||
if x:
|
||||
r['skill_state'] = x
|
||||
|
||||
return r
|
||||
|
||||
def change_uncap_override(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 切换觉醒状态
|
||||
if user:
|
||||
self.user = user
|
||||
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})
|
||||
|
||||
x = self.c.fetchone()
|
||||
if x is None or x[0] == 0:
|
||||
raise ArcError('Unknown Error')
|
||||
|
||||
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})
|
||||
|
||||
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})
|
||||
|
||||
self.is_uncapped_override = x[1] == 0
|
||||
|
||||
def character_uncap(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 觉醒角色
|
||||
if user:
|
||||
self.user = user
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
# 全解锁了你觉醒个鬼啊
|
||||
raise ArcError('All characters are available.')
|
||||
|
||||
if not self.uncap_cores:
|
||||
self.select_character_core()
|
||||
|
||||
if self.is_uncapped is None:
|
||||
self.c.execute(
|
||||
'''select is_uncapped from user_char where user_id=? and character_id=?''', (self.user.user_id, self.character_id))
|
||||
x = self.c.fetchone()
|
||||
if x and x[0] == 1:
|
||||
raise ArcError('The character has been uncapped.')
|
||||
elif self.is_uncapped:
|
||||
raise ArcError('The character has been uncapped.')
|
||||
|
||||
for i in self.uncap_cores:
|
||||
self.c.execute(
|
||||
'''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()
|
||||
if i.amount > 0 and (not y or i.amount > y[0]):
|
||||
raise ItemNotEnough('The cores are not enough.')
|
||||
|
||||
for i in self.uncap_cores:
|
||||
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.user.user_id, self.character_id))
|
||||
|
||||
self.is_uncapped = True
|
||||
self.is_uncapped_override = False
|
||||
|
||||
def upgrade(self, user=None, exp_addition: float = 0) -> None:
|
||||
# parameter: user - User类或子类的实例
|
||||
# 升级角色
|
||||
if user:
|
||||
self.user = user
|
||||
if exp_addition == 0:
|
||||
return None
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
# 全解锁了你升级个鬼啊
|
||||
raise ArcError('All characters are available.')
|
||||
|
||||
if self.level.exp is None:
|
||||
self.select_character_info(self.user)
|
||||
|
||||
if self.is_uncapped is None:
|
||||
self.c.execute(
|
||||
'''select is_uncapped from user_char where user_id=? and character_id=?''', (self.user.user_id, self.character_id))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.is_uncapped = x[0] == 1
|
||||
|
||||
self.level.max_level = 30 if self.is_uncapped else 20
|
||||
self.level.add_exp(exp_addition)
|
||||
|
||||
self.c.execute('''update user_char set level=?, exp=? where user_id=? and character_id=?''',
|
||||
(self.level.level, self.level.exp, self.user.user_id, self.character_id))
|
||||
|
||||
def upgrade_by_core(self, user=None, core=None):
|
||||
'''
|
||||
以太之滴升级,注意这里core.amount应该是负数
|
||||
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
`core` - `ItemCore`类或子类的实例
|
||||
'''
|
||||
if user:
|
||||
self.user = user
|
||||
if not core:
|
||||
raise InputError('No `core_generic`.')
|
||||
if core.item_id != 'core_generic':
|
||||
raise ArcError('Core type error.')
|
||||
|
||||
if core.amount >= 0:
|
||||
raise InputError(
|
||||
'The amount of `core_generic` should be negative.')
|
||||
|
||||
core.user_claim_item(self.user)
|
||||
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:
|
||||
'''
|
||||
用户拥有角色列表类
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.characters: list = []
|
||||
|
||||
def select_user_characters(self):
|
||||
self.c.execute(
|
||||
f'''select character_id from {self.database_table_name} where user_id=?''', (self.user.user_id,))
|
||||
x = self.c.fetchall()
|
||||
self.characters: list = []
|
||||
if x:
|
||||
for i in x:
|
||||
self.characters.append(UserCharacter(self.c, i[0], self.user))
|
||||
|
||||
def select_characters_info(self):
|
||||
for i in self.characters:
|
||||
i.select_character_info(self.user)
|
||||
120
latest version/core/config_manager.py
Normal file
120
latest version/core/config_manager.py
Normal file
@@ -0,0 +1,120 @@
|
||||
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 = ['/autumntrain/37', '/'] # str | list[str]
|
||||
OLD_GAME_API_PREFIX = [] # str | list[str]
|
||||
|
||||
ALLOW_APPVERSION = [] # list[str]
|
||||
|
||||
BUNDLE_STRICT_MODE = True
|
||||
|
||||
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 = '' # http(s)://host(:port)/download/
|
||||
BUNDLE_DOWNLOAD_LINK_PREFIX = '' # http(s)://host(:port)/bundle_download/
|
||||
|
||||
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
|
||||
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
|
||||
BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_bundle_download/'
|
||||
|
||||
DOWNLOAD_TIMES_LIMIT = 3000
|
||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||
|
||||
DOWNLOAD_FORBID_WHEN_NO_ITEM = False
|
||||
|
||||
BUNDLE_DOWNLOAD_TIMES_LIMIT = '100/60 minutes'
|
||||
BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = 3000
|
||||
|
||||
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||
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
|
||||
|
||||
ALLOW_SELF_ACCOUNT_DELETE = False
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
# You can change this to make another PTT mechanism.
|
||||
BEST30_WEIGHT = 1 / 40
|
||||
RECENT10_WEIGHT = 1 / 40
|
||||
|
||||
INVASION_START_WEIGHT = 0.1
|
||||
INVASION_HARD_WEIGHT = 0.1
|
||||
|
||||
MAX_FRIEND_COUNT = 50
|
||||
|
||||
WORLD_MAP_FOLDER_PATH = './database/map/'
|
||||
SONG_FILE_FOLDER_PATH = './database/songs/'
|
||||
SONGLIST_FILE_PATH = './database/songs/songlist'
|
||||
CONTENT_BUNDLE_FOLDER_PATH = './database/bundle/'
|
||||
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'
|
||||
SQLITE_DATABASE_DELETED_PATH = './database/arcaea_database_deleted.db'
|
||||
|
||||
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
|
||||
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||
GAME_REGISTER_IP_RATE_LIMIT = '10/1 day'
|
||||
GAME_REGISTER_DEVICE_RATE_LIMIT = '3/1 day'
|
||||
|
||||
NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
|
||||
@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)
|
||||
134
latest version/core/constant.py
Normal file
134
latest version/core/constant.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from .config_manager import Config
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.12.1.7'
|
||||
ARCAEA_DATABASE_VERSION = 'v2.12.1.7'
|
||||
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||
|
||||
|
||||
class Constant:
|
||||
|
||||
BAN_TIME = [1, 3, 7, 15, 31]
|
||||
|
||||
MAX_STAMINA = 12
|
||||
|
||||
# INSIGHT_STATES = [x for x in range(7)]
|
||||
INSIGHT_TOGGLE_STATES = [3, 4, 5, 6]
|
||||
# DEFAULT_INSIGHT_STATE = Config.DEFAULT_INSIGHT_STATE
|
||||
|
||||
STAMINA_RECOVER_TICK = 1800000
|
||||
FRAGSTAM_RECOVER_TICK = 23 * 3600 * 1000
|
||||
|
||||
COURSE_STAMINA_COST = 4
|
||||
|
||||
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,
|
||||
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
|
||||
LUNA_UNCAP_BONUS_PROGRESS = 7
|
||||
AYU_UNCAP_BONUS_PROGRESS = 5
|
||||
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
|
||||
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
|
||||
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
|
||||
FATALIS_MAX_VALUE = 100
|
||||
|
||||
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
||||
|
||||
MY_RANK_MAX_LOCAL_POSITION = 5
|
||||
MY_RANK_MAX_GLOBAL_POSITION = 9999
|
||||
|
||||
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
||||
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
||||
INVASION_START_WEIGHT = Config.INVASION_START_WEIGHT
|
||||
INVASION_HARD_WEIGHT = Config.INVASION_HARD_WEIGHT
|
||||
|
||||
WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH
|
||||
SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH
|
||||
SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH
|
||||
CONTENT_BUNDLE_FOLDER_PATH = Config.CONTENT_BUNDLE_FOLDER_PATH
|
||||
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
|
||||
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
|
||||
SQLITE_DATABASE_DELETED_PATH = Config.SQLITE_DATABASE_DELETED_PATH
|
||||
|
||||
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
||||
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
||||
DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX
|
||||
BUNDLE_DOWNLOAD_TIMES_LIMIT = Config.BUNDLE_DOWNLOAD_TIMES_LIMIT
|
||||
BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = Config.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT
|
||||
BUNDLE_DOWNLOAD_LINK_PREFIX = Config.BUNDLE_DOWNLOAD_LINK_PREFIX
|
||||
|
||||
LINKPLAY_UNLOCK_LENGTH = 1024 # Units: bytes
|
||||
LINKPLAY_TIMEOUT = 5 # Units: seconds
|
||||
|
||||
LINKPLAY_HOST = '127.0.0.1' if Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS else Config.LINKPLAY_HOST
|
||||
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
|
||||
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
|
||||
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
|
||||
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
|
||||
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||
|
||||
LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4 # Units: seconds
|
||||
LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000]
|
||||
LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1]
|
||||
LINKPLAY_MATCH_TIMEOUT = 15 # Units: seconds
|
||||
LINKPLAY_MATCH_MEMORY_CLEAN_INTERVAL = 60 # Units: seconds
|
||||
|
||||
# Well, I can't say a word when I see this.
|
||||
FINALE_SWITCH = [
|
||||
(0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B),
|
||||
(0x02429E, 0x0449A4), (0x099C9C,
|
||||
0x07CFB4), (0x0785BF, 0x019B2C),
|
||||
(0x0EFF43, 0x0841BF), (0x07C88B,
|
||||
0x0DE9FC), (0x000778, 0x064815),
|
||||
(0x0E62E3, 0x079F02), (0x0188FE,
|
||||
0x0923EB), (0x0E06CD, 0x0E1A26),
|
||||
(0x00669E, 0x0C8BE1), (0x0BEB7A, 0x05D635), (0x040E6F,
|
||||
0x0B465B), (0x0568EC, 0x07ED2B),
|
||||
(0x189614, 0x00A3D2), (0x62D98D,
|
||||
0x45E5CA), (0x6D8769, 0x473F0E),
|
||||
(0x922E4F, 0x667D6C), (0x021F5C,
|
||||
0x298839), (0x2A1201, 0x49FB7E),
|
||||
(0x158B81, 0x8D905D), (0x2253A5,
|
||||
0x7E7067), (0x3BEF79, 0x9368E9),
|
||||
(0x00669E, 0x0C8BE1), (0x0BEB7A,
|
||||
0x05D635), (0x040E6F, 0x0B465B),
|
||||
(0x756276, 0x55CD57), (0x130055, 0x7010E7), (0x55E28D,
|
||||
0x4477FB), (0x5E99CB, 0x81060E),
|
||||
(0x7F43A4, 0x8FEC56), (0x69412F,
|
||||
0x32735C), (0x8FF846, 0x14B5A1),
|
||||
(0x8716BE, 0x5C78BE), (0x62ED0E,
|
||||
0x348E4B), (0x4B20C8, 0x56E0C3),
|
||||
(0x0AF6BC, 0x872441), (0x8825BC,
|
||||
0x94B315), (0x792784, 0x5B2C8E),
|
||||
(0x1AE3A7, 0x688E97), (0x0D630F,
|
||||
0x06BE78), (0x792784, 0x5B2C8E),
|
||||
(0x314869, 0x41CCC1), (0x311934, 0x24DD94), (0x190EDB,
|
||||
0x33993D), (0x25F5C5, 0x15FAE6),
|
||||
(0x18CA10, 0x1B761A), (0x51BE82,
|
||||
0x120089), (0x51D3B6, 0x2C29A2),
|
||||
(0x402075, 0x4A89B2), (0x00697B,
|
||||
0x0E6497), (0x6D872D, 0x618AE7),
|
||||
(0x3DC0BE, 0x4E2AC8), (0x8C6ACF,
|
||||
0x9776CF), (0x84673B, 0x5CA060),
|
||||
(0x4B05EC, 0x97FDFE), (0x207258,
|
||||
0x02BB9B), (0x20A9EE, 0x1BA4BB),
|
||||
(0x503D21, 0x6A41D0), (0x1C256C,
|
||||
0x6DD3BC), (0x6E4E0C, 0x89FDAA), (0x3C5F95, 0x3BA786),
|
||||
(0XFEA5, 0x2e4ca), (0X7BF653, 0x4befd11), (0X46BEA7B,
|
||||
0x11d3684), (0X8BFB04, 0xa83d6c1),
|
||||
(0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE,
|
||||
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
|
||||
301
latest version/core/course.py
Normal file
301
latest version/core/course.py
Normal file
@@ -0,0 +1,301 @@
|
||||
from .error import NoData
|
||||
from .item import ItemFactory
|
||||
from .song import Chart
|
||||
|
||||
|
||||
class CourseChart(Chart):
|
||||
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
||||
super().__init__(c, song_id, difficulty)
|
||||
self.flag_as_hidden: bool = None
|
||||
|
||||
def from_dict(self, d: dict) -> 'CourseChart':
|
||||
self.song_id = d['id']
|
||||
self.difficulty = d['difficulty']
|
||||
self.flag_as_hidden = d.get('flag_as_hidden', False)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'id': self.song_id,
|
||||
'difficulty': self.difficulty,
|
||||
'flag_as_hidden': self.flag_as_hidden
|
||||
}
|
||||
|
||||
def insert_course_chart(self, course_id: str, song_index: int) -> None:
|
||||
self.c.execute('insert into course_chart values (?,?,?,?,?)',
|
||||
(course_id, self.song_id, self.difficulty, self.flag_as_hidden, song_index))
|
||||
|
||||
|
||||
class Course:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.course_id: str = None
|
||||
self.course_name: str = None
|
||||
self.dan_name: str = None
|
||||
self.style: int = None
|
||||
self.gauge_requirement: str = None
|
||||
self.flag_as_hidden_when_requirements_not_met: bool = None
|
||||
self.can_start: bool = None # 这有什么鬼用?
|
||||
|
||||
self.requirements: list = []
|
||||
self.charts: list = [None, None, None, None]
|
||||
self.items: list = []
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.course_name is None:
|
||||
self.select_course()
|
||||
if not self.items:
|
||||
self.select_course_item()
|
||||
if not self.charts:
|
||||
self.select_course_chart()
|
||||
return {
|
||||
'course_id': self.course_id,
|
||||
'course_name': self.course_name,
|
||||
'dan_name': self.dan_name,
|
||||
'style': self.style,
|
||||
'gauge_requirement': self.gauge_requirement,
|
||||
'flag_as_hidden_when_requirements_not_met': self.flag_as_hidden_when_requirements_not_met,
|
||||
'can_start': self.can_start,
|
||||
'requirements': [{'value': x, 'type': 'course'} for x in self.requirements],
|
||||
'songs': [x.to_dict() for x in self.charts],
|
||||
'rewards': [str(x) for x in self.items]
|
||||
}
|
||||
|
||||
def from_dict(self, d: dict) -> 'Course':
|
||||
self.course_id = d['course_id']
|
||||
self.course_name = d.get('course_name', '')
|
||||
self.dan_name = d.get('dan_name', '')
|
||||
self.style = d.get('style', 1)
|
||||
self.gauge_requirement = d.get('gauge_requirement', 'default')
|
||||
self.flag_as_hidden_when_requirements_not_met = d.get(
|
||||
'flag_as_hidden_when_requirements_not_met', False)
|
||||
self.can_start = d.get('can_start', True)
|
||||
self.requirements = [x['value'] for x in d.get('requirements', [])]
|
||||
self.charts = [CourseChart(self.c).from_dict(x)
|
||||
for x in d.get('songs', [])]
|
||||
self.items = [ItemFactory.from_str(x, self.c)
|
||||
for x in d.get('rewards', [])]
|
||||
|
||||
return self
|
||||
|
||||
def from_list(self, l: list) -> 'Course':
|
||||
self.course_id = l[0]
|
||||
self.course_name = l[1]
|
||||
self.dan_name = l[2]
|
||||
self.style = l[3] if l[3] else 1
|
||||
self.gauge_requirement = l[4] if l[4] else 'default'
|
||||
self.flag_as_hidden_when_requirements_not_met = l[5] == 1
|
||||
self.can_start = l[6] == 1
|
||||
|
||||
return self
|
||||
|
||||
def select_course(self, course_id: str = None) -> 'Course':
|
||||
if course_id is not None:
|
||||
self.course_id = course_id
|
||||
self.c.execute(
|
||||
'''select * from course where course_id = ?''', (self.course_id,))
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(f'The course `{self.course_id}` is not found.')
|
||||
return self.from_list(x)
|
||||
|
||||
def select_course_chart(self) -> None:
|
||||
self.c.execute(
|
||||
'''select * from course_chart where course_id = ?''', (self.course_id,))
|
||||
|
||||
for i in self.c.fetchall():
|
||||
self.charts[i[4]] = CourseChart(self.c).from_dict({
|
||||
'id': i[1],
|
||||
'difficulty': i[2],
|
||||
'flag_as_hidden': i[3] == 1
|
||||
})
|
||||
|
||||
def select_course_requirement(self) -> None:
|
||||
self.c.execute(
|
||||
'''select required_id from course_requirement where course_id = ?''', (self.course_id,))
|
||||
self.requirements = [x[0] for x in self.c.fetchall()]
|
||||
|
||||
def select_course_item(self) -> None:
|
||||
self.c.execute(
|
||||
'''select * from course_item where course_id = ?''', (self.course_id,))
|
||||
self.items = [ItemFactory.from_dict({
|
||||
'item_id': x[1],
|
||||
'type': x[2],
|
||||
'amount': x[3] if x[3] else 1,
|
||||
}, self.c) for x in self.c.fetchall()]
|
||||
|
||||
def insert_course(self) -> None:
|
||||
self.c.execute(
|
||||
'''insert into course values (?,?,?,?,?,?,?)''', (self.course_id, self.course_name, self.dan_name, self.style, self.gauge_requirement, self.flag_as_hidden_when_requirements_not_met, self.can_start))
|
||||
|
||||
def insert_course_item(self) -> None:
|
||||
for i in self.items:
|
||||
self.c.execute('''insert into course_item values (?,?,?,?)''',
|
||||
(self.course_id, i.item_id, i.item_type, i.amount))
|
||||
|
||||
def insert_course_chart(self) -> None:
|
||||
for i, x in enumerate(self.charts):
|
||||
x.insert_course_chart(self.course_id, i)
|
||||
|
||||
def insert_course_requirement(self) -> None:
|
||||
for i in self.requirements:
|
||||
self.c.execute('''insert into course_requirement values (?,?)''',
|
||||
(self.course_id, i))
|
||||
|
||||
def insert_all(self) -> None:
|
||||
self.insert_course()
|
||||
self.insert_course_item()
|
||||
self.insert_course_chart()
|
||||
self.insert_course_requirement()
|
||||
|
||||
|
||||
class UserCourse(Course):
|
||||
'''
|
||||
用户课题类
|
||||
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
# self.is_completed: bool = False
|
||||
self.high_score: int = None
|
||||
self.best_clear_type: int = None
|
||||
|
||||
@property
|
||||
def is_completed(self) -> bool:
|
||||
return self.best_clear_type != 0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = super().to_dict()
|
||||
if self.best_clear_type is None:
|
||||
self.select_user_course()
|
||||
r.update({
|
||||
'is_completed': self.is_completed,
|
||||
'high_score': self.high_score,
|
||||
'best_clear_type': self.best_clear_type
|
||||
})
|
||||
return r
|
||||
|
||||
def select_user_course(self, course_id: str = None) -> None:
|
||||
if course_id is not None:
|
||||
self.course_id = course_id
|
||||
self.c.execute('''select * from user_course where user_id = ? and course_id = ?''',
|
||||
(self.user.user_id, self.course_id))
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
self.high_score = 0
|
||||
self.best_clear_type = 0
|
||||
else:
|
||||
self.high_score = x[2]
|
||||
self.best_clear_type = x[3]
|
||||
|
||||
def insert_or_update_user_course(self) -> None:
|
||||
self.c.execute('''insert or replace into user_course values (?,?,?,?)''',
|
||||
(self.user.user_id, self.course_id, self.high_score, self.best_clear_type))
|
||||
|
||||
|
||||
class UserCourseList:
|
||||
'''
|
||||
用户课题列表类
|
||||
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.courses: list = []
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
# 临时修复满足条件也无法解锁隐藏段位的问题
|
||||
completed_course_id_list: list = []
|
||||
r: list = []
|
||||
for x in self.courses:
|
||||
if x.is_completed:
|
||||
completed_course_id_list.append(x.course_id)
|
||||
r.append(x.to_dict())
|
||||
for x in r:
|
||||
for i in x['requirements']:
|
||||
if i['value'] in completed_course_id_list:
|
||||
i['is_fullfilled'] = True
|
||||
return r
|
||||
|
||||
def select_all(self) -> None:
|
||||
self.c.execute('''select * from course''')
|
||||
self.courses = [UserCourse(self.c, self.user).from_list(x)
|
||||
for x in self.c.fetchall()]
|
||||
for i in self.courses:
|
||||
i.select_user_course()
|
||||
i.select_course_chart()
|
||||
i.select_course_item()
|
||||
i.select_course_requirement()
|
||||
|
||||
|
||||
class CoursePlay(UserCourse):
|
||||
'''
|
||||
课题模式打歌类,联动UserPlay
|
||||
|
||||
parameter: `user` - `UserOnline`类或子类的实例
|
||||
'user_play` - `UserPlay`类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None, user_play=None) -> None:
|
||||
super().__init__(c, user)
|
||||
self.user_play = user_play
|
||||
|
||||
self.score: int = None
|
||||
self.clear_type: int = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'rewards': [x.to_dict() for x in self.items],
|
||||
"current_stamina": self.user.stamina.stamina,
|
||||
"max_stamina_ts": self.user.stamina.max_stamina_ts,
|
||||
'user_course_banners': self.user.course_banners
|
||||
}
|
||||
|
||||
def update(self) -> None:
|
||||
'''课题模式更新'''
|
||||
self.select_user_course()
|
||||
|
||||
self.score += self.user_play.score
|
||||
|
||||
flag = False
|
||||
if self.score > self.high_score:
|
||||
self.high_score = self.score
|
||||
flag = True
|
||||
|
||||
if self.user_play.health < 0:
|
||||
# 你挂了
|
||||
self.user_play.course_play_state = 5
|
||||
self.score = 0
|
||||
self.clear_type = 0
|
||||
self.user_play.update_play_state_for_course()
|
||||
if flag:
|
||||
self.insert_or_update_user_course()
|
||||
return None
|
||||
|
||||
self.user_play.course_play_state += 1
|
||||
|
||||
from .score import Score
|
||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type):
|
||||
self.clear_type = self.user_play.clear_type
|
||||
self.user_play.update_play_state_for_course()
|
||||
|
||||
if self.user_play.course_play_state == 4:
|
||||
self.user.select_user_about_stamina()
|
||||
if not self.is_completed:
|
||||
self.select_course_item()
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
|
||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.best_clear_type):
|
||||
self.best_clear_type = self.clear_type
|
||||
flag = True
|
||||
|
||||
if flag:
|
||||
self.insert_or_update_user_course()
|
||||
354
latest version/core/download.py
Normal file
354
latest version/core/download.py
Normal file
@@ -0,0 +1,354 @@
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from json import loads
|
||||
from time import time
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import NoAccess
|
||||
from .limiter import ArcLimiter
|
||||
from .user import User
|
||||
from .util import get_file_md5, md5
|
||||
|
||||
|
||||
@lru_cache(maxsize=8192)
|
||||
def get_song_file_md5(song_id: str, file_name: str) -> str:
|
||||
path = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, file_name)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
return get_file_md5(path)
|
||||
|
||||
|
||||
class SonglistParser:
|
||||
'''songlist文件解析器'''
|
||||
|
||||
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff', '4.aff',
|
||||
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4']
|
||||
|
||||
has_songlist = False
|
||||
songs: dict = {} # {song_id: value, ...}
|
||||
# value: bitmap
|
||||
|
||||
pack_info: 'dict[str, set]' = {} # {pack_id: {song_id, ...}, ...}
|
||||
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, v in enumerate(SonglistParser.FILE_NAMES):
|
||||
if file_name == v 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 |= 32
|
||||
for i in song.get('difficulties', []):
|
||||
if i['ratingClass'] == 3 and i.get('audioOverride', False):
|
||||
r |= 64
|
||||
r |= 1 << i['ratingClass']
|
||||
else: # 針對remote_dl為False時BYD難度強制下載的處理
|
||||
for i in song.get('difficulties', []):
|
||||
if i['ratingClass'] == 3:
|
||||
r |= 8
|
||||
if i.get('audioOverride', False):
|
||||
r |= 64
|
||||
|
||||
for extra_file in song.get('additional_files', []):
|
||||
x = extra_file['file_name']
|
||||
if x == SonglistParser.FILE_NAMES[7]:
|
||||
r |= 128
|
||||
elif x == SonglistParser.FILE_NAMES[8]:
|
||||
r |= 256
|
||||
elif x == SonglistParser.FILE_NAMES[9]:
|
||||
r |= 512
|
||||
elif x == SonglistParser.FILE_NAMES[10]:
|
||||
r |= 1024
|
||||
|
||||
return {song['id']: r}
|
||||
|
||||
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:
|
||||
'''
|
||||
用户下载类
|
||||
|
||||
properties: `user` - `UserInfo`类或子类的实例
|
||||
'''
|
||||
|
||||
limiter = ArcLimiter(
|
||||
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.song_id: str = None
|
||||
self.file_name: str = None
|
||||
|
||||
self.token: str = None
|
||||
self.token_time: int = None
|
||||
|
||||
@property
|
||||
def is_limited(self) -> bool:
|
||||
'''是否达到用户最大下载量'''
|
||||
if self.user is None:
|
||||
self.select_for_check()
|
||||
|
||||
return not self.limiter.test(str(self.user.user_id))
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
'''链接是否有效且未过期'''
|
||||
if self.token_time is None:
|
||||
self.select_for_check()
|
||||
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT
|
||||
|
||||
def download_hit(self) -> bool:
|
||||
'''下载次数+1,返回成功与否bool值'''
|
||||
return self.limiter.hit(str(self.user.user_id))
|
||||
|
||||
def select_for_check(self) -> None:
|
||||
'''利用token、song_id、file_name查询其它信息'''
|
||||
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))
|
||||
|
||||
x = self.c_m.fetchone()
|
||||
if not x:
|
||||
raise NoAccess('The token `%s` is not valid.' %
|
||||
self.token, status=403)
|
||||
self.user = User()
|
||||
self.user.user_id = x[0]
|
||||
self.token_time = x[1]
|
||||
|
||||
def generate_token(self) -> None:
|
||||
self.token_time = int(time())
|
||||
self.token = md5(str(self.user.user_id) + self.song_id +
|
||||
self.file_name + str(self.token_time) + str(os.urandom(8)))
|
||||
|
||||
def insert_download_token(self) -> None:
|
||||
'''将数据插入数据库,让这个下载链接可用'''
|
||||
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})
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
'''生成下载链接'''
|
||||
if self.token is None:
|
||||
self.generate_token()
|
||||
# self.insert_download_token() # 循环插入速度慢,改为executemany
|
||||
if Constant.DOWNLOAD_LINK_PREFIX:
|
||||
prefix = Constant.DOWNLOAD_LINK_PREFIX
|
||||
if prefix[-1] != '/':
|
||||
prefix += '/'
|
||||
return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
|
||||
return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
|
||||
|
||||
@property
|
||||
def hash(self) -> str:
|
||||
return get_song_file_md5(self.song_id, self.file_name)
|
||||
|
||||
|
||||
class DownloadList(UserDownload):
|
||||
'''
|
||||
下载列表类
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c_m=None, user=None) -> None:
|
||||
super().__init__(c_m, user)
|
||||
|
||||
self.song_ids: list = None
|
||||
self.url_flag: bool = None
|
||||
|
||||
self.downloads: list = []
|
||||
self.urls: dict = {}
|
||||
|
||||
@classmethod
|
||||
def initialize_cache(cls) -> None:
|
||||
'''初始化歌曲数据缓存,包括md5、文件目录遍历、解析songlist'''
|
||||
SonglistParser()
|
||||
if Config.SONG_FILE_HASH_PRE_CALCULATE:
|
||||
x = cls()
|
||||
x.url_flag = False
|
||||
x.add_songs()
|
||||
del x
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
|
||||
re = {}
|
||||
for i in self.get_one_song_file_names(song_id):
|
||||
x = UserDownload(self.c_m, self.user)
|
||||
self.downloads.append(x)
|
||||
x.song_id = song_id
|
||||
x.file_name = i
|
||||
if i == 'base.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
re['audio']["checksum"] = x.hash
|
||||
if self.url_flag:
|
||||
re['audio']["url"] = x.url
|
||||
elif i == '3.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
if self.url_flag:
|
||||
re['audio']['3'] = {"checksum": x.hash, "url": x.url}
|
||||
else:
|
||||
re['audio']['3'] = {"checksum": x.hash}
|
||||
elif i in ('video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4'):
|
||||
if 'additional_files' not in re:
|
||||
re['additional_files'] = []
|
||||
|
||||
if self.url_flag:
|
||||
re['additional_files'].append(
|
||||
{"checksum": x.hash, "url": x.url, 'file_name': i})
|
||||
else:
|
||||
re['additional_files'].append(
|
||||
{"checksum": x.hash, 'file_name': i})
|
||||
# 有参数 requirement 作用未知
|
||||
else:
|
||||
if 'chart' not in re:
|
||||
re['chart'] = {}
|
||||
|
||||
if self.url_flag:
|
||||
re['chart'][i[0]] = {"checksum": x.hash, "url": x.url}
|
||||
else:
|
||||
re['chart'][i[0]] = {"checksum": x.hash}
|
||||
|
||||
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:
|
||||
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
|
||||
if song_ids is not None:
|
||||
self.song_ids = song_ids
|
||||
|
||||
if not self.song_ids:
|
||||
self.song_ids = self.get_all_song_ids()
|
||||
if Config.DOWNLOAD_FORBID_WHEN_NO_ITEM and SonglistParser.has_songlist:
|
||||
# 没有歌曲时不允许下载
|
||||
self.song_ids = list(SonglistParser.get_user_unlocks(
|
||||
self.user) & set(self.song_ids))
|
||||
|
||||
for i in self.song_ids:
|
||||
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)):
|
||||
self.add_one_song(i)
|
||||
|
||||
if self.url_flag:
|
||||
self.clear_download_token()
|
||||
self.insert_download_tokens()
|
||||
120
latest version/core/error.py
Normal file
120
latest version/core/error.py
Normal file
@@ -0,0 +1,120 @@
|
||||
class ArcError(Exception):
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
self.message: str = message
|
||||
self.error_code: int = error_code
|
||||
self.api_error_code: int = api_error_code
|
||||
self.extra_data = extra_data
|
||||
self.status: int = status
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class InputError(ArcError):
|
||||
'''输入类型错误'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class DataExist(ArcError):
|
||||
'''数据存在'''
|
||||
|
||||
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):
|
||||
'''数据不存在'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class PostError(ArcError):
|
||||
'''缺少输入'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class UserBan(ArcError):
|
||||
'''用户封禁'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class ItemNotEnough(ArcError):
|
||||
'''物品数量不足'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class ItemUnavailable(ArcError):
|
||||
'''物品不可用'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class RedeemUnavailable(ArcError):
|
||||
'''兑换码不可用'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class MapLocked(ArcError):
|
||||
'''地图锁定'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class StaminaNotEnough(ArcError):
|
||||
'''体力不足'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TicketNotEnough(ArcError):
|
||||
'''记忆源点不足'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class FriendError(ArcError):
|
||||
'''好友系统出错'''
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class NoAccess(ArcError):
|
||||
'''无权限'''
|
||||
|
||||
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 LowVersion(ArcError):
|
||||
'''版本过低'''
|
||||
|
||||
def __init__(self, message=None, error_code=5, api_error_code=-999, extra_data=None, status=403) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class Timeout(ArcError):
|
||||
'''超时'''
|
||||
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)
|
||||
368
latest version/core/init.py
Normal file
368
latest version/core/init.py
Normal file
@@ -0,0 +1,368 @@
|
||||
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.bundle import BundleParser
|
||||
from core.config_manager import Config
|
||||
from core.constant import ARCAEA_DATABASE_VERSION, ARCAEA_LOG_DATBASE_VERSION
|
||||
from core.course import Course
|
||||
from core.download import DownloadList
|
||||
from core.purchase import Purchase
|
||||
from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
|
||||
MemoryDatabase)
|
||||
from core.user import UserRegister
|
||||
from core.util import try_rename
|
||||
from core.world import MapParser
|
||||
|
||||
|
||||
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_DATABASE_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))
|
||||
self.c.execute('''insert into item values(?,?,?)''',
|
||||
('pick_ticket', 'pick_ticket', 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))
|
||||
|
||||
self.c.execute('''insert into item values(?,?,?)''', # 新手任务奖励曲
|
||||
('innocence', 'world_song', 1))
|
||||
|
||||
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 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_LOG_DATBASE_VERSION})
|
||||
|
||||
def init(self) -> None:
|
||||
with Connect(self.db_path) as c:
|
||||
self.c = c
|
||||
self.table_init()
|
||||
|
||||
|
||||
class DeletedDatabaseInit(DatabaseInit):
|
||||
def __init__(self, db_path: str = Config.SQLITE_DATABASE_DELETED_PATH) -> None:
|
||||
super().__init__(db_path)
|
||||
|
||||
def init(self) -> None:
|
||||
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_log(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
|
||||
|
||||
return True
|
||||
|
||||
def _check_update_database_main(self, db_path=Config.SQLITE_DATABASE_PATH, init_class=DatabaseInit) -> bool:
|
||||
if not self.check_file(db_path):
|
||||
# 新建数据库
|
||||
try:
|
||||
self.logger.info(f'Try to new the file `{db_path}`.')
|
||||
init_class().init()
|
||||
self.logger.info(f'Success to new the file `{db_path}`.')
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning(f'Failed to new the file `{db_path}`.')
|
||||
return False
|
||||
else:
|
||||
# 检查更新
|
||||
with Connect(db_path) 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_DATABASE_VERSION:
|
||||
self.logger.warning(
|
||||
f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}')
|
||||
try:
|
||||
self.logger.info(
|
||||
f'Try to update the file `{db_path}` to version {ARCAEA_DATABASE_VERSION}.')
|
||||
|
||||
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
|
||||
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
|
||||
|
||||
backup_path = try_rename(db_path, os.path.join(
|
||||
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(db_path)[-1] + '.bak'))
|
||||
|
||||
try:
|
||||
copy2(backup_path, db_path)
|
||||
except:
|
||||
copy(backup_path, db_path)
|
||||
|
||||
temp_path = os.path.join(
|
||||
*os.path.split(db_path)[:-1], 'old_arcaea_database.db')
|
||||
if os.path.isfile(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
try_rename(db_path, temp_path)
|
||||
|
||||
init_class().init()
|
||||
self.update_database(temp_path, db_path)
|
||||
|
||||
self.logger.info(
|
||||
f'Success to update the file `{db_path}`.')
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning(
|
||||
f'Fail to update the file `{db_path}`.')
|
||||
|
||||
return True
|
||||
|
||||
def check_update_database(self) -> bool:
|
||||
return self._check_update_database_main() and self._check_update_database_log() and self._check_update_database_main(Config.SQLITE_DATABASE_DELETED_PATH, DeletedDatabaseInit)
|
||||
|
||||
@staticmethod
|
||||
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("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.')
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning('Song data initialization error!')
|
||||
f = False
|
||||
return f
|
||||
|
||||
def check_content_bundle(self) -> bool:
|
||||
'''检查 content bundle 有关文件并初始化缓存'''
|
||||
f = self.check_folder(Config.CONTENT_BUNDLE_FOLDER_PATH)
|
||||
self.logger.info("Initialize content bundle data...")
|
||||
try:
|
||||
BundleParser()
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning('Content bundle data initialization error!')
|
||||
f = False
|
||||
return f
|
||||
|
||||
def check_world_map(self) -> bool:
|
||||
'''检查 world map 有关文件并初始化缓存'''
|
||||
f = self.check_folder(Config.WORLD_MAP_FOLDER_PATH)
|
||||
self.logger.info("Initialize world map data...")
|
||||
try:
|
||||
MapParser()
|
||||
except Exception as e:
|
||||
self.logger.error(format_exc())
|
||||
self.logger.warning('World map data initialization error!')
|
||||
f = False
|
||||
return f
|
||||
|
||||
def check_before_run(self) -> bool:
|
||||
'''运行前检查,返回布尔值'''
|
||||
MemoryDatabase() # 初始化内存数据库
|
||||
return self.check_song_file() and self.check_content_bundle() and self.check_update_database() and self.check_world_map()
|
||||
526
latest version/core/item.py
Normal file
526
latest version/core/item.py
Normal file
@@ -0,0 +1,526 @@
|
||||
from .config_manager import Config
|
||||
from .error import (DataExist, InputError, ItemNotEnough, ItemUnavailable,
|
||||
NoData)
|
||||
|
||||
|
||||
class Item:
|
||||
item_type = None
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.item_id = None
|
||||
self.__amount = 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
|
||||
def amount(self):
|
||||
return self.__amount
|
||||
|
||||
@amount.setter
|
||||
def amount(self, value: int):
|
||||
self.__amount = int(value)
|
||||
|
||||
def to_dict(self, has_is_available: bool = False, has_amount: bool = True) -> dict:
|
||||
r = {
|
||||
'id': self.item_id,
|
||||
'type': self.item_type
|
||||
}
|
||||
if has_amount:
|
||||
r['amount'] = self.amount
|
||||
if has_is_available:
|
||||
r['is_available'] = self.is_available
|
||||
return r
|
||||
|
||||
def user_claim_item(self, user):
|
||||
# parameter: user - User类或子类的实例
|
||||
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):
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.user = None
|
||||
|
||||
def select_user_item(self, user=None):
|
||||
'''
|
||||
查询用户item
|
||||
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
self.c.execute('''select amount from user_item where user_id=? and item_id=? and type=?''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.amount = x[0] if x[0] is not None else 1
|
||||
else:
|
||||
self.amount = 0
|
||||
|
||||
|
||||
class NormalItem(UserItem):
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def user_claim_item(self, user):
|
||||
self.user = user
|
||||
if not self.is_available:
|
||||
self.c.execute(
|
||||
'''select is_available from item where item_id=? and type=?''', (self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
if x[0] == 0:
|
||||
self.is_available = False
|
||||
raise ItemUnavailable('The item is unavailable.')
|
||||
self.is_available = True
|
||||
else:
|
||||
raise NoData('No item data.')
|
||||
|
||||
self.c.execute('''select exists(select * from user_item where user_id=? and item_id=? and type=?)''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute('''insert into user_item values(:a,:b,:c,1)''',
|
||||
{'a': self.user.user_id, 'b': self.item_id, 'c': self.item_type})
|
||||
|
||||
|
||||
class PositiveItem(UserItem):
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''添加或使用用户item,注意是+amount'''
|
||||
self.user = user
|
||||
self.c.execute('''select amount from user_item where user_id=? and item_id=? and type=?''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
if x[0] + self.amount < 0: # 数量不足
|
||||
raise ItemNotEnough(
|
||||
'The user does not have enough `%s`.' % self.item_id)
|
||||
self.c.execute('''update user_item set amount=? where user_id=? and item_id=? and type=?''',
|
||||
(x[0]+self.amount, self.user.user_id, self.item_id, self.item_type))
|
||||
else:
|
||||
if self.amount < 0: # 添加数量错误
|
||||
raise InputError(
|
||||
'The amount of `%s` is wrong.' % self.item_id)
|
||||
self.c.execute('''insert into user_item values(?,?,?,?)''',
|
||||
(self.user.user_id, self.item_id, self.item_type, self.amount))
|
||||
|
||||
|
||||
class ItemCore(PositiveItem):
|
||||
item_type = 'core'
|
||||
|
||||
def __init__(self, c=None, core_type: str = '', amount: int = 0) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
self.item_id = core_type
|
||||
self.amount = amount
|
||||
|
||||
def __str__(self) -> str:
|
||||
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):
|
||||
item_type = 'character'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
|
||||
def set_id(self, character_id: str) -> None:
|
||||
# 将name: str转为character_id: int存到item_id里
|
||||
if character_id.isdigit():
|
||||
self.item_id = int(character_id)
|
||||
else:
|
||||
self.c.execute(
|
||||
'''select character_id from character where name=?''', (character_id,))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.item_id = x[0]
|
||||
else:
|
||||
raise NoData('No character `%s`.' % character_id)
|
||||
|
||||
def user_claim_item(self, user):
|
||||
if not isinstance(self.item_id, int):
|
||||
self.set_id(self.item_id)
|
||||
|
||||
self.c.execute(
|
||||
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user.user_id, self.item_id))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute(
|
||||
'''insert into user_char values(?,?,1,0,0,0,0)''', (user.user_id, self.item_id))
|
||||
|
||||
|
||||
class Memory(UserItem):
|
||||
item_type = 'memory'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
|
||||
def user_claim_item(self, user):
|
||||
self.c.execute(
|
||||
'''select ticket from user where user_id=?''', (user.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
self.c.execute('''update user set ticket=? where user_id=?''',
|
||||
(x[0]+self.amount, user.user_id))
|
||||
else:
|
||||
raise NoData('The ticket of the user is null.')
|
||||
|
||||
|
||||
class Fragment(UserItem):
|
||||
item_type = 'fragment'
|
||||
|
||||
def __init__(self, c=None, amount=0) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
self.item_id = self.item_type
|
||||
self.amount = amount
|
||||
|
||||
def user_claim_item(self, user):
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'fragment' + str(self.amount)
|
||||
|
||||
|
||||
class Anni5tix(PositiveItem):
|
||||
item_type = 'anni5tix'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
self.item_id = self.item_type
|
||||
self.amount = 1
|
||||
|
||||
|
||||
class PickTicket(PositiveItem):
|
||||
item_type = 'pick_ticket'
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
self.item_id = self.item_type
|
||||
self.amount = 1
|
||||
|
||||
|
||||
class WorldSong(NormalItem):
|
||||
item_type = 'world_song'
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
|
||||
class WorldUnlock(NormalItem):
|
||||
item_type = 'world_unlock'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
|
||||
class CourseBanner(NormalItem):
|
||||
item_type = 'course_banner'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.item_id)
|
||||
|
||||
|
||||
class Single(NormalItem):
|
||||
item_type = 'single'
|
||||
|
||||
|
||||
class Pack(NormalItem):
|
||||
item_type = 'pack'
|
||||
|
||||
|
||||
class ProgBoost(UserItem):
|
||||
item_type = 'prog_boost_300'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''
|
||||
世界模式prog_boost
|
||||
|
||||
parameters: `user` - `UserOnline`类或子类的实例
|
||||
'''
|
||||
user.update_user_one_column('prog_boost', 300)
|
||||
|
||||
|
||||
class Stamina6(UserItem):
|
||||
item_type = 'stamina6'
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__(c)
|
||||
self.item_id = 'stamina6'
|
||||
self.amount = 1
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''
|
||||
世界模式记忆源点或残片买体力+6
|
||||
顺手清一下世界模式过载状态
|
||||
'''
|
||||
user.select_user_about_stamina()
|
||||
user.stamina.stamina += 6
|
||||
user.stamina.update()
|
||||
user.update_user_one_column('world_mode_locked_end_ts', -1)
|
||||
|
||||
|
||||
class ItemStamina(UserItem):
|
||||
item_type = 'stamina'
|
||||
|
||||
def __init__(self, c=None, amount=1) -> None:
|
||||
super().__init__(c)
|
||||
self.item_id = 'stamina'
|
||||
self.amount = amount
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''
|
||||
新手任务奖励体力
|
||||
'''
|
||||
user.select_user_about_stamina()
|
||||
user.stamina.stamina += self.amount
|
||||
user.stamina.update()
|
||||
|
||||
|
||||
class ItemFactory:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
|
||||
def get_item(self, item_type: str):
|
||||
'''
|
||||
根据item_type实例化对应的item类
|
||||
return: Item类或子类的实例
|
||||
'''
|
||||
if item_type == 'core':
|
||||
return ItemCore(self.c)
|
||||
elif item_type == 'character':
|
||||
return ItemCharacter(self.c)
|
||||
elif item_type == 'memory':
|
||||
return Memory(self.c)
|
||||
elif item_type == 'anni5tix':
|
||||
return Anni5tix(self.c)
|
||||
elif item_type == 'pick_ticket':
|
||||
return PickTicket(self.c)
|
||||
elif item_type == 'world_song':
|
||||
return WorldSong(self.c)
|
||||
elif item_type == 'world_unlock':
|
||||
return WorldUnlock(self.c)
|
||||
elif item_type == 'single':
|
||||
return Single(self.c)
|
||||
elif item_type == 'pack':
|
||||
return Pack(self.c)
|
||||
elif item_type == 'fragment':
|
||||
return Fragment(self.c)
|
||||
elif item_type == 'prog_boost_300':
|
||||
return ProgBoost(self.c)
|
||||
elif item_type == 'stamina6':
|
||||
return Stamina6(self.c)
|
||||
elif item_type == 'course_banner':
|
||||
return CourseBanner(self.c)
|
||||
else:
|
||||
raise InputError(
|
||||
f'The item type `{item_type}` is invalid.', api_error_code=-120)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict, c=None):
|
||||
'''注意这里没有处理character_id的转化,是为了世界模式的map服务的'''
|
||||
if 'item_type' in d:
|
||||
item_type = d['item_type']
|
||||
elif 'type' in d:
|
||||
item_type = d['type']
|
||||
else:
|
||||
raise InputError('The dict of item is wrong.')
|
||||
i = cls().get_item(item_type)
|
||||
if c is not None:
|
||||
i.c = c
|
||||
if 'item_id' in d:
|
||||
i.item_id = d['item_id']
|
||||
elif 'id' in d:
|
||||
i.item_id = d['id']
|
||||
else:
|
||||
i.item_id = item_type
|
||||
i.amount = d.get('amount', 1)
|
||||
i.is_available = d.get('is_available', True)
|
||||
return i
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s: str, c=None):
|
||||
if s.startswith('fragment'):
|
||||
item_type = 'fragment'
|
||||
item_id = 'fragment'
|
||||
amount = int(s[8:])
|
||||
elif s.startswith('core'):
|
||||
item_type = 'core'
|
||||
x = s.split('_')
|
||||
item_id = x[0] + '_' + x[1]
|
||||
amount = int(x[-1])
|
||||
elif s.startswith('course_banner'):
|
||||
item_type = 'course_banner'
|
||||
item_id = s
|
||||
amount = 1
|
||||
else:
|
||||
raise InputError('The string of item is wrong.')
|
||||
i = cls().get_item(item_type)
|
||||
if c is not None:
|
||||
i.c = c
|
||||
i.item_id = item_id
|
||||
i.amount = amount
|
||||
i.is_available = True
|
||||
return i
|
||||
|
||||
|
||||
class UserItemList:
|
||||
'''
|
||||
用户的item列表
|
||||
注意只能查在user_item里面的,character不行
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.items: list = []
|
||||
|
||||
def select_from_type(self, item_type: str) -> 'UserItemList':
|
||||
'''
|
||||
根据item_type搜索用户的item
|
||||
'''
|
||||
if Config.WORLD_SONG_FULL_UNLOCK and item_type == 'world_song' or Config.WORLD_SCENERY_FULL_UNLOCK and item_type == 'world_unlock':
|
||||
self.c.execute(
|
||||
'''select item_id from item where type=?''', (item_type,))
|
||||
else:
|
||||
self.c.execute('''select item_id, amount from user_item where type = :a and user_id = :b''', {
|
||||
'a': item_type, 'b': self.user.user_id})
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return self
|
||||
|
||||
self.items: list = []
|
||||
for i in x:
|
||||
if len(i) > 1:
|
||||
amount = i[1] if i[1] is not None else 1
|
||||
else:
|
||||
amount = 1
|
||||
self.items.append(ItemFactory.from_dict(
|
||||
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
|
||||
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)
|
||||
361
latest version/core/linkplay.py
Normal file
361
latest version/core/linkplay.py
Normal file
@@ -0,0 +1,361 @@
|
||||
import socket
|
||||
from base64 import b64decode, b64encode
|
||||
from json import dumps, loads
|
||||
from threading import RLock
|
||||
from time import time
|
||||
|
||||
from core.error import ArcError, Timeout
|
||||
|
||||
from .constant import Constant
|
||||
from .user import UserInfo
|
||||
from .util import aes_gcm_128_decrypt, aes_gcm_128_encrypt
|
||||
|
||||
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
|
||||
|
||||
|
||||
def get_song_unlock(client_song_map: 'dict[str, list]') -> bytes:
|
||||
'''处理可用歌曲bit,返回bytes'''
|
||||
|
||||
user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH
|
||||
|
||||
for k, v in client_song_map.items():
|
||||
for i in range(5):
|
||||
if not v[i]:
|
||||
continue
|
||||
index = int(k) * 5 + i
|
||||
if index >= Constant.LINKPLAY_UNLOCK_LENGTH * 8:
|
||||
continue
|
||||
user_song_unlock[index // 8] |= 1 << (index % 8)
|
||||
|
||||
return bytes(user_song_unlock)
|
||||
|
||||
|
||||
class Player(UserInfo):
|
||||
def __init__(self, c=None, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
self.player_id: int = 0
|
||||
self.token: int = 0
|
||||
self.key: bytes = None
|
||||
|
||||
self.__song_unlock: bytes = None
|
||||
self.client_song_map: dict = None
|
||||
|
||||
self.last_match_timestamp: int = 0
|
||||
self.match_times: int = None # 已匹配次数,减 1 后乘 5 就大致是匹配时间
|
||||
self.match_room: Room = None # 匹配到的房间,这个仅用来在两个人同时匹配时使用,一人建房,通知另一个人加入
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'userId': self.user_id,
|
||||
'playerId': str(self.player_id),
|
||||
'token': str(self.token),
|
||||
'key': (b64encode(self.key)).decode()
|
||||
}
|
||||
|
||||
@property
|
||||
def song_unlock(self) -> bytes:
|
||||
if self.__song_unlock is None:
|
||||
self.get_song_unlock()
|
||||
return self.__song_unlock
|
||||
|
||||
def get_song_unlock(self, client_song_map: dict = None) -> bytes:
|
||||
if client_song_map is not None:
|
||||
self.client_song_map = client_song_map
|
||||
self.__song_unlock = get_song_unlock(self.client_song_map)
|
||||
|
||||
def calc_available_chart_num(self, song_unlock: bytes) -> int:
|
||||
'''计算交叠后可用谱面数量'''
|
||||
new_unlock = [i & j for i, j in zip(self.song_unlock, song_unlock)]
|
||||
s = 0
|
||||
for i in range(len(new_unlock)):
|
||||
for j in range(8):
|
||||
if new_unlock[i] & (1 << j):
|
||||
s += 1
|
||||
return s
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self) -> None:
|
||||
self.room_id: int = 0
|
||||
self.room_code: str = 'AAAA00'
|
||||
|
||||
self.song_unlock: bytes = None
|
||||
|
||||
self.share_token: str = 'abcde12345'
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'roomId': str(self.room_id),
|
||||
'roomCode': self.room_code,
|
||||
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode(),
|
||||
'shareToken': self.share_token
|
||||
}
|
||||
|
||||
|
||||
class RemoteMultiPlayer:
|
||||
TCP_AES_KEY = Constant.LINKPLAY_TCP_SECRET_KEY.encode(
|
||||
'utf-8').ljust(16, b'\x00')[:16]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.user: 'Player' = None
|
||||
self.room: 'Room' = None
|
||||
|
||||
self.data_recv: 'dict | list' = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return dict(self.room.to_dict(), **self.user.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def tcp(data: bytes) -> bytes:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.connect((Constant.LINKPLAY_HOST,
|
||||
Constant.LINKPLAY_TCP_PORT))
|
||||
|
||||
sock.sendall(data)
|
||||
try:
|
||||
cipher_len = int.from_bytes(sock.recv(8), byteorder='little')
|
||||
if cipher_len > Constant.LINKPLAY_TCP_MAX_LENGTH:
|
||||
raise ArcError(
|
||||
'Too long body from link play server', status=400)
|
||||
iv = sock.recv(12)
|
||||
tag = sock.recv(16)
|
||||
ciphertext = sock.recv(cipher_len)
|
||||
received = aes_gcm_128_decrypt(
|
||||
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||
except socket.timeout as e:
|
||||
raise Timeout(
|
||||
'Timeout when waiting for data from link play server.', status=400) from e
|
||||
# print(received)
|
||||
return received
|
||||
|
||||
def data_swap(self, data: dict) -> dict:
|
||||
|
||||
iv, ciphertext, tag = aes_gcm_128_encrypt(
|
||||
self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
|
||||
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
|
||||
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag + ciphertext
|
||||
recv_data = self.tcp(send_data)
|
||||
self.data_recv = loads(recv_data)
|
||||
|
||||
code = self.data_recv['code']
|
||||
if code != 0:
|
||||
raise ArcError(f'Link Play error code: {code}', code, status=400)
|
||||
|
||||
return self.data_recv
|
||||
|
||||
def create_room(self, user: 'Player' = None) -> None:
|
||||
'''创建房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
user.select_user_about_link_play()
|
||||
self.data_swap({
|
||||
'endpoint': 'create_room',
|
||||
'data': {
|
||||
'name': self.user.name,
|
||||
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||
'rating_ptt': self.user.rating_ptt,
|
||||
'is_hide_rating': self.user.is_hide_rating,
|
||||
'match_times': self.user.match_times
|
||||
}
|
||||
})
|
||||
|
||||
self.room = Room()
|
||||
x = self.data_recv['data']
|
||||
self.room.room_code = x['room_code']
|
||||
self.room.room_id = int(x['room_id'])
|
||||
self.room.song_unlock = self.user.song_unlock
|
||||
self.user.token = int(x['token'])
|
||||
self.user.key = b64decode(x['key'])
|
||||
self.user.player_id = int(x['player_id'])
|
||||
|
||||
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
|
||||
'''加入房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
if room is not None:
|
||||
self.room = room
|
||||
|
||||
self.user.select_user_about_link_play()
|
||||
self.data_swap({
|
||||
'endpoint': 'join_room',
|
||||
'data': {
|
||||
'name': self.user.name,
|
||||
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||
'room_code': self.room.room_code,
|
||||
'rating_ptt': self.user.rating_ptt,
|
||||
'is_hide_rating': self.user.is_hide_rating,
|
||||
'match_times': self.user.match_times
|
||||
}
|
||||
})
|
||||
x = self.data_recv['data']
|
||||
self.room.room_code = x['room_code']
|
||||
self.room.room_id = int(x['room_id'])
|
||||
self.room.song_unlock = b64decode(x['song_unlock'])
|
||||
self.user.token = int(x['token'])
|
||||
self.user.key = b64decode(x['key'])
|
||||
self.user.player_id = int(x['player_id'])
|
||||
|
||||
def update_room(self, user: 'Player' = None) -> None:
|
||||
'''更新房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self.user.select_user_about_link_play()
|
||||
self.data_swap({
|
||||
'endpoint': 'update_room',
|
||||
'data': {
|
||||
'token': self.user.token,
|
||||
'rating_ptt': self.user.rating_ptt,
|
||||
'is_hide_rating': self.user.is_hide_rating
|
||||
}
|
||||
})
|
||||
|
||||
self.room = Room()
|
||||
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.key = b64decode(x['key'])
|
||||
self.user.player_id = int(x['player_id'])
|
||||
|
||||
def get_rooms(self, offset=0, limit=50) -> dict:
|
||||
'''获取房间列表'''
|
||||
self.data_swap({
|
||||
'endpoint': 'get_rooms',
|
||||
'data': {
|
||||
'offset': offset,
|
||||
'limit': limit
|
||||
}
|
||||
})
|
||||
|
||||
return self.data_recv['data']
|
||||
|
||||
def select_room(self, room_code: str = None, share_token: str = None) -> dict:
|
||||
self.data_swap({
|
||||
'endpoint': 'select_room',
|
||||
'data': {
|
||||
'room_code': room_code,
|
||||
'share_token': share_token
|
||||
}
|
||||
})
|
||||
|
||||
return self.data_recv['data']
|
||||
|
||||
def get_match_rooms(self) -> dict:
|
||||
'''获取一定数量的公共房间列表'''
|
||||
self.data_swap({
|
||||
'endpoint': 'get_match_rooms',
|
||||
'data': {
|
||||
'limit': 100
|
||||
}
|
||||
})
|
||||
|
||||
return self.data_recv['data']
|
||||
|
||||
|
||||
class MatchStore:
|
||||
|
||||
last_get_rooms_timestamp = 0
|
||||
room_cache: 'list[Room]' = []
|
||||
|
||||
player_queue: 'dict[int, Player]' = {}
|
||||
|
||||
lock = RLock()
|
||||
|
||||
last_memory_clean_timestamp = 0
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.remote = RemoteMultiPlayer()
|
||||
|
||||
def refresh_rooms(self):
|
||||
now = time()
|
||||
if now - self.last_get_rooms_timestamp < Constant.LINKPLAY_MATCH_GET_ROOMS_INTERVAL:
|
||||
return
|
||||
MatchStore.room_cache = self.remote.get_match_rooms()['rooms']
|
||||
MatchStore.last_get_rooms_timestamp = now
|
||||
|
||||
def init_player(self, user: 'Player'):
|
||||
user.match_times = 0
|
||||
MatchStore.player_queue[user.user_id] = user
|
||||
user.last_match_timestamp = time()
|
||||
user.c = self.c
|
||||
user.select_user_about_link_play()
|
||||
user.c = None
|
||||
|
||||
def clear_player(self, user_id: int):
|
||||
MatchStore.player_queue.pop(user_id, None)
|
||||
|
||||
def clean_room_cache(self):
|
||||
MatchStore.room_cache = []
|
||||
MatchStore.last_get_rooms_timestamp = 0
|
||||
|
||||
def memory_clean(self):
|
||||
now = time()
|
||||
if now - self.last_memory_clean_timestamp < Constant.LINKPLAY_MEMORY_CLEAN_INTERVAL:
|
||||
return
|
||||
with self.lock:
|
||||
for i in MatchStore.player_queue:
|
||||
if now - i.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||
self.clear_player(i)
|
||||
|
||||
def match(self, user_id: int):
|
||||
user = MatchStore.player_queue.get(user_id)
|
||||
if user is None:
|
||||
raise ArcError(
|
||||
f'User `{user_id}` not found in match queue.', code=999)
|
||||
|
||||
if user.match_room is not None:
|
||||
# 二人开新房,第二人加入
|
||||
user.c = self.c
|
||||
self.remote.join_room(user.match_room, user)
|
||||
self.clear_player(user_id)
|
||||
return self.remote.to_dict()
|
||||
|
||||
self.refresh_rooms()
|
||||
|
||||
rule = min(user.match_times, len(Constant.LINKPLAY_MATCH_PTT_ABS) -
|
||||
1, len(Constant.LINKPLAY_MATCH_UNLOCK_MIN) - 1)
|
||||
ptt_abs = Constant.LINKPLAY_MATCH_PTT_ABS[rule]
|
||||
unlock_min = Constant.LINKPLAY_MATCH_UNLOCK_MIN[rule]
|
||||
|
||||
# 加入已有房间
|
||||
for i in MatchStore.room_cache:
|
||||
f = True
|
||||
num = 0
|
||||
for j in i['players']:
|
||||
if j['player_id'] != 0:
|
||||
num += 1
|
||||
if abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs:
|
||||
f = False
|
||||
break
|
||||
|
||||
# 有玩家非正常退房时,next_state_timestamp 不为 0,有概率新玩家进不来,所以使用 num 统计玩家数量
|
||||
|
||||
if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0 or num == 1):
|
||||
room = Room()
|
||||
room.room_code = i['room_code']
|
||||
user.c = self.c
|
||||
self.remote.join_room(room, user)
|
||||
self.clean_room_cache()
|
||||
self.clear_player(user_id)
|
||||
return self.remote.to_dict()
|
||||
|
||||
now = time()
|
||||
|
||||
# 二人开新房,第一人开房
|
||||
for p in MatchStore.player_queue.values():
|
||||
if p.user_id == user_id or now - p.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||
continue
|
||||
new_rule = min(rule, p.match_times)
|
||||
if abs(user.rating_ptt - p.rating_ptt) < Constant.LINKPLAY_MATCH_PTT_ABS[new_rule] and user.calc_available_chart_num(p.song_unlock) >= Constant.LINKPLAY_MATCH_UNLOCK_MIN[new_rule]:
|
||||
user.c = self.c
|
||||
self.remote.create_room(user)
|
||||
self.clear_player(user_id)
|
||||
p.match_room = self.remote.room
|
||||
return self.remote.to_dict()
|
||||
|
||||
user.match_times += 1
|
||||
user.last_match_timestamp = now
|
||||
|
||||
return None
|
||||
240
latest version/core/mission.py
Normal file
240
latest version/core/mission.py
Normal file
@@ -0,0 +1,240 @@
|
||||
from .item import Fragment, ItemCore, ItemStamina, PickTicket, WorldSong
|
||||
|
||||
|
||||
class Mission:
|
||||
mission_id: str = None
|
||||
items: list = []
|
||||
|
||||
def __init__(self, c=None):
|
||||
self.c = c
|
||||
self.user = None
|
||||
self._status: int = None
|
||||
|
||||
if self.c is not None:
|
||||
for i in self.items:
|
||||
i.c = self.c
|
||||
|
||||
def to_dict(self, has_items=False) -> dict:
|
||||
r = {
|
||||
'mission_id': self.mission_id,
|
||||
'status': self.status,
|
||||
}
|
||||
if has_items:
|
||||
r['items'] = [x.to_dict() for x in self.items]
|
||||
return r
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
if self._status == 1:
|
||||
return 'inprogress'
|
||||
elif self._status == 2:
|
||||
return 'cleared'
|
||||
elif self._status == 3:
|
||||
return 'prevclaimedfragmission'
|
||||
elif self._status == 4:
|
||||
return 'claimed'
|
||||
|
||||
return 'locked'
|
||||
|
||||
def user_claim_mission(self, user):
|
||||
# param: user - User 类或子类的实例
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 4)''',
|
||||
(self.user.user_id, self.mission_id))
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
self._status = 4
|
||||
|
||||
def user_clear_mission(self, user):
|
||||
# param: user - User 类或子类的实例
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 2)''',
|
||||
(self.user.user_id, self.mission_id))
|
||||
self._status = 2
|
||||
|
||||
def select_user_mission(self, user):
|
||||
# param: user - User 类或子类的实例
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self._status = 0
|
||||
self.c.execute('''select status from user_mission where user_id=? and mission_id=?''',
|
||||
(self.user.user_id, self.mission_id))
|
||||
x = self.c.fetchone()
|
||||
|
||||
if x and x[0]:
|
||||
self._status = x[0]
|
||||
|
||||
|
||||
class M11(Mission):
|
||||
mission_id = 'mission_1_1_tutorial'
|
||||
items = [Fragment(amount=10)]
|
||||
|
||||
|
||||
class M12(Mission):
|
||||
mission_id = 'mission_1_2_clearsong'
|
||||
items = [Fragment(amount=10)]
|
||||
|
||||
|
||||
class M13(Mission):
|
||||
mission_id = 'mission_1_3_settings'
|
||||
items = [Fragment(amount=10)]
|
||||
|
||||
|
||||
class M14(Mission):
|
||||
mission_id = 'mission_1_4_allsongsview'
|
||||
items = [Fragment(amount=10)]
|
||||
|
||||
|
||||
class M15(Mission):
|
||||
mission_id = 'mission_1_5_fragunlock'
|
||||
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||
|
||||
|
||||
class M1E(Mission):
|
||||
mission_id = 'mission_1_end'
|
||||
items = [Fragment(amount=100)]
|
||||
|
||||
|
||||
class M21(Mission):
|
||||
mission_id = 'mission_2_1_account'
|
||||
items = [Fragment(amount=20)]
|
||||
|
||||
|
||||
class M22(Mission):
|
||||
mission_id = 'mission_2_2_profile'
|
||||
items = [Fragment(amount=20)]
|
||||
|
||||
|
||||
class M23(Mission):
|
||||
mission_id = 'mission_2_3_partner'
|
||||
items = [Fragment(amount=20)]
|
||||
|
||||
|
||||
class M24(Mission):
|
||||
mission_id = 'mission_2_4_usestamina'
|
||||
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||
|
||||
|
||||
class M25(Mission):
|
||||
mission_id = 'mission_2_5_prologuestart'
|
||||
items = [ItemCore(core_type='core_generic', amount=1)]
|
||||
|
||||
|
||||
class M2E(Mission):
|
||||
mission_id = 'mission_2_end'
|
||||
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||
|
||||
|
||||
class M31(Mission):
|
||||
mission_id = 'mission_3_1_prsclear'
|
||||
items = [Fragment(amount=50)]
|
||||
|
||||
|
||||
class M32(Mission):
|
||||
mission_id = 'mission_3_2_etherdrop'
|
||||
items = [ItemStamina(amount=2)]
|
||||
|
||||
|
||||
class M33(Mission):
|
||||
mission_id = 'mission_3_3_step50'
|
||||
items = [Fragment(amount=50)]
|
||||
|
||||
|
||||
class M34(Mission):
|
||||
mission_id = 'mission_3_4_frag60'
|
||||
items = [ItemStamina(amount=2)]
|
||||
|
||||
|
||||
class M3E(Mission):
|
||||
mission_id = 'mission_3_end'
|
||||
items = [ItemStamina(amount=6)]
|
||||
|
||||
|
||||
class M41(Mission):
|
||||
mission_id = 'mission_4_1_exgrade'
|
||||
items = [Fragment(amount=100)]
|
||||
|
||||
|
||||
class M42(Mission):
|
||||
mission_id = 'mission_4_2_potential350'
|
||||
items = [ItemStamina(amount=2)]
|
||||
|
||||
|
||||
class M43(Mission):
|
||||
mission_id = 'mission_4_3_twomaps'
|
||||
items = [Fragment(amount=100)]
|
||||
|
||||
|
||||
class M44(Mission):
|
||||
mission_id = 'mission_4_4_worldsongunlock'
|
||||
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||
|
||||
|
||||
class M45(Mission):
|
||||
mission_id = 'mission_4_5_prologuefinish'
|
||||
items = [ItemStamina(amount=2)]
|
||||
|
||||
|
||||
_innocence = WorldSong()
|
||||
_innocence.amount = 1
|
||||
_innocence.item_id = 'innocence'
|
||||
|
||||
|
||||
class M4E(Mission):
|
||||
mission_id = 'mission_4_end'
|
||||
items = [_innocence]
|
||||
|
||||
|
||||
class M51(Mission):
|
||||
mission_id = 'mission_5_1_songgrouping'
|
||||
items = [Fragment(amount=50)]
|
||||
|
||||
|
||||
class M52(Mission):
|
||||
mission_id = 'mission_5_2_partnerlv12'
|
||||
items = [Fragment(amount=250)]
|
||||
|
||||
|
||||
class M53(Mission):
|
||||
mission_id = 'mission_5_3_cores'
|
||||
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||
|
||||
|
||||
class M54(Mission):
|
||||
mission_id = 'mission_5_4_courseclear'
|
||||
items = [ItemCore(core_type='core_generic', amount=3)]
|
||||
|
||||
|
||||
class M5E(Mission):
|
||||
mission_id = 'mission_5_end'
|
||||
items = [PickTicket()]
|
||||
|
||||
|
||||
MISSION_DICT = {i.mission_id: i for i in Mission.__subclasses__()}
|
||||
|
||||
|
||||
class UserMissionList:
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.missions: list = []
|
||||
|
||||
def select_all(self):
|
||||
self.missions = []
|
||||
self.c.execute('''select mission_id, status from user_mission where user_id=?''',
|
||||
(self.user.user_id,))
|
||||
for i in self.c.fetchall():
|
||||
x = MISSION_DICT[i[0]]()
|
||||
x._status = i[1]
|
||||
self.missions.append(x)
|
||||
|
||||
return self
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [i.to_dict() for i in self.missions]
|
||||
101
latest version/core/notification.py
Normal file
101
latest version/core/notification.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from .config_manager import Config
|
||||
from .user import User
|
||||
from .sql import Connect
|
||||
|
||||
from time import time
|
||||
|
||||
|
||||
class BaseNotification:
|
||||
|
||||
notification_type = None
|
||||
|
||||
def __init__(self, c_m=None) -> None:
|
||||
self.receiver = None
|
||||
self.sender = None
|
||||
self.timestamp = None
|
||||
self.content = None
|
||||
|
||||
self.c_m = c_m
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
now = round(time() * 1000)
|
||||
return now - self.timestamp > Config.NOTIFICATION_EXPIRE_TIME
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
def insert(self):
|
||||
self.receiver.select_user_one_column('mp_notification_enabled', True, bool)
|
||||
if not self.receiver.mp_notification_enabled:
|
||||
return
|
||||
|
||||
self.c_m.execute(
|
||||
'''select max(id) from notification where user_id = ?''', (self.receiver.user_id,))
|
||||
x = self.c_m.fetchone()
|
||||
if x is None or x[0] is None:
|
||||
x = 0
|
||||
else:
|
||||
x = x[0] + 1
|
||||
|
||||
self.c_m.execute(
|
||||
'''insert into notification values (?, ?, ?, ?, ?, ?, ?)''',
|
||||
(self.receiver.user_id, x, self.notification_type, self.content,
|
||||
self.sender.user_id, self.sender.name, self.timestamp)
|
||||
)
|
||||
|
||||
|
||||
class RoomInviteNotification(BaseNotification):
|
||||
|
||||
notification_type = 'room_inv'
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, l: list, user: User = None) -> 'RoomInviteNotification':
|
||||
x = cls()
|
||||
x.sender = User()
|
||||
x.sender.user_id = l[2]
|
||||
x.sender.name = l[3]
|
||||
x.content = l[1]
|
||||
x.timestamp = l[4]
|
||||
x.receiver = user
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def from_sender(cls, sender: User, receiver: User, share_token: str, c_m) -> 'RoomInviteNotification':
|
||||
x = cls()
|
||||
x.c_m = c_m
|
||||
x.sender = sender
|
||||
x.receiver = receiver
|
||||
x.content = share_token
|
||||
x.timestamp = round(time() * 1000)
|
||||
return x
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'sender': self.sender.name,
|
||||
'type': self.notification_type,
|
||||
'shareToken': self.content,
|
||||
'sendTs': self.timestamp
|
||||
}
|
||||
|
||||
|
||||
class NotificationFactory:
|
||||
def __init__(self, c_m: Connect, user=None):
|
||||
self.c_m = c_m
|
||||
self.user = user
|
||||
|
||||
def get_notification(self) -> 'list[BaseNotification]':
|
||||
r = []
|
||||
|
||||
self.c_m.execute('''select type, content, sender_user_id, sender_name, timestamp from notification where user_id = ?''',
|
||||
(self.user.user_id,))
|
||||
for i in self.c_m.fetchall():
|
||||
x = None
|
||||
if i[0] == 'room_inv':
|
||||
x = RoomInviteNotification.from_list(i, self.user)
|
||||
|
||||
if x is not None and not x.is_expired:
|
||||
r.append(x)
|
||||
self.c_m.execute(
|
||||
'''delete from notification where user_id = ?''', (self.user.user_id,))
|
||||
return r
|
||||
387
latest version/core/operation.py
Normal file
387
latest version/core/operation.py
Normal file
@@ -0,0 +1,387 @@
|
||||
from .bundle import BundleParser
|
||||
from .constant import Constant
|
||||
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
|
||||
from .world import MapParser
|
||||
|
||||
|
||||
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):
|
||||
'''
|
||||
刷新所有成绩的评分
|
||||
包括 score_v2
|
||||
'''
|
||||
_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, rating_etr 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, 5):
|
||||
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, shiny_perfect_count, perfect_count, near_count, miss_count 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)
|
||||
score_v2 = Score.calculate_score_v2(
|
||||
defnum, k[2], k[3], k[4], k[5])
|
||||
values.append((ptt, score_v2,))
|
||||
where_values.append((k[0], i[0], j))
|
||||
if values:
|
||||
Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [
|
||||
'user_id', 'song_id', 'difficulty'], where_values)
|
||||
|
||||
# 更新 recent30
|
||||
song_defum: 'dict[str, list[int]]' = {}
|
||||
for i in x:
|
||||
song_defum[i[0]] = []
|
||||
for j in range(0, 5):
|
||||
defnum = -10
|
||||
if i[j+1] is not None and i[j+1] > 0:
|
||||
defnum = float(i[j+1]) / 10
|
||||
song_defum[i[0]].append(defnum)
|
||||
|
||||
users = c.execute('''select user_id from user''').fetchall()
|
||||
for i in users:
|
||||
values = []
|
||||
where_values = []
|
||||
user_id = i[0]
|
||||
c.execute(
|
||||
'''select r_index, song_id, difficulty, score from recent30 where user_id = ?''', (user_id,))
|
||||
for j in c.fetchall():
|
||||
if j[1] in song_defum:
|
||||
defnum = song_defum[j[1]][j[2]]
|
||||
else:
|
||||
defnum = -10
|
||||
ptt = Score.calculate_rating(defnum, j[3])
|
||||
ptt = max(ptt, 0)
|
||||
|
||||
values.append((ptt,))
|
||||
where_values.append((user_id, j[0]))
|
||||
|
||||
if values:
|
||||
Sql(c).update_many('recent30', ['rating'], values, [
|
||||
'user_id', 'r_index'], where_values)
|
||||
|
||||
|
||||
class RefreshSongFileCache(BaseOperation):
|
||||
'''
|
||||
刷新歌曲文件缓存,包括文件hash缓存重建、文件目录重遍历、songlist重解析
|
||||
注意在设置里预先计算关闭的情况下,文件hash不会计算
|
||||
'''
|
||||
_name = 'refresh_song_file_cache'
|
||||
|
||||
def run(self):
|
||||
DownloadList.clear_all_cache()
|
||||
DownloadList.initialize_cache()
|
||||
|
||||
|
||||
class RefreshBundleCache(BaseOperation):
|
||||
'''
|
||||
刷新 bundle 缓存
|
||||
'''
|
||||
_name = 'refresh_content_bundle_cache'
|
||||
|
||||
def run(self):
|
||||
BundleParser().re_init()
|
||||
|
||||
|
||||
class RefreshWorldMapCache(BaseOperation):
|
||||
'''
|
||||
刷新 map 缓存
|
||||
'''
|
||||
_name = 'refresh_world_map_cache'
|
||||
|
||||
def run(self):
|
||||
MapParser().re_init()
|
||||
|
||||
|
||||
class SaveUpdateScore(BaseOperation):
|
||||
'''
|
||||
云存档更新成绩,是覆盖式更新
|
||||
提供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, rating_etr 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], i[5]]
|
||||
for i in x} # chart const * 10
|
||||
|
||||
new_scores = []
|
||||
for i in save.scores_data:
|
||||
rating = 0
|
||||
score_v2 = 0
|
||||
if i['song_id'] in song_chart_const:
|
||||
defnum = song_chart_const[i['song_id']
|
||||
][i['difficulty']] / 10
|
||||
rating = Score.calculate_rating(defnum, i['score'])
|
||||
rating = max(rating, 0)
|
||||
|
||||
score_v2 = Score.calculate_score_v2(
|
||||
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
|
||||
|
||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||
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, score_v2))
|
||||
|
||||
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, rating_etr from chart''')
|
||||
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4], i[5]]
|
||||
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
|
||||
score_v2 = 0
|
||||
if i['song_id'] in song_chart_const:
|
||||
defnum = song_chart_const[i['song_id']
|
||||
][i['difficulty']] / 10
|
||||
rating = Score.calculate_rating(defnum, i['score'])
|
||||
rating = max(rating, 0)
|
||||
|
||||
score_v2 = Score.calculate_score_v2(
|
||||
defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count'])
|
||||
|
||||
y = f'{i["song_id"]}{i["difficulty"]}'
|
||||
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, score_v2))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _delete_one_table(c, table_name, user_id):
|
||||
c.execute(
|
||||
f'''insert into db_deleted.{table_name} select * from {table_name} where user_id = ?''', (user_id,))
|
||||
c.execute(f'''delete from {table_name} where user_id = ?''', (user_id,))
|
||||
|
||||
|
||||
class DeleteUserScore(BaseOperation):
|
||||
'''
|
||||
删除单用户成绩,不包含 recent 数据
|
||||
'''
|
||||
_name = 'delete_user_score'
|
||||
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||
if user_id is not None:
|
||||
self.user = User()
|
||||
self.user.user_id = int(user_id)
|
||||
return self
|
||||
|
||||
def run(self):
|
||||
assert self.user is not None
|
||||
with Connect() as c:
|
||||
c.execute('''attach database ? as db_deleted''',
|
||||
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||
_delete_one_table(c, 'best_score', self.user.user_id)
|
||||
_delete_one_table(c, 'recent30', self.user.user_id)
|
||||
|
||||
|
||||
class DeleteOneUser(BaseOperation):
|
||||
'''
|
||||
删除单用户
|
||||
'''
|
||||
_name = 'delete_one_user'
|
||||
|
||||
TABLES = ['best_score', 'recent30', 'user_char', 'user_course', 'user_item',
|
||||
'user_present', 'user_redeem', 'user_role', 'user_save', 'user_world', 'user']
|
||||
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
def set_params(self, user_id: int = None, *args, **kwargs):
|
||||
if user_id is not None:
|
||||
self.user = User()
|
||||
self.user.user_id = int(user_id)
|
||||
return self
|
||||
|
||||
def run(self):
|
||||
assert self.user is not None
|
||||
with Connect() as c:
|
||||
c.execute('''attach database ? as db_deleted''',
|
||||
(Constant.SQLITE_DATABASE_DELETED_PATH,))
|
||||
|
||||
self._clear_login(c)
|
||||
self._data_save(c)
|
||||
|
||||
def _data_save(self, c):
|
||||
c.execute(
|
||||
f'''insert into db_deleted.friend select * from friend where user_id_me = ? or user_id_other = ?''', (self.user.user_id, self.user.user_id))
|
||||
c.execute(f'''delete from friend where user_id_me = ? or user_id_other = ?''',
|
||||
(self.user.user_id, self.user.user_id))
|
||||
|
||||
[_delete_one_table(c, x, self.user.user_id) for x in self.TABLES]
|
||||
|
||||
def _clear_login(self, c):
|
||||
c.execute('''delete from login where user_id = ?''',
|
||||
(self.user.user_id,))
|
||||
c.execute('''delete from api_login where user_id = ?''',
|
||||
(self.user.user_id,))
|
||||
196
latest version/core/present.py
Normal file
196
latest version/core/present.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from time import time
|
||||
|
||||
from .error import ArcError, NoData
|
||||
from .item import CollectionItemMixin, ItemFactory
|
||||
|
||||
|
||||
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:
|
||||
self.c = c
|
||||
self.present_id: str = None
|
||||
self.expire_ts: int = None
|
||||
self.description: str = None
|
||||
|
||||
self.items: list = None
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return self.expire_ts < int(time() * 1000)
|
||||
|
||||
def to_dict(self, has_items: bool = True) -> dict:
|
||||
r = {
|
||||
'present_id': self.present_id,
|
||||
'expire_ts': self.expire_ts,
|
||||
'description': self.description
|
||||
}
|
||||
if has_items:
|
||||
r['items'] = [x.to_dict() for x in self.items]
|
||||
return r
|
||||
|
||||
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查询信息
|
||||
'''
|
||||
if present_id:
|
||||
self.present_id = present_id
|
||||
|
||||
self.c.execute(
|
||||
'''select * from present where present_id=:a''', {'a': self.present_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The present `%s` does not exist.' % self.present_id)
|
||||
|
||||
self.from_list(x)
|
||||
return self
|
||||
|
||||
def select_items(self) -> None:
|
||||
'''
|
||||
查询奖励的物品
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select * from present_item where present_id=:a''', {'a': self.present_id})
|
||||
self.items = [ItemFactory.from_dict({
|
||||
'item_id': i[1],
|
||||
'type': i[2],
|
||||
'amount': i[3] if i[3] else 1
|
||||
}, 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):
|
||||
'''
|
||||
用户登录奖励类
|
||||
忽视了description的多语言
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
def delete_user_present(self) -> None:
|
||||
'''
|
||||
删除用户奖励
|
||||
'''
|
||||
self.c.execute('''delete from user_present where user_id=:a and present_id=:b''',
|
||||
{'a': self.user.user_id, 'b': self.present_id})
|
||||
|
||||
def claim_user_present(self, present_id: str = None, user=None) -> None:
|
||||
'''
|
||||
确认并删除用户奖励
|
||||
'''
|
||||
if present_id:
|
||||
self.present_id = present_id
|
||||
if user:
|
||||
self.user = user
|
||||
if self.expire_ts is None:
|
||||
self.select()
|
||||
if self.items is None:
|
||||
self.select_items()
|
||||
|
||||
self.c.execute('''select exists(select * from user_present where user_id=:a and present_id=:b)''',
|
||||
{'a': self.user.user_id, 'b': self.present_id})
|
||||
|
||||
if self.c.fetchone() == (0,):
|
||||
raise NoData('The present `%s` for the user `%s` does not exist.' % (
|
||||
self.present_id, self.user.user_id))
|
||||
|
||||
self.delete_user_present()
|
||||
|
||||
if self.is_expired:
|
||||
raise ArcError('The present `%s` has expired.' % self.present_id)
|
||||
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
|
||||
|
||||
class UserPresentList:
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.presents: list = None
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.presents]
|
||||
|
||||
def select_user_presents(self) -> None:
|
||||
'''
|
||||
查询用户全部奖励
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select * from present where present_id in (select present_id from user_present where user_id=:a)''', {'a': self.user.user_id})
|
||||
x = self.c.fetchall()
|
||||
self.presents = []
|
||||
if not x:
|
||||
return None
|
||||
|
||||
for i in x:
|
||||
p = UserPresent(self.c, self.user)
|
||||
p.present_id = i[0]
|
||||
p.expire_ts = i[1]
|
||||
p.description = i[2]
|
||||
if not p.is_expired:
|
||||
p.select_items()
|
||||
self.presents.append(p)
|
||||
254
latest version/core/purchase.py
Normal file
254
latest version/core/purchase.py
Normal file
@@ -0,0 +1,254 @@
|
||||
from time import time
|
||||
|
||||
from .error import InputError, NoData, TicketNotEnough
|
||||
from .item import CollectionItemMixin, ItemFactory
|
||||
|
||||
|
||||
class Purchase(CollectionItemMixin):
|
||||
'''
|
||||
购买类
|
||||
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):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.purchase_name: str = None
|
||||
self.price: int = None
|
||||
self.orig_price: int = None
|
||||
self.discount_from: int = None
|
||||
self.discount_to: int = None
|
||||
self.discount_reason: str = None
|
||||
|
||||
self.items: list = []
|
||||
|
||||
# TODO: "discount_reason": extend, sale
|
||||
|
||||
@property
|
||||
def price_displayed(self) -> int:
|
||||
'''
|
||||
返回显示的价格
|
||||
'''
|
||||
if self.discount_from > 0 and self.discount_to > 0:
|
||||
if self.discount_from <= int(time() * 1000) <= self.discount_to:
|
||||
if self.discount_reason == 'anni5tix':
|
||||
x = ItemFactory(self.c).get_item('anni5tix')
|
||||
x.item_id = 'anni5tix'
|
||||
x.select_user_item(self.user)
|
||||
if x.amount >= 1:
|
||||
return 0
|
||||
elif self.discount_reason == 'pick_ticket':
|
||||
x = ItemFactory(self.c).get_item('pick_ticket')
|
||||
x.item_id = 'pick_ticket'
|
||||
x.select_user_item(self.user)
|
||||
if x.amount >= 1:
|
||||
return 0
|
||||
return self.price
|
||||
return self.orig_price
|
||||
|
||||
def to_dict(self, has_items: bool = True, show_real_price: bool = True) -> dict:
|
||||
if show_real_price:
|
||||
price = self.price_displayed
|
||||
r = {
|
||||
'name': self.purchase_name,
|
||||
'price': price if show_real_price else self.price,
|
||||
'orig_price': self.orig_price,
|
||||
}
|
||||
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:
|
||||
r['discount_from'] = self.discount_from
|
||||
r['discount_to'] = self.discount_to
|
||||
if not show_real_price or (self.discount_reason in ('anni5tix', 'pick_ticket') and price == 0):
|
||||
r['discount_reason'] = self.discount_reason
|
||||
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':
|
||||
'''
|
||||
用purchase_name查询信息
|
||||
'''
|
||||
if purchase_name:
|
||||
self.purchase_name = purchase_name
|
||||
|
||||
self.c.execute(
|
||||
'''select * from purchase where purchase_name=:name''', {'name': self.purchase_name})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData(
|
||||
f'Purchase `{self.purchase_name}` does not exist.', 501)
|
||||
|
||||
self.price = x[1]
|
||||
self.orig_price = x[2]
|
||||
self.discount_from = x[3] if x[3] else -1
|
||||
self.discount_to = x[4] if x[4] else -1
|
||||
self.discount_reason = x[5] if x[5] else ''
|
||||
self.select_items()
|
||||
return self
|
||||
|
||||
def select_items(self) -> None:
|
||||
'''从数据库拉取purchase_item数据'''
|
||||
self.c.execute(
|
||||
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
|
||||
x = self.c.fetchall()
|
||||
# if not x:
|
||||
# raise NoData(
|
||||
# f'The items of the purchase `{self.purchase_name}` does not exist.', 501)
|
||||
|
||||
self.items = []
|
||||
t = None
|
||||
|
||||
for i in x:
|
||||
if i[0] == self.purchase_name:
|
||||
# 物品排序,否则客户端报错
|
||||
t = ItemFactory.from_dict({
|
||||
'item_id': i[0],
|
||||
'type': i[1],
|
||||
'amount': i[2] if i[2] else 1
|
||||
}, self.c)
|
||||
else:
|
||||
self.items.append(ItemFactory.from_dict({
|
||||
'item_id': i[0],
|
||||
'type': i[1],
|
||||
'amount': i[2] if i[2] else 1
|
||||
}, self.c))
|
||||
if t is not None:
|
||||
self.items = [t] + self.items
|
||||
|
||||
def buy(self) -> None:
|
||||
'''进行购买'''
|
||||
if self.price is None or self.orig_price is None:
|
||||
self.select()
|
||||
if not self.items:
|
||||
self.select_items()
|
||||
self.user.select_user_one_column('ticket', 0)
|
||||
price_used = self.price_displayed
|
||||
|
||||
if self.user.ticket < price_used:
|
||||
raise TicketNotEnough(
|
||||
'The user does not have enough memories.', -6)
|
||||
|
||||
if not (self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to):
|
||||
if price_used == 0:
|
||||
x = ItemFactory(self.c).get_item(self.discount_reason)
|
||||
x.item_id = self.discount_reason
|
||||
x.amount = -1
|
||||
x.user_claim_item(self.user)
|
||||
else:
|
||||
self.user.ticket -= price_used
|
||||
self.user.update_user_one_column('ticket')
|
||||
|
||||
for i in self.items:
|
||||
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:
|
||||
'''
|
||||
购买列表类
|
||||
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.purchases: list = []
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.purchases]
|
||||
|
||||
def select_from_type(self, item_type: str) -> 'PurchaseList':
|
||||
self.c.execute('''select purchase_name from purchase_item where type = :a''', {
|
||||
'a': item_type})
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return self
|
||||
|
||||
self.purchases: list = []
|
||||
for i in x:
|
||||
self.purchases.append(Purchase(self.c, self.user).select(i[0]))
|
||||
return self
|
||||
159
latest version/core/rank.py
Normal file
159
latest version/core/rank.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from .constant import Constant
|
||||
from .score import UserScore
|
||||
from .song import Chart
|
||||
from .sql import Query, Sql
|
||||
from .user import UserInfo
|
||||
|
||||
|
||||
class RankList:
|
||||
'''
|
||||
排行榜类
|
||||
默认limit=20,limit<0认为是all
|
||||
|
||||
property: `user` - `User`类或者子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.list: list = []
|
||||
self.song = Chart()
|
||||
self.limit: int = 20
|
||||
self.user = None
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.list]
|
||||
|
||||
def select_top(self) -> None:
|
||||
'''
|
||||
得到top分数表
|
||||
'''
|
||||
if self.limit >= 0:
|
||||
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})
|
||||
else:
|
||||
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})
|
||||
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
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
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None:
|
||||
'''
|
||||
得到用户好友分数表
|
||||
'''
|
||||
self.limit = limit
|
||||
if user:
|
||||
self.user = user
|
||||
|
||||
user_ids = [self.user.user_id] + [x[0] for x in self.user.friend_ids]
|
||||
|
||||
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()
|
||||
if not x:
|
||||
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
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
@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):
|
||||
'''
|
||||
计算我的排名中的查询参数
|
||||
|
||||
returns:
|
||||
`sql_limit`: int - 查询limit参数
|
||||
`sql_offset`: int - 查询offset参数
|
||||
`need_myself`: bool - 是否需要在排名结尾添加自己
|
||||
'''
|
||||
sql_limit = all_limit
|
||||
sql_offset = 0
|
||||
need_myself = False
|
||||
|
||||
if my_rank <= max_local_position: # 排名在前面,前方人数不足
|
||||
pass
|
||||
elif my_rank > max_global_position: # 排名太后了,不显示排名
|
||||
sql_limit -= 1
|
||||
sql_offset = max_global_position - all_limit + 1
|
||||
need_myself = True
|
||||
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
|
||||
sql_offset = amount - all_limit
|
||||
elif max_local_position <= my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
|
||||
sql_offset = my_rank - max_local_position
|
||||
else: # 我已经忘了这是什么了
|
||||
sql_offset = max_global_position - all_limit
|
||||
|
||||
return sql_limit, sql_offset, need_myself
|
||||
|
||||
def select_me(self, user=None) -> None:
|
||||
'''
|
||||
得到我的排名分数表
|
||||
'''
|
||||
if user:
|
||||
self.user = user
|
||||
self.c.execute('''select score, time_played from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty''', {
|
||||
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
return None
|
||||
|
||||
self.c.execute('''select count(*) from best_score where song_id = :song_id and difficulty = :difficulty and ( score > :score or (score = :score and time_played > :time_played) )''', {
|
||||
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'score': x[0], 'time_played': x[1]})
|
||||
my_rank = int(self.c.fetchone()[0]) + 1
|
||||
self.c.execute('''select count(*) from best_score where song_id=:a and difficulty=:b''',
|
||||
{'a': self.song.song_id, 'b': self.song.difficulty})
|
||||
|
||||
sql_limit, sql_offset, need_myself = self.get_my_rank_parameter(
|
||||
my_rank, int(self.c.fetchone()[0]), self.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 offset :offset''', {
|
||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': sql_limit, 'offset': sql_offset})
|
||||
x = self.c.fetchall()
|
||||
|
||||
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
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
if need_myself:
|
||||
y = UserScore(self.c, UserInfo(self.c, self.user.user_id))
|
||||
y.song = self.song
|
||||
y.select_score()
|
||||
y.rank = -1
|
||||
self.list.append(y)
|
||||
155
latest version/core/redeem.py
Normal file
155
latest version/core/redeem.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from .error import NoData, RedeemUnavailable
|
||||
from .item import CollectionItemMixin, ItemFactory
|
||||
|
||||
|
||||
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:
|
||||
self.c = c
|
||||
self.code: str = None
|
||||
self.redeem_type: int = None
|
||||
|
||||
self.items: list = []
|
||||
self.fragment: int = 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:
|
||||
self.code = code
|
||||
self.c.execute('''select * from redeem where code=:a''',
|
||||
{'a': self.code})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(f'The redeem `{self.code}` does not exist.', 504)
|
||||
|
||||
self.redeem_type = x[1]
|
||||
return self
|
||||
|
||||
def select_items(self) -> None:
|
||||
self.c.execute('''select * from redeem_item where code=:a''',
|
||||
{'a': self.code})
|
||||
self.items = [ItemFactory.from_dict({
|
||||
'item_id': i[1],
|
||||
'type': i[2],
|
||||
'amount': i[3] if i[3] else 1
|
||||
}, 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):
|
||||
'''
|
||||
用户兑换码类
|
||||
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
if self.redeem_type is None:
|
||||
self.select()
|
||||
|
||||
if self.redeem_type == 0:
|
||||
# 一次性
|
||||
self.c.execute(
|
||||
'''select exists(select * from user_redeem where code=:a)''', {'a': self.code})
|
||||
if self.c.fetchone() == (1,):
|
||||
return False
|
||||
elif self.redeem_type == 1:
|
||||
# 每个玩家一次
|
||||
self.c.execute('''select exists(select * from user_redeem where code=:a and user_id=:b)''',
|
||||
{'a': self.code, 'b': self.user.user_id})
|
||||
if self.c.fetchone() == (1,):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def insert_user_redeem(self) -> None:
|
||||
self.c.execute('''insert into user_redeem values(:b,:a)''',
|
||||
{'a': self.code, 'b': self.user.user_id})
|
||||
|
||||
def claim_user_redeem(self, code: str = None) -> None:
|
||||
if code:
|
||||
self.code = code
|
||||
if not self.is_available:
|
||||
if self.redeem_type == 0:
|
||||
raise RedeemUnavailable(
|
||||
'The redeem `%s` is unavailable.' % self.code)
|
||||
if self.redeem_type == 1:
|
||||
raise RedeemUnavailable(
|
||||
'The redeem `%s` is unavailable.' % self.code, 506)
|
||||
|
||||
if not self.items:
|
||||
self.select_items()
|
||||
|
||||
self.insert_user_redeem()
|
||||
|
||||
self.fragment = 0
|
||||
for i in self.items:
|
||||
if i.item_type == 'fragment':
|
||||
self.fragment += i.amount
|
||||
else:
|
||||
i.user_claim_item(self.user)
|
||||
142
latest version/core/save.py
Normal file
142
latest version/core/save.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import json
|
||||
from time import time
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import InputError, NoData
|
||||
from .util import md5
|
||||
|
||||
|
||||
class SaveData:
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.user = None
|
||||
|
||||
self.scores_data = []
|
||||
self.clearlamps_data = []
|
||||
self.clearedsongs_data = []
|
||||
self.unlocklist_data = []
|
||||
self.installid_data: str = ''
|
||||
self.devicemodelname_data: str = ''
|
||||
self.story_data = []
|
||||
self.createdAt: int = 0
|
||||
self.finalestate_data: str = ''
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"user_id": self.user.user_id,
|
||||
"story": {
|
||||
"": self.story_data
|
||||
},
|
||||
"devicemodelname": {
|
||||
"val": self.devicemodelname_data
|
||||
},
|
||||
"installid": {
|
||||
"val": self.installid_data
|
||||
},
|
||||
"unlocklist": {
|
||||
"": self.unlocklist_data
|
||||
},
|
||||
"clearedsongs": {
|
||||
"": self.clearedsongs_data
|
||||
},
|
||||
"clearlamps": {
|
||||
"": self.clearlamps_data
|
||||
},
|
||||
"scores": {
|
||||
"": self.scores_data
|
||||
},
|
||||
"version": {
|
||||
"val": 1
|
||||
},
|
||||
"createdAt": self.createdAt,
|
||||
'finalestate': {
|
||||
'val': self.finalestate_data
|
||||
}
|
||||
}
|
||||
|
||||
def select_scores(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.user = user
|
||||
self.c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''',
|
||||
{'a': user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData(f'User `{user.user_id}` has no cloud save data')
|
||||
|
||||
self.scores_data: list = json.loads(x[0])[""]
|
||||
self.clearlamps_data: list = json.loads(x[1])[""]
|
||||
|
||||
def select_all(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.user = user
|
||||
self.c.execute('''select * from user_save where user_id=:a''',
|
||||
{'a': user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.scores_data = json.loads(x[1])[""]
|
||||
self.clearlamps_data = json.loads(x[2])[""]
|
||||
self.clearedsongs_data = json.loads(x[3])[""]
|
||||
self.unlocklist_data = json.loads(x[4])[""]
|
||||
self.installid_data = json.loads(x[5])["val"]
|
||||
self.devicemodelname_data = json.loads(x[6])["val"]
|
||||
self.story_data = json.loads(x[7])[""]
|
||||
if x[8] is not None:
|
||||
self.createdAt = int(x[8])
|
||||
self.finalestate_data = x[9] if x[9] is not None else ''
|
||||
|
||||
if Config.SAVE_FULL_UNLOCK:
|
||||
self.installid_data = "0fcec8ed-7b62-48e2-9d61-55041a22b123" # 使得可以进入存档选择上传或下载界面
|
||||
for i in self.story_data:
|
||||
i['c'] = True
|
||||
i['r'] = True
|
||||
for i in self.unlocklist_data:
|
||||
x = i['unlock_key']
|
||||
if x[-3:] == '101':
|
||||
i['complete'] = 100
|
||||
elif x[:16] == 'aegleseeker|2|3|':
|
||||
i['complete'] = 10
|
||||
elif x == 'saikyostronger|2|3|einherjar|2':
|
||||
i['complete'] = 6
|
||||
elif x == 'saikyostronger|2|3|laqryma|2':
|
||||
i['complete'] = 3
|
||||
elif x[-5:-2] == '109':
|
||||
i['complete'] = 3
|
||||
elif x.endswith('112'):
|
||||
i['complete'] = 999
|
||||
else:
|
||||
i['complete'] = 1
|
||||
|
||||
self.finalestate_data = '|'.join(
|
||||
['0', '100'] + [str(x[0]) if i in [64, 65, 66, 71] else str(x[1]) for i, x in enumerate(Constant.FINALE_SWITCH)] + ['1337'])
|
||||
|
||||
def update_all(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.createdAt = int(time() * 1000)
|
||||
self.c.execute('''insert or replace 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})
|
||||
|
||||
def set_value(self, key: str, value: str, checksum: str) -> None:
|
||||
'''
|
||||
从Arcaea客户端给的奇怪字符串中获取存档数据,并进行数据校验
|
||||
'''
|
||||
if not value:
|
||||
return None
|
||||
if key not in self.__dict__:
|
||||
raise KeyError(
|
||||
'Property `%s` is not found in the instance of `SaveData` class.' % key)
|
||||
|
||||
if md5(value) == checksum:
|
||||
if key in ('installid_data', 'devicemodelname_data', 'finalestate_data'):
|
||||
self.__dict__[key] = json.loads(value)['val']
|
||||
else:
|
||||
self.__dict__[key] = json.loads(value)['']
|
||||
else:
|
||||
raise InputError('Hash value of cloud save data mismatches.')
|
||||
718
latest version/core/score.py
Normal file
718
latest version/core/score.py
Normal file
@@ -0,0 +1,718 @@
|
||||
from base64 import b64encode
|
||||
from os import urandom
|
||||
from random import choices
|
||||
from time import time
|
||||
|
||||
from .bgtask import BGTask, logdb_execute
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .course import CoursePlay
|
||||
from .error import NoData, StaminaNotEnough
|
||||
from .item import ItemCore
|
||||
from .song import Chart
|
||||
from .sql import Connect, Query, Sql
|
||||
from .util import get_today_timestamp, md5
|
||||
from .world import BeyondWorldPlay, BreachedWorldPlay, WorldPlay
|
||||
|
||||
|
||||
class Score:
|
||||
def __init__(self) -> None:
|
||||
self.c = None
|
||||
|
||||
self.song: 'Chart' = Chart()
|
||||
self.score: int = None
|
||||
self.shiny_perfect_count: int = None
|
||||
self.perfect_count: int = None
|
||||
self.near_count: int = None
|
||||
self.miss_count: int = None
|
||||
self.health: int = None
|
||||
self.modifier: int = None
|
||||
self.time_played: int = None
|
||||
self.best_clear_type: int = None
|
||||
self.clear_type: int = None
|
||||
self.rating: float = None
|
||||
self.score_v2: float = None # for `world_rank_score` of global rank
|
||||
|
||||
def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int):
|
||||
self.score = int(score) if score is not None else 0
|
||||
self.shiny_perfect_count = int(
|
||||
shiny_perfect_count) if shiny_perfect_count is not None else 0
|
||||
self.perfect_count = int(
|
||||
perfect_count) if perfect_count is not None else 0
|
||||
self.near_count = int(near_count) if near_count is not None else 0
|
||||
self.miss_count = int(miss_count) if miss_count is not None else 0
|
||||
self.health = int(health) if health is not None else 0
|
||||
self.modifier = int(modifier) if modifier is not None else 0
|
||||
self.time_played = int(time_played) if time_played is not None else 0
|
||||
self.clear_type = int(clear_type) if clear_type is not None else 0
|
||||
|
||||
@staticmethod
|
||||
def get_song_grade(score: int) -> int:
|
||||
'''分数转换为评级'''
|
||||
if score >= 9900000: # EX+
|
||||
return 6
|
||||
if score >= 9800000: # EX
|
||||
return 5
|
||||
if score >= 9500000: # AA
|
||||
return 4
|
||||
if score >= 9200000: # A
|
||||
return 3
|
||||
if score >= 8900000: # B
|
||||
return 2
|
||||
if score >= 8600000: # C
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@property
|
||||
def song_grade(self) -> int:
|
||||
return self.get_song_grade(self.score)
|
||||
|
||||
@staticmethod
|
||||
def get_song_state(clear_type: int) -> int:
|
||||
'''clear_type转换为成绩状态,用数字大小标识便于比较'''
|
||||
if clear_type == 3: # PM
|
||||
return 5
|
||||
if clear_type == 2: # FC
|
||||
return 4
|
||||
if clear_type == 5: # Hard Clear
|
||||
return 3
|
||||
if clear_type == 1: # Clear
|
||||
return 2
|
||||
if clear_type == 4: # Easy Clear
|
||||
return 1
|
||||
return 0 # Track Lost
|
||||
|
||||
@property
|
||||
def song_state(self) -> int:
|
||||
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
|
||||
def is_valid(self) -> bool:
|
||||
'''分数有效性检查'''
|
||||
if self.shiny_perfect_count < 0 or self.perfect_count < 0 or self.near_count < 0 or self.miss_count < 0 or self.score < 0 or self.time_played <= 0:
|
||||
return False
|
||||
if self.song.difficulty not in (0, 1, 2, 3, 4):
|
||||
return False
|
||||
|
||||
all_note = self.all_note_count
|
||||
if all_note == 0:
|
||||
return False
|
||||
|
||||
calc_score = 10000000 / all_note * \
|
||||
(self.perfect_count + self.near_count/2) + self.shiny_perfect_count
|
||||
if abs(calc_score - self.score) >= 5:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def calculate_rating(defnum: float, score: int) -> float:
|
||||
'''计算rating,谱面定数小于等于0视为Unrank,返回值会为-1,这里的defnum = Chart const'''
|
||||
if not defnum or defnum <= 0:
|
||||
# 谱面没定数或者定数小于等于0被视作Unrank
|
||||
return -1
|
||||
|
||||
if score >= 10000000:
|
||||
ptt = defnum + 2
|
||||
elif score < 9800000:
|
||||
ptt = defnum + (score-9500000) / 300000
|
||||
ptt = max(ptt, 0)
|
||||
else:
|
||||
ptt = defnum + 1 + (score-9800000) / 200000
|
||||
|
||||
return ptt
|
||||
|
||||
@staticmethod
|
||||
def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int) -> float:
|
||||
# 计算score_v2 refer: https://www.bilibili.com/video/BV1ys421u7BY
|
||||
# 谱面定数小于等于 0 视为 unranked,返回值会为 0
|
||||
if not defnum or defnum <= 0:
|
||||
return 0
|
||||
|
||||
all_note = perfect_count + near_count + miss_count
|
||||
if all_note == 0:
|
||||
return 0
|
||||
shiny_ratio = shiny_perfect_count / all_note
|
||||
score_ratio = (perfect_count + near_count/2) / \
|
||||
all_note + shiny_perfect_count / 10000000
|
||||
acc_rating = max(0, min(shiny_ratio - 0.9, 0.095)) / 9.5 * 25
|
||||
score_rating = max(0, min(score_ratio - 0.99, 0.01)) * 75
|
||||
return defnum * (acc_rating + score_rating)
|
||||
|
||||
def get_rating_by_calc(self) -> float:
|
||||
# 通过计算得到本成绩的 rating & score_v2
|
||||
if not self.song.defnum:
|
||||
self.song.c = self.c
|
||||
self.song.select()
|
||||
self.rating = self.calculate_rating(self.song.chart_const, self.score)
|
||||
self.score_v2 = self.calculate_score_v2(
|
||||
self.song.chart_const, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count)
|
||||
return self.rating
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = {
|
||||
"rating": self.rating,
|
||||
"modifier": self.modifier,
|
||||
"time_played": self.time_played,
|
||||
"health": self.health,
|
||||
"clear_type": self.clear_type,
|
||||
"miss_count": self.miss_count,
|
||||
"near_count": self.near_count,
|
||||
"perfect_count": self.perfect_count,
|
||||
"shiny_perfect_count": self.shiny_perfect_count,
|
||||
"score": self.score,
|
||||
"difficulty": self.song.difficulty,
|
||||
"song_id": self.song.song_id
|
||||
}
|
||||
if self.song.song_name is not None:
|
||||
r["song_name"] = self.song.song_name
|
||||
return r
|
||||
|
||||
|
||||
class UserScore(Score):
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
'''
|
||||
parameter: `user` - `UserInfo`类或子类的实例
|
||||
'''
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.rank = None # 成绩排名,给Ranklist用的
|
||||
|
||||
def select_score(self) -> None:
|
||||
'''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询'''
|
||||
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})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('No score data.')
|
||||
self.user.select_user_about_character()
|
||||
|
||||
self.from_list(x)
|
||||
|
||||
def from_list(self, x: list) -> 'UserScore':
|
||||
if self.song.song_id is None:
|
||||
self.song.song_id = x[1]
|
||||
if self.song.difficulty is None:
|
||||
self.song.difficulty = x[2]
|
||||
self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12])
|
||||
self.best_clear_type = int(x[11])
|
||||
self.rating = float(x[13])
|
||||
self.score_v2 = float(x[14])
|
||||
|
||||
return self
|
||||
|
||||
def to_dict(self, has_user_info: bool = True) -> dict:
|
||||
r = super().to_dict()
|
||||
r['best_clear_type'] = self.best_clear_type
|
||||
if has_user_info:
|
||||
r['user_id'] = self.user.user_id
|
||||
r['name'] = self.user.name
|
||||
r['is_skill_sealed'] = self.user.is_skill_sealed
|
||||
character = self.user.character_displayed
|
||||
r['is_char_uncapped'] = character.is_uncapped_displayed
|
||||
r['character'] = character.character_id
|
||||
if self.rank:
|
||||
r['rank'] = self.rank
|
||||
return r
|
||||
|
||||
|
||||
class UserPlay(UserScore):
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c, user)
|
||||
self.song_token: str = None
|
||||
self.song_hash: str = None
|
||||
self.submission_hash: str = None
|
||||
self.beyond_gauge: int = None
|
||||
self.unrank_flag: bool = None
|
||||
self.new_best_protect_flag: bool = None
|
||||
self.ptt: 'Potential' = None
|
||||
|
||||
self.is_world_mode: bool = None
|
||||
self.stamina_multiply: int = None
|
||||
self.fragment_multiply: int = None
|
||||
self.prog_boost_multiply: int = None
|
||||
self.beyond_boost_gauge_usage: int = None
|
||||
|
||||
self.ptt: Potential = None # 临时用来计算用户ptt的
|
||||
self.world_play: 'WorldPlay' = None
|
||||
|
||||
self.course_play_state: int = None
|
||||
self.course_play: 'CoursePlay' = None
|
||||
|
||||
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.hp_interval_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.fever_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.rank_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.maya_gauge: int = None # 不能给 None 以外的默认值
|
||||
self.nextstage_bonus: int = None # 不能给 None 以外的默认值
|
||||
self.skill_cytusii_flag: str = None
|
||||
self.skill_chinatsu_flag: str = None
|
||||
self.highest_health: int = None
|
||||
self.lowest_health: int = None
|
||||
|
||||
# room score
|
||||
self.room_code: str = None
|
||||
self.room_total_score: int = None
|
||||
self.room_total_players: int = None
|
||||
|
||||
self.invasion_flag: int = None # 1: invasion_start, 2: invasion_hard
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
# 不能super
|
||||
if self.is_world_mode is None or self.course_play_state is None:
|
||||
return {}
|
||||
if self.course_play_state == 4:
|
||||
r = self.course_play.to_dict()
|
||||
elif self.is_world_mode:
|
||||
r = self.world_play.to_dict()
|
||||
else:
|
||||
r = {}
|
||||
r['user_rating'] = self.user.rating_ptt
|
||||
r['finale_challenge_higher'] = self.rating > self.ptt.value
|
||||
r['global_rank'] = self.user.global_rank
|
||||
r['finale_play_value'] = 9.065 * self.rating ** 0.5 # by Lost-MSth
|
||||
return r
|
||||
|
||||
@property
|
||||
def is_protected(self) -> bool:
|
||||
return self.health == -1 or int(self.score) >= 9800000 or self.new_best_protect_flag
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
'''分数有效性检查,带hash校验'''
|
||||
if not super().is_valid:
|
||||
return False
|
||||
|
||||
# 歌曲谱面MD5检查,服务器没有谱面就不管了
|
||||
from .download import get_song_file_md5
|
||||
songfile_hash = get_song_file_md5(
|
||||
self.song.song_id, str(self.song.difficulty) + '.aff')
|
||||
if songfile_hash and songfile_hash != self.song_hash:
|
||||
return False
|
||||
|
||||
x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{
|
||||
self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
|
||||
if self.combo_interval_bonus is not None:
|
||||
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)
|
||||
|
||||
if self.hp_interval_bonus is not None and self.hp_interval_bonus < 0:
|
||||
return False
|
||||
|
||||
if self.fever_bonus is not None and (self.fever_bonus < 0 or self.fever_bonus > self.perfect_count * 5):
|
||||
# fever 等级最高为 5
|
||||
return False
|
||||
|
||||
if self.rank_bonus is not None and (self.rank_bonus < 0 or self.rank_bonus > 4):
|
||||
return False
|
||||
|
||||
y = f'{self.user.user_id}{self.song_hash}'
|
||||
checksum = md5(x+md5(y))
|
||||
|
||||
if checksum != self.submission_hash:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_play_state(self) -> None:
|
||||
'''检查token,当然这里不管有没有,是用来判断世界模式和课题模式的'''
|
||||
if self.song_token == '1145141919810':
|
||||
# 硬编码检查,绕过数据库
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = -1
|
||||
return None
|
||||
|
||||
self.c.execute(
|
||||
'''select * from songplay_token where token=:a ''', {'a': self.song_token})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = -1
|
||||
return None
|
||||
# raise NoData('No token data.')
|
||||
# self.song.set_chart(x[2], x[3])
|
||||
if x[4]:
|
||||
self.course_play = CoursePlay(self.c, self.user, self)
|
||||
self.course_play.course_id = x[4]
|
||||
self.course_play.score = x[6]
|
||||
self.course_play.clear_type = x[7]
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = x[5]
|
||||
else:
|
||||
self.stamina_multiply = int(x[8])
|
||||
self.fragment_multiply = int(x[9])
|
||||
self.prog_boost_multiply = int(x[10])
|
||||
self.beyond_boost_gauge_usage = int(x[11])
|
||||
self.skill_cytusii_flag = x[12]
|
||||
self.skill_chinatsu_flag = x[13]
|
||||
self.invasion_flag = x[14]
|
||||
self.is_world_mode = True
|
||||
self.course_play_state = -1
|
||||
|
||||
def set_play_state_for_world(
|
||||
self,
|
||||
stamina_multiply: int = 1,
|
||||
fragment_multiply: int = 100,
|
||||
prog_boost_multiply: int = 0,
|
||||
beyond_boost_gauge_usage: int = 0,
|
||||
skill_cytusii_flag: str = None,
|
||||
skill_chinatsu_flag: str = None
|
||||
) -> None:
|
||||
self.song_token = b64encode(urandom(64)).decode()
|
||||
self.stamina_multiply = int(stamina_multiply)
|
||||
self.fragment_multiply = int(fragment_multiply)
|
||||
self.prog_boost_multiply = int(prog_boost_multiply)
|
||||
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
|
||||
self.skill_cytusii_flag = skill_cytusii_flag
|
||||
self.skill_chinatsu_flag = skill_chinatsu_flag
|
||||
if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
|
||||
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
|
||||
'a': self.user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
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.user.select_user_about_current_map()
|
||||
self.user.current_map.select_map_info()
|
||||
|
||||
self.user.select_user_about_stamina()
|
||||
if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
|
||||
raise StaminaNotEnough('Stamina is not enough.')
|
||||
|
||||
fatalis_stamina_multiply = 1
|
||||
self.user.select_user_about_character()
|
||||
if not self.user.is_skill_sealed:
|
||||
self.user.character.select_character_info()
|
||||
# invasion 扔骰子
|
||||
_flag = choices([0, 1, 2], [
|
||||
max(1 - Constant.INVASION_START_WEIGHT - Constant.INVASION_HARD_WEIGHT, 0), Constant.INVASION_START_WEIGHT, Constant.INVASION_HARD_WEIGHT])[0]
|
||||
if self.user.is_insight_enabled and _flag != 0:
|
||||
self.invasion_flag = _flag
|
||||
|
||||
elif self.user.character.skill_id_displayed == 'skill_fatalis':
|
||||
# 特殊判断hikari fatalis的双倍体力消耗
|
||||
fatalis_stamina_multiply = 2
|
||||
|
||||
self.clear_play_state()
|
||||
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h,:i,:j)''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 'i': self.skill_chinatsu_flag, 'j': self.invasion_flag, 't': self.song_token})
|
||||
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
||||
self.stamina_multiply * fatalis_stamina_multiply
|
||||
self.user.stamina.update()
|
||||
|
||||
def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
|
||||
'''课题模式打歌初始化'''
|
||||
self.song_token = 'c_' + b64encode(urandom(64)).decode()
|
||||
if course_id is not None:
|
||||
self.course_play.course_id = course_id
|
||||
|
||||
self.course_play_state = 0
|
||||
self.course_play.score = 0
|
||||
self.course_play.clear_type = 3 # 设置为PM,即最大值
|
||||
|
||||
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0,"","",0)''', (self.song_token, self.user.user_id, self.song.song_id,
|
||||
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
||||
self.user.select_user_about_stamina()
|
||||
if use_course_skip_purchase:
|
||||
x = ItemCore(self.c)
|
||||
x.item_id = 'core_course_skip_purchase'
|
||||
x.amount = -1
|
||||
x.user_claim_item(self.user)
|
||||
else:
|
||||
if self.user.stamina.stamina < Constant.COURSE_STAMINA_COST:
|
||||
raise StaminaNotEnough('Stamina is not enough.')
|
||||
self.user.stamina.stamina -= Constant.COURSE_STAMINA_COST
|
||||
self.user.stamina.update()
|
||||
|
||||
def update_token_for_course(self) -> None:
|
||||
'''课题模式更新token,并查用户体力'''
|
||||
previous_token = self.song_token
|
||||
self.song_token = 'c_' + b64encode(urandom(64)).decode()
|
||||
self.c.execute('''update songplay_token set token=? where token=?''',
|
||||
(self.song_token, previous_token))
|
||||
self.user.select_user_about_stamina()
|
||||
|
||||
def update_play_state_for_course(self) -> None:
|
||||
self.c.execute('''update songplay_token set course_state=?, course_score=?, course_clear_type=? where token=?''',
|
||||
(self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token))
|
||||
|
||||
def clear_play_state(self) -> None:
|
||||
self.c.execute('''delete from songplay_token where user_id=:a''', {
|
||||
'a': self.user.user_id})
|
||||
|
||||
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:
|
||||
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
||||
self.get_play_state()
|
||||
self.get_rating_by_calc()
|
||||
if self.rating < 0:
|
||||
self.unrank_flag = True
|
||||
self.rating = 0
|
||||
else:
|
||||
self.unrank_flag = False
|
||||
|
||||
self.time_played = int(time())
|
||||
|
||||
# 记录分数
|
||||
self.record_score()
|
||||
|
||||
# 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''', {
|
||||
'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})
|
||||
|
||||
# 成绩录入
|
||||
self.c.execute('''select score, best_clear_type 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})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.new_best_protect_flag = True # 初见保护
|
||||
self.c.execute('''insert into best_score values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(self.user.user_id, self.song.song_id, self.song.difficulty, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count,
|
||||
self.health, self.modifier, self.time_played, self.clear_type, self.clear_type, self.rating, self.score_v2))
|
||||
self.user.update_global_rank()
|
||||
else:
|
||||
self.new_best_protect_flag = False
|
||||
if self.song_state > self.get_song_state(int(x[1])): # best状态更新
|
||||
self.c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', {
|
||||
'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty})
|
||||
if self.score >= int(x[0]): # best成绩更新
|
||||
self.new_best_protect_flag = True
|
||||
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m, score_v2 = :n where user_id = :a and song_id = :b and difficulty = :c ''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played, 'n': self.score_v2})
|
||||
self.user.update_global_rank()
|
||||
|
||||
self.ptt = Potential(self.c, self.user)
|
||||
if not self.unrank_flag:
|
||||
self.ptt.r30_push_score(self)
|
||||
|
||||
# 总PTT更新
|
||||
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''', {
|
||||
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
||||
|
||||
# 世界模式判断
|
||||
if self.is_world_mode:
|
||||
self.user.select_user_about_world_play()
|
||||
self.user.current_map.select_map_info()
|
||||
if self.user.current_map.is_breached:
|
||||
self.world_play = BreachedWorldPlay(self.c, self.user, self)
|
||||
elif self.user.current_map.is_beyond:
|
||||
self.world_play = BeyondWorldPlay(self.c, self.user, self)
|
||||
else:
|
||||
self.world_play = WorldPlay(self.c, self.user, self)
|
||||
self.world_play.update()
|
||||
|
||||
# 课题模式判断
|
||||
if self.course_play_state >= 0:
|
||||
self.course_play.update()
|
||||
|
||||
|
||||
class Potential:
|
||||
'''
|
||||
用户潜力值计算处理类
|
||||
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.r30_tuples: 'list[tuple[int, str, int, float]]' = None
|
||||
self.r30: 'list[Score]' = None
|
||||
|
||||
self.b30: list = None
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
'''计算用户潜力值'''
|
||||
return self.best_30 * Constant.BEST30_WEIGHT + self.recent_10 * Constant.RECENT10_WEIGHT
|
||||
|
||||
@property
|
||||
def best_30(self) -> float:
|
||||
'''获取用户best30的总潜力值'''
|
||||
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
|
||||
'a': self.user.user_id})
|
||||
return sum(x[0] for x in self.c.fetchall())
|
||||
|
||||
def select_recent_30_tuple(self) -> None:
|
||||
'''获取用户recent30数据'''
|
||||
self.c.execute(
|
||||
'''select r_index, song_id, difficulty, rating from recent30 where user_id = ? and song_id != '' order by time_played DESC''', (self.user.user_id,))
|
||||
|
||||
self.r30_tuples = self.c.fetchall()
|
||||
|
||||
def select_recent_30(self) -> None:
|
||||
self.c.execute(
|
||||
'''select song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, time_played, clear_type, rating from recent30 where user_id = ? and song_id != '' order by time_played DESC''', (self.user.user_id,))
|
||||
|
||||
self.r30 = []
|
||||
for x in self.c.fetchall():
|
||||
s = Score()
|
||||
s.song.set_chart(x[0], x[1])
|
||||
s.set_score(*x[2:-1])
|
||||
s.rating = x[-1]
|
||||
self.r30.append(s)
|
||||
|
||||
@property
|
||||
def recent_10(self) -> float:
|
||||
'''获取用户recent10的总潜力值'''
|
||||
if self.r30_tuples is None:
|
||||
self.select_recent_30_tuple()
|
||||
|
||||
max_dict = {}
|
||||
for x in self.r30_tuples:
|
||||
if (x[1], x[2]) not in max_dict or max_dict[(x[1], x[2])] < x[3]:
|
||||
max_dict[(x[1], x[2])] = x[3]
|
||||
|
||||
top_10_rating = sorted(max_dict.values(), reverse=True)[:10]
|
||||
return sum(top_10_rating)
|
||||
|
||||
def recent_30_to_dict_list(self) -> list:
|
||||
if self.r30 is None:
|
||||
self.select_recent_30()
|
||||
|
||||
return [x.to_dict() for x in self.r30]
|
||||
|
||||
def update_one_r30(self, r_index: int, user_score: 'UserPlay | UserScore') -> None:
|
||||
'''更新数据表中的一条数据'''
|
||||
self.c.execute('''insert or replace into recent30 values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
||||
(self.user.user_id, r_index, user_score.time_played, user_score.song.song_id, user_score.song.difficulty,
|
||||
user_score.score, user_score.shiny_perfect_count, user_score.perfect_count, user_score.near_count, user_score.miss_count, user_score.health, user_score.modifier, user_score.clear_type, user_score.rating))
|
||||
|
||||
# 更新内存中的数据
|
||||
x = (r_index, user_score.song.song_id,
|
||||
user_score.song.difficulty, user_score.rating)
|
||||
if len(self.r30_tuples) < 30:
|
||||
self.r30_tuples.append(x)
|
||||
return
|
||||
|
||||
for i in range(30):
|
||||
if self.r30_tuples[i][0] == r_index:
|
||||
self.r30_tuples[i] = x
|
||||
break
|
||||
|
||||
def r30_push_score(self, user_score: 'UserPlay | UserScore') -> None:
|
||||
'''根据新成绩调整 r30'''
|
||||
if self.r30_tuples is None:
|
||||
self.select_recent_30_tuple()
|
||||
|
||||
if len(self.r30_tuples) < 30:
|
||||
self.update_one_r30(len(self.r30_tuples), user_score)
|
||||
return None
|
||||
|
||||
if user_score.is_protected:
|
||||
# 保护,替换最低的最旧的成绩
|
||||
f_tuples = list(
|
||||
filter(lambda x: x[-1] <= user_score.rating, self.r30_tuples))
|
||||
f_tuples.reverse() # 从旧到新
|
||||
f_tuples = sorted(f_tuples, key=lambda x: x[-1])
|
||||
if not f_tuples:
|
||||
# 找不到更低的成绩,不更新
|
||||
return None
|
||||
|
||||
unique_songs: 'dict[tuple[str, int], list[tuple[int, int, float]]]' = {}
|
||||
for i, x in enumerate(self.r30_tuples):
|
||||
unique_songs.setdefault((x[1], x[2]), []).append((i, x[0], x[3]))
|
||||
|
||||
new_song = user_score.song.to_tuple()
|
||||
|
||||
if len(unique_songs) >= 11 or (len(unique_songs) == 10 and new_song not in unique_songs):
|
||||
if user_score.is_protected:
|
||||
# 保护,替换最低的最旧的成绩
|
||||
self.update_one_r30(f_tuples[0][0], user_score)
|
||||
else:
|
||||
self.update_one_r30(self.r30_tuples[-1][0], user_score)
|
||||
return None
|
||||
|
||||
filtered_songs = dict(filter(lambda x: len(
|
||||
x[1]) > 1, unique_songs.items())) # 过滤掉只有单个成绩的
|
||||
|
||||
if new_song in unique_songs and new_song not in filtered_songs:
|
||||
# 如果新成绩有相同谱面的唯一成绩在 r30 中,则它也应该有可能被替换
|
||||
filtered_songs[new_song] = unique_songs[new_song]
|
||||
|
||||
if user_score.is_protected:
|
||||
# 保护,替换最低的最旧的成绩,此时需在 filtered_songs 中
|
||||
for x in f_tuples:
|
||||
if (x[1], x[2]) in filtered_songs:
|
||||
self.update_one_r30(x[0], user_score)
|
||||
return None
|
||||
else:
|
||||
# 找到符合条件的最旧成绩
|
||||
max_idx = -1
|
||||
max_r_index = -1
|
||||
for x in filtered_songs.values():
|
||||
for y in x:
|
||||
if y[0] > max_idx:
|
||||
max_idx = y[0]
|
||||
max_r_index = y[1]
|
||||
|
||||
self.update_one_r30(max_r_index, user_score)
|
||||
|
||||
|
||||
class UserScoreList:
|
||||
'''
|
||||
用户分数查询类
|
||||
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.scores: list = None
|
||||
self.query: 'Query' = Query(['user_id', 'song_id', 'difficulty'], ['song_id'], [
|
||||
'rating', 'difficulty', 'song_id', 'score', 'time_played'])
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict(has_user_info=False) for x in self.scores]
|
||||
|
||||
def select_from_user(self, user=None) -> None:
|
||||
'''获取用户的best_score数据'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self.query.query_append({'user_id': self.user.user_id})
|
||||
self.query.sort += [{'column': 'rating', 'order': 'DESC'}]
|
||||
x = Sql(self.c).select('best_score', query=self.query)
|
||||
|
||||
self.scores = [UserScore(self.c, self.user).from_list(i) for i in x]
|
||||
|
||||
def select_song_name(self) -> None:
|
||||
'''为所有成绩中的song_id查询song_name'''
|
||||
if self.scores is None:
|
||||
return
|
||||
for score in self.scores:
|
||||
self.c.execute(
|
||||
'''select name from chart where song_id = ?''', (score.song.song_id,))
|
||||
x = self.c.fetchone()
|
||||
score.song.song_name = x[0] if x else ''
|
||||
120
latest version/core/song.py
Normal file
120
latest version/core/song.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from .error import NoData
|
||||
from .config_manager import Config
|
||||
|
||||
|
||||
class Chart:
|
||||
# defnum: chart const * 10
|
||||
|
||||
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
||||
self.c = c
|
||||
self.song_id: str = None
|
||||
self.difficulty: int = None
|
||||
self.set_chart(song_id, difficulty)
|
||||
self.defnum: int = None
|
||||
self.song_name: str = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'difficulty': self.difficulty,
|
||||
'chart_const': self.chart_const
|
||||
}
|
||||
|
||||
def to_tuple(self) -> tuple:
|
||||
return (self.song_id, self.difficulty)
|
||||
|
||||
@property
|
||||
def chart_const(self) -> float:
|
||||
return self.defnum / 10 if self.defnum else -1
|
||||
|
||||
@property
|
||||
def song_id_difficulty(self) -> str:
|
||||
return '%s%d' % (self.song_id, self.difficulty)
|
||||
|
||||
def set_chart(self, song_id: str = None, difficulty: int = None) -> None:
|
||||
self.song_id = song_id
|
||||
self.difficulty = int(difficulty) if difficulty is not None else None
|
||||
|
||||
def select(self) -> None:
|
||||
self.c.execute(
|
||||
'''select rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart where song_id=:a''', {'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
if Config.ALLOW_SCORE_WITH_NO_SONG:
|
||||
self.defnum = -10
|
||||
else:
|
||||
raise NoData(f'The song `{self.song_id}` does not exist.', 120)
|
||||
else:
|
||||
self.defnum = x[self.difficulty]
|
||||
|
||||
|
||||
class Song:
|
||||
def __init__(self, c=None, song_id: str = None) -> None:
|
||||
self.c = c
|
||||
self.song_id: str = song_id
|
||||
self.name: str = None
|
||||
self.charts: dict = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'song_id': self.song_id,
|
||||
'name': self.name,
|
||||
'charts': [chart.to_dict() for chart in self.charts]
|
||||
}
|
||||
|
||||
def from_list(self, x: list) -> 'Song':
|
||||
if self.song_id is None:
|
||||
self.song_id = x[0]
|
||||
self.name = x[1]
|
||||
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), Chart(self.c, self.song_id, 4)]
|
||||
self.charts[0].defnum = x[2]
|
||||
self.charts[1].defnum = x[3]
|
||||
self.charts[2].defnum = x[4]
|
||||
self.charts[3].defnum = x[5]
|
||||
self.charts[4].defnum = x[6]
|
||||
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), Chart(self.c, self.song_id, 4)]
|
||||
for i in range(5):
|
||||
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=?, rating_etr=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.charts[4].defnum, self.song_id))
|
||||
|
||||
def insert(self) -> None:
|
||||
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, self.charts[4].defnum))
|
||||
|
||||
def select_exists(self, song_id: str = None) -> bool:
|
||||
if song_id is not None:
|
||||
self.song_id = song_id
|
||||
|
||||
self.c.execute(
|
||||
'''select exists(select * from chart where song_id=?)''', (self.song_id,))
|
||||
return bool(self.c.fetchone()[0])
|
||||
|
||||
def select(self, song_id: str = None) -> 'Song':
|
||||
if song_id is not None:
|
||||
self.song_id = song_id
|
||||
|
||||
self.c.execute('''select * from chart where song_id=:a''', {
|
||||
'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(f'The song `{self.song_id}` does not exist.')
|
||||
|
||||
return self.from_list(x)
|
||||
559
latest version/core/sql.py
Normal file
559
latest version/core/sql.py
Normal file
@@ -0,0 +1,559 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import traceback
|
||||
from atexit import register
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
|
||||
from .error import ArcError, InputError
|
||||
from .util import parse_version
|
||||
|
||||
|
||||
class Connect:
|
||||
# 数据库连接类,上下文管理
|
||||
logger = None
|
||||
|
||||
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db
|
||||
接受:文件路径
|
||||
返回:sqlite3连接操作对象
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.in_memory: bool = in_memory
|
||||
if logger is not None:
|
||||
self.logger = logger
|
||||
|
||||
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.c = self.conn.cursor()
|
||||
return self.c
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||
flag = True
|
||||
if exc_type is not None:
|
||||
if issubclass(exc_type, ArcError):
|
||||
flag = False
|
||||
else:
|
||||
self.conn.rollback()
|
||||
|
||||
self.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
class Query:
|
||||
'''查询参数类'''
|
||||
|
||||
def __init__(self, query_able: list = None, fuzzy_query_able: list = None, sort_able: list = None) -> None:
|
||||
self.query_able: list = query_able # None表示不限制
|
||||
self.fuzzy_query_able: list = fuzzy_query_able # None表示不限制
|
||||
self.sort_able: list = sort_able
|
||||
|
||||
self.__limit: int = -1
|
||||
self.__offset: int = 0
|
||||
|
||||
# {'name': 'admin'} or {'name': ['admin', 'user']}
|
||||
self.__query: dict = {}
|
||||
self.__fuzzy_query: dict = {} # {'name': 'dmi'}
|
||||
|
||||
# [{'column': 'user_id', 'order': 'ASC'}, ...]
|
||||
self.__sort: list = []
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
return self.__limit
|
||||
|
||||
@limit.setter
|
||||
def limit(self, limit: int) -> None:
|
||||
if not isinstance(limit, int):
|
||||
raise InputError(api_error_code=-101)
|
||||
self.__limit = limit
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
return self.__offset
|
||||
|
||||
@offset.setter
|
||||
def offset(self, offset: int) -> None:
|
||||
if not isinstance(offset, int):
|
||||
raise InputError(api_error_code=-101)
|
||||
self.__offset = offset
|
||||
|
||||
@property
|
||||
def query(self) -> dict:
|
||||
return self.__query
|
||||
|
||||
@query.setter
|
||||
def query(self, query: dict) -> None:
|
||||
self.__query = {}
|
||||
self.query_append(query)
|
||||
|
||||
def query_append(self, query: dict) -> None:
|
||||
if not isinstance(query, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.query_able is not None and query and not set(query).issubset(set(self.query_able)):
|
||||
raise InputError(api_error_code=-102)
|
||||
if not self.__query:
|
||||
self.__query = query
|
||||
else:
|
||||
self.__query.update(query)
|
||||
|
||||
@property
|
||||
def fuzzy_query(self) -> dict:
|
||||
return self.__fuzzy_query
|
||||
|
||||
@fuzzy_query.setter
|
||||
def fuzzy_query(self, fuzzy_query: dict) -> None:
|
||||
self.__fuzzy_query = {}
|
||||
self.fuzzy_query_append(fuzzy_query)
|
||||
|
||||
def fuzzy_query_append(self, fuzzy_query: dict) -> None:
|
||||
if not isinstance(fuzzy_query, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
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)
|
||||
if not self.__fuzzy_query:
|
||||
self.__fuzzy_query = fuzzy_query
|
||||
else:
|
||||
self.__fuzzy_query.update(fuzzy_query)
|
||||
|
||||
@property
|
||||
def sort(self) -> list:
|
||||
return self.__sort
|
||||
|
||||
@sort.setter
|
||||
def sort(self, sort: list) -> None:
|
||||
if not isinstance(sort, list):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.sort_able is not None and sort:
|
||||
for x in sort:
|
||||
if not isinstance(x, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if 'column' not in x or x['column'] not in self.sort_able:
|
||||
raise InputError(api_error_code=-103)
|
||||
if 'order' not in x:
|
||||
x['order'] = 'ASC'
|
||||
else:
|
||||
if x['order'] not in ['ASC', 'DESC']:
|
||||
raise InputError(api_error_code=-104)
|
||||
self.__sort = sort
|
||||
|
||||
def set_value(self, limit=-1, offset=0, query=None, fuzzy_query=None, sort=None) -> None:
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.query = query if query is not None else {}
|
||||
self.fuzzy_query = fuzzy_query if fuzzy_query is not None else {}
|
||||
self.sort = sort if sort is not None else []
|
||||
|
||||
def from_dict(self, d: dict) -> 'Query':
|
||||
self.set_value(d.get('limit', -1), d.get('offset', 0),
|
||||
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
|
||||
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:
|
||||
'''
|
||||
数据库增查删改类
|
||||
'''
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
|
||||
@staticmethod
|
||||
def get_select_sql(table_name: str, target_column: list = None, query: 'Query' = None):
|
||||
'''拼接单表内行查询单句sql语句,返回语句和参数列表'''
|
||||
sql_list = []
|
||||
if not target_column:
|
||||
sql = f'select * from {table_name}'
|
||||
else:
|
||||
sql = f"select {', '.join(target_column)} 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)
|
||||
|
||||
if query.sort:
|
||||
sql += ' order by ' + \
|
||||
', '.join([x['column'] + ' ' + x['order'] for x in query.sort])
|
||||
|
||||
if query.limit >= 0:
|
||||
sql += ' limit ? offset ?'
|
||||
sql_list.append(query.limit)
|
||||
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)
|
||||
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:
|
||||
|
||||
SPECIAL_UPDATE_VERSION = {
|
||||
'2.11.3.11': '_version_2_11_3_11',
|
||||
'2.11.3.13': '_version_2_11_3_13'
|
||||
}
|
||||
|
||||
def __init__(self, c1_path: str, c2_path: str) -> None:
|
||||
self.c1_path = c1_path
|
||||
self.c2_path = c2_path
|
||||
|
||||
self.c1 = None
|
||||
self.c2 = None
|
||||
|
||||
self.tables = Constant.DATABASE_MIGRATE_TABLES
|
||||
|
||||
@staticmethod
|
||||
def update_one_table(c1, c2, table_name: str) -> bool:
|
||||
'''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖'''
|
||||
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:
|
||||
self.c1 = c1
|
||||
self.c2 = c2
|
||||
self.special_update()
|
||||
|
||||
for i in self.tables:
|
||||
self.update_one_table(c1, c2, i)
|
||||
|
||||
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||
self.update_one_table(c1, c2, 'character')
|
||||
|
||||
self.update_user_char_full(c2) # 更新user_char_full
|
||||
|
||||
def special_update(self):
|
||||
old_version = self.c1.execute(
|
||||
'''select value from config where id = "version"''').fetchone()
|
||||
new_version = self.c2.execute(
|
||||
'''select value from config where id = "version"''').fetchone()
|
||||
old_version = old_version[0] if old_version else '0.0.0'
|
||||
new_version = new_version[0] if new_version else '0.0.0'
|
||||
old_version = parse_version(old_version)
|
||||
new_version = parse_version(new_version)
|
||||
|
||||
for k, v in self.SPECIAL_UPDATE_VERSION.items():
|
||||
if old_version < parse_version(k) <= new_version:
|
||||
getattr(self, v)()
|
||||
|
||||
def _version_2_11_3_11(self):
|
||||
'''
|
||||
2.11.3.11 版本特殊更新,调整 recent30 表结构
|
||||
recent30 表从 (user_id: int PK, rating<index>: real, song_id<index>: text, ...) \
|
||||
更改为 (user_id: int PK, r_index: int PK, time_played: int, song_id: text, difficulty: int, score: int, sp, p, n, m, hp, mod, clear_type, rating: real)
|
||||
'''
|
||||
|
||||
self.tables = [x for x in self.tables if x != 'recent30']
|
||||
|
||||
x = self.c1.execute('''select * from recent30''')
|
||||
sql_list = []
|
||||
for i in x:
|
||||
user_id = int(i[0])
|
||||
for j in range(30):
|
||||
rating = i[1 + j * 2]
|
||||
rating = float(rating) if rating else 0
|
||||
song_id_difficulty: str = i[2 + j * 2]
|
||||
if song_id_difficulty:
|
||||
song_id = song_id_difficulty[:-1]
|
||||
difficulty = song_id_difficulty[-1]
|
||||
difficulty = int(difficulty) if difficulty.isdigit() else 0
|
||||
else:
|
||||
song_id = ''
|
||||
difficulty = 0
|
||||
|
||||
sql_list.append(
|
||||
(user_id, j, 100-j, song_id, difficulty, rating))
|
||||
|
||||
self.c2.executemany(
|
||||
'''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list)
|
||||
|
||||
def _version_2_11_3_13(self):
|
||||
'''
|
||||
2.11.3.13 版本特殊更新,world_rank_score 机制调整,需清空用户分数
|
||||
'''
|
||||
self.c1.execute('''update user set world_rank_score = 0''')
|
||||
|
||||
|
||||
class LogDatabaseMigrator:
|
||||
|
||||
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 table if not exists bundle_download_token(token text primary key,
|
||||
file_path text, time int, device_id text);''')
|
||||
self.c.execute(
|
||||
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
||||
self.c.execute('''
|
||||
create table if not exists notification(
|
||||
user_id int, id int,
|
||||
type text, content text,
|
||||
sender_user_id int, sender_name text,
|
||||
timestamp int,
|
||||
primary key(user_id, id)
|
||||
)
|
||||
''')
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
@register
|
||||
def atexit():
|
||||
MemoryDatabase.conn.close()
|
||||
|
||||
|
||||
class UserKVTable:
|
||||
'''用户键值对表'''
|
||||
|
||||
def __init__(self, c=None, user_id: int = None, class_name: str = None) -> None:
|
||||
self.c = c
|
||||
self.user_id = user_id
|
||||
self.class_name = class_name
|
||||
|
||||
def get(self, key: str, idx: int = 0):
|
||||
'''获取键值对'''
|
||||
x = self.c.execute(
|
||||
'''select value from user_kvdata where user_id = ? and class = ? and key = ? and idx = ?''', (self.user_id, self.class_name, key, idx)).fetchone()
|
||||
return x[0] if x else None
|
||||
|
||||
def set(self, key: str, value, idx: int = 0) -> None:
|
||||
'''设置键值对'''
|
||||
self.c.execute('''insert or replace into user_kvdata values(?,?,?,?,?)''',
|
||||
(self.user_id, self.class_name, key, idx, value))
|
||||
|
||||
def __getitem__(self, args):
|
||||
if isinstance(args, tuple):
|
||||
return self.get(*args)
|
||||
else:
|
||||
return self.get(args)
|
||||
|
||||
def __setitem__(self, args, value):
|
||||
if isinstance(args, tuple):
|
||||
self.set(args[0], value, args[1])
|
||||
else:
|
||||
self.set(args, value)
|
||||
19
latest version/core/system.py
Normal file
19
latest version/core/system.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from time import time
|
||||
|
||||
from .constant import Constant
|
||||
|
||||
|
||||
class GameInfo:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"max_stamina": Constant.MAX_STAMINA,
|
||||
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
||||
"core_exp": Constant.CORE_EXP,
|
||||
"curr_ts": int(time()*1000),
|
||||
"level_steps": [{'level': k, 'level_exp': v} for k, v in Constant.LEVEL_STEPS.items()],
|
||||
"world_ranking_enabled": True,
|
||||
"is_byd_chapter_unlocked": True
|
||||
}
|
||||
881
latest version/core/user.py
Normal file
881
latest version/core/user.py
Normal file
@@ -0,0 +1,881 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
from os import urandom
|
||||
from random import randint
|
||||
|
||||
from .character import UserCharacter, UserCharacterList
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
||||
NoData, RateLimit, UserBan)
|
||||
from .item import UserItemList
|
||||
from .limiter import ArcLimiter
|
||||
from .mission import UserMissionList
|
||||
from .score import Score
|
||||
from .sql import Query, Sql, UserKVTable
|
||||
from .world import Map, MapParser, UserMap, UserStamina
|
||||
|
||||
|
||||
def code_get_id(c, user_code: str) -> int:
|
||||
# 用user_code获取user_id
|
||||
|
||||
c.execute('''select user_id from user where user_code = :a''',
|
||||
{'a': user_code})
|
||||
x = c.fetchone()
|
||||
|
||||
if x is not None:
|
||||
user_id = int(x[0])
|
||||
else:
|
||||
raise NoData('No user.', 401, -3)
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self) -> None:
|
||||
self.name: str = None
|
||||
self.email: str = None
|
||||
self.password: str = None
|
||||
self.user_id: int = None
|
||||
self.user_code: str = None
|
||||
|
||||
self.join_date = None
|
||||
self.rating_ptt: int = None # 100 times
|
||||
|
||||
self.ticket: int = None
|
||||
self.world_rank_score: int = None
|
||||
self.ban_flag = None
|
||||
|
||||
@property
|
||||
def hash_pwd(self) -> str:
|
||||
'''`password`的SHA-256值'''
|
||||
return hashlib.sha256(self.password.encode("utf8")).hexdigest()
|
||||
|
||||
|
||||
class UserRegister(User):
|
||||
|
||||
limiter_ip = ArcLimiter(
|
||||
Config.GAME_REGISTER_IP_RATE_LIMIT, 'game_register_ip')
|
||||
limiter_device = ArcLimiter(
|
||||
Config.GAME_REGISTER_DEVICE_RATE_LIMIT, 'game_register_device')
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def set_name(self, name: str):
|
||||
if 3 <= len(name) <= 16:
|
||||
self.c.execute(
|
||||
'''select exists(select * from user where name = :name)''', {'name': name})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.name = name
|
||||
else:
|
||||
raise DataExist('Username exists.', 101, -210)
|
||||
|
||||
else:
|
||||
raise InputError('Username is invalid.')
|
||||
|
||||
def set_password(self, password: str):
|
||||
if 8 <= len(password) <= 32:
|
||||
self.password = password
|
||||
else:
|
||||
raise InputError('Password is invalid.')
|
||||
|
||||
def set_email(self, email: str):
|
||||
# 邮箱格式懒得多判断
|
||||
if 4 <= len(email) <= 64 and '@' in email and '.' in email:
|
||||
self.c.execute(
|
||||
'''select exists(select * from user where email = :email)''', {'email': email})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.email = email
|
||||
else:
|
||||
raise DataExist('Email address exists.', 102, -211)
|
||||
else:
|
||||
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):
|
||||
# 生成9位的user_code,用的自然是随机
|
||||
random_times = 0
|
||||
|
||||
while random_times <= 1000:
|
||||
random_times += 1
|
||||
user_code = ''.join([str(randint(0, 9)) for _ in range(9)])
|
||||
self.c.execute('''select exists(select * from user where user_code = :user_code)''',
|
||||
{'user_code': user_code})
|
||||
if self.c.fetchone() == (0,):
|
||||
break
|
||||
|
||||
if random_times <= 1000:
|
||||
self.user_code = user_code
|
||||
else:
|
||||
raise ArcError('No available user code.')
|
||||
|
||||
def _build_user_id(self):
|
||||
# 生成user_id,往后加1
|
||||
self.c.execute('''select max(user_id) from user''')
|
||||
x = self.c.fetchone()
|
||||
if x[0] is not None:
|
||||
self.user_id = x[0] + 1
|
||||
else:
|
||||
self.user_id = 2000001
|
||||
|
||||
def _insert_user_char(self):
|
||||
# 为用户添加初始角色
|
||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
|
||||
(self.user_id, 0, 1, 0, 0, 0))
|
||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
|
||||
(self.user_id, 1, 1, 0, 0, 0))
|
||||
self.c.execute(
|
||||
'''select character_id, max_level, is_uncapped from character''')
|
||||
x = self.c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
exp = 25000 if i[1] == 30 else 10000
|
||||
self.c.execute('''insert or replace into user_char_full values(?,?,?,?,?,?,0)''',
|
||||
(self.user_id, i[0], i[1], exp, i[2], 0))
|
||||
|
||||
def register(self, device_id: str = None, ip: str = None):
|
||||
if device_id is not None and not self.limiter_device.hit(device_id):
|
||||
raise RateLimit(f'''Too many register attempts of device `{
|
||||
device_id}`''', 124, -213)
|
||||
if ip is not None and ip != '127.0.0.1' and not self.limiter_ip.hit(ip):
|
||||
raise RateLimit(f'''Too many register attempts of ip `{
|
||||
ip}`''', 124, -213)
|
||||
now = int(time.time() * 1000)
|
||||
if self.user_code is None:
|
||||
self._build_user_code()
|
||||
if self.user_id is None:
|
||||
self._build_user_id()
|
||||
self._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': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email})
|
||||
|
||||
|
||||
class UserLogin(User):
|
||||
# 密码和token的加密方式为 SHA-256
|
||||
limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login')
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.device_id = None
|
||||
self.ip = None
|
||||
self.token = None
|
||||
self.now = 0
|
||||
|
||||
def set_name(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def set_password(self, password: str):
|
||||
self.password = password
|
||||
|
||||
def set_device_id(self, device_id: str):
|
||||
self.device_id = device_id
|
||||
|
||||
def set_ip(self, ip: str):
|
||||
self.ip = ip
|
||||
|
||||
def _arc_auto_ban(self):
|
||||
# 多设备自动封号机制,返回封号时长
|
||||
self.c.execute('''delete from login where user_id=?''',
|
||||
(self.user_id, ))
|
||||
self.c.execute(
|
||||
'''select ban_flag from user where user_id=?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if x and x[0] != '' and x[0] is not None:
|
||||
last_ban_time = int(x[0].split(':', 1)[0])
|
||||
i = 0
|
||||
while i < len(Constant.BAN_TIME) - 1 and Constant.BAN_TIME[i] <= last_ban_time:
|
||||
i += 1
|
||||
ban_time = Constant.BAN_TIME[i]
|
||||
else:
|
||||
ban_time = Constant.BAN_TIME[0]
|
||||
|
||||
ban_flag = ':'.join(
|
||||
(str(ban_time), str(self.now + ban_time * 86400000)))
|
||||
self.c.execute('''update user set ban_flag=? where user_id=?''',
|
||||
(ban_flag, self.user_id))
|
||||
|
||||
return ban_time * 86400000
|
||||
|
||||
def _check_device(self, device_list):
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if not Config.ALLOW_LOGIN_SAME_DEVICE:
|
||||
if self.device_id in device_list: # 对相同设备进行删除
|
||||
self.c.execute('''delete from login where login_device=:a and user_id=:b''', {
|
||||
'a': self.device_id, 'b': self.user_id})
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - device_list.count(self.device_id) - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if should_delete_num >= 1: # 删掉多余token
|
||||
if not Config.ALLOW_LOGIN_SAME_DEVICE and Config.ALLOW_BAN_MULTIDEVICE_USER_AUTO: # 自动封号检查
|
||||
self.c.execute(
|
||||
'''select count(*) from login where user_id=? and login_time>?''', (self.user_id, self.now-86400000))
|
||||
if self.c.fetchone()[0] >= Config.LOGIN_DEVICE_NUMBER_LIMIT:
|
||||
remaining_ts = self._arc_auto_ban()
|
||||
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
|
||||
'remaining_ts': remaining_ts})
|
||||
|
||||
self.c.execute('''delete from login where rowid in (select rowid from login where user_id=:user_id limit :a);''',
|
||||
{'user_id': self.user_id, 'a': int(should_delete_num)})
|
||||
|
||||
def login(self, name: str = '', password: str = '', device_id: str = '', ip: str = ''):
|
||||
if name:
|
||||
self.set_name(name)
|
||||
if password:
|
||||
self.set_password(password)
|
||||
if device_id:
|
||||
self.set_device_id(device_id)
|
||||
if 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''', {
|
||||
'name': self.name})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData(f'Username `{self.name}` does not exist.', 104)
|
||||
|
||||
self.user_id = x[0]
|
||||
self.now = int(time.time() * 1000)
|
||||
if x[2] is not None and x[2] != '':
|
||||
# 自动封号检查
|
||||
ban_timestamp = int(x[2].split(':', 1)[1])
|
||||
if ban_timestamp > self.now:
|
||||
raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
|
||||
'remaining_ts': ban_timestamp-self.now})
|
||||
|
||||
if x[1] == '':
|
||||
# 账号封禁
|
||||
raise UserBan(
|
||||
f'The account `{self.user_id}` has been banned.', 106)
|
||||
|
||||
if x[1] != self.hash_pwd:
|
||||
raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
|
||||
|
||||
self.token = base64.b64encode(hashlib.sha256(
|
||||
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
||||
|
||||
self.c.execute(
|
||||
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
|
||||
y = self.c.fetchall()
|
||||
if y:
|
||||
self._check_device([i[0] if i[0] else '' for i in y])
|
||||
|
||||
self.c.execute('''insert into login values(:access_token, :user_id, :time, :ip, :device_id)''', {
|
||||
'user_id': self.user_id, 'access_token': self.token, 'device_id': self.device_id, 'time': self.now, 'ip': self.ip})
|
||||
|
||||
|
||||
class UserAuth(User):
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.token = None
|
||||
|
||||
def token_get_id(self):
|
||||
# 用token获取id,没有考虑不同用户token相同情况,说不定会有bug
|
||||
self.c.execute('''select user_id from login where access_token = :token''', {
|
||||
'token': self.token})
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
self.user_id = x[0]
|
||||
else:
|
||||
raise NoAccess('Wrong token.', -4)
|
||||
|
||||
return self.user_id
|
||||
|
||||
|
||||
class UserInfo(User):
|
||||
def __init__(self, c, user_id=None) -> None:
|
||||
User.__init__(self)
|
||||
self.c = c
|
||||
self.user_id = user_id
|
||||
self.character = None
|
||||
self.is_skill_sealed = False
|
||||
self.is_hide_rating = False
|
||||
self.recent_score = Score()
|
||||
self.favorite_character = None
|
||||
self.max_stamina_notification_enabled = False
|
||||
self.mp_notification_enabled = True
|
||||
self.prog_boost: int = 0
|
||||
self.beyond_boost_gauge: float = 0
|
||||
self.kanae_stored_prog: float = 0
|
||||
self.next_fragstam_ts: int = None
|
||||
self.world_mode_locked_end_ts: int = None
|
||||
self.current_map: 'Map' = None
|
||||
self.stamina: 'UserStamina' = None
|
||||
|
||||
self.insight_state: int = None
|
||||
|
||||
self.custom_banner = None
|
||||
|
||||
self.__cores: list = None
|
||||
self.__packs: list = None
|
||||
self.__singles: list = None
|
||||
self.characters: 'UserCharacterList' = None
|
||||
self.__friends: list = None
|
||||
self.__world_unlocks: list = None
|
||||
self.__world_songs: list = None
|
||||
self.curr_available_maps: list = None
|
||||
self.__course_banners: list = None
|
||||
|
||||
@property
|
||||
def is_insight_enabled(self) -> bool:
|
||||
if self.insight_state is None:
|
||||
self.select_user_one_column('insight_state', 4, int)
|
||||
return self.insight_state == 3 or self.insight_state == 5
|
||||
|
||||
@property
|
||||
def cores(self) -> list:
|
||||
if self.__cores is None:
|
||||
x = UserItemList(self.c, self).select_from_type('core')
|
||||
self.__cores = [{'core_type': i.item_id,
|
||||
'amount': i.amount} for i in x.items]
|
||||
|
||||
return self.__cores
|
||||
|
||||
@property
|
||||
def singles(self) -> list:
|
||||
if self.__singles is None:
|
||||
x = UserItemList(self.c, self).select_from_type('single')
|
||||
self.__singles = [i.item_id for i in x.items]
|
||||
|
||||
return self.__singles
|
||||
|
||||
@property
|
||||
def packs(self) -> list:
|
||||
if self.__packs is None:
|
||||
x = UserItemList(self.c, self).select_from_type('pack')
|
||||
self.__packs = [i.item_id for i in x.items]
|
||||
|
||||
return self.__packs
|
||||
|
||||
@property
|
||||
def pick_ticket(self) -> int:
|
||||
x = UserItemList(self.c, self).select_from_type('pick_ticket')
|
||||
if not x.items:
|
||||
return 0
|
||||
return x.items[0].amount
|
||||
|
||||
@property
|
||||
def world_unlocks(self) -> list:
|
||||
if self.__world_unlocks is None:
|
||||
x = UserItemList(self.c, self).select_from_type(
|
||||
'world_unlock')
|
||||
self.__world_unlocks = [i.item_id for i in x.items]
|
||||
|
||||
return self.__world_unlocks
|
||||
|
||||
@property
|
||||
def world_songs(self) -> list:
|
||||
if self.__world_songs is None:
|
||||
x = UserItemList(
|
||||
self.c, self).select_from_type('world_song')
|
||||
self.__world_songs = [i.item_id for i in x.items]
|
||||
|
||||
return self.__world_songs
|
||||
|
||||
@property
|
||||
def course_banners(self) -> list:
|
||||
if self.__course_banners is None:
|
||||
x = UserItemList(
|
||||
self.c, self).select_from_type('course_banner')
|
||||
self.__course_banners = [i.item_id for i in x.items]
|
||||
|
||||
return self.__course_banners
|
||||
|
||||
def select_characters(self) -> None:
|
||||
self.characters = UserCharacterList(self.c, self)
|
||||
self.characters.select_user_characters()
|
||||
|
||||
@property
|
||||
def characters_list(self) -> list:
|
||||
if self.characters is None:
|
||||
self.select_characters()
|
||||
return [x.character_id for x in self.characters.characters]
|
||||
|
||||
@property
|
||||
def character_displayed(self) -> 'UserCharacter':
|
||||
'''对外显示的角色'''
|
||||
if self.favorite_character is None:
|
||||
return self.character
|
||||
|
||||
self.favorite_character.select_character_uncap_condition(self)
|
||||
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
|
||||
def friends(self) -> list:
|
||||
# 得到用户的朋友列表
|
||||
if self.__friends is None:
|
||||
s = []
|
||||
for i in self.friend_ids:
|
||||
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})
|
||||
|
||||
is_mutual = self.c.fetchone() == (1,)
|
||||
|
||||
you = UserOnline(self.c, i[0])
|
||||
you.select_user()
|
||||
character = you.character if you.favorite_character is None else you.favorite_character
|
||||
character.select_character_uncap_condition(you)
|
||||
|
||||
rating = you.rating_ptt if not you.is_hide_rating else -1
|
||||
|
||||
s.append({
|
||||
"is_mutual": is_mutual,
|
||||
"is_char_uncapped_override": character.is_uncapped_override,
|
||||
"is_char_uncapped": character.is_uncapped,
|
||||
"is_skill_sealed": you.is_skill_sealed,
|
||||
"rating": rating,
|
||||
"join_date": you.join_date,
|
||||
"character": character.character_id,
|
||||
"recent_score": you.recent_score_list,
|
||||
"name": you.name,
|
||||
"user_id": you.user_id
|
||||
})
|
||||
s.sort(key=lambda item: item["recent_score"][0]["time_played"] if len(
|
||||
item["recent_score"]) > 0 else 0, reverse=True)
|
||||
self.__friends = s
|
||||
|
||||
return self.__friends
|
||||
|
||||
@property
|
||||
def recent_score_list(self) -> list:
|
||||
# 用户最近一次成绩,是列表
|
||||
if self.name is None:
|
||||
self.select_user()
|
||||
|
||||
if self.recent_score.song.song_id is None:
|
||||
return []
|
||||
|
||||
self.c.execute('''select best_clear_type from best_score where user_id=:u and song_id=:s and difficulty=:d''', {
|
||||
'u': self.user_id, 's': self.recent_score.song.song_id, 'd': self.recent_score.song.difficulty})
|
||||
y = self.c.fetchone()
|
||||
best_clear_type = y[0] if y is not None else self.recent_score.clear_type
|
||||
|
||||
r = self.recent_score.to_dict()
|
||||
r["best_clear_type"] = best_clear_type
|
||||
return [r]
|
||||
|
||||
def select_curr_available_maps(self) -> None:
|
||||
self.curr_available_maps: list = []
|
||||
for i in Config.AVAILABLE_MAP:
|
||||
self.curr_available_maps.append(Map(i))
|
||||
|
||||
@property
|
||||
def curr_available_maps_list(self) -> list:
|
||||
if self.curr_available_maps is None:
|
||||
self.select_curr_available_maps()
|
||||
return [x.to_dict() for x in self.curr_available_maps]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
'''返回用户信息的字典,其实就是/user/me'''
|
||||
if self.name is None:
|
||||
self.select_user()
|
||||
|
||||
# 这是考虑有可能favourite_character设置了用户未拥有的角色,同时提前计算角色列表
|
||||
character_list = self.characters_list
|
||||
if self.favorite_character and self.favorite_character.character_id in character_list:
|
||||
favorite_character_id = self.favorite_character.character_id
|
||||
else:
|
||||
favorite_character_id = -1
|
||||
|
||||
if self.character.character_id not in character_list:
|
||||
self.character.character_id = 0
|
||||
|
||||
return {
|
||||
"is_aprilfools": Config.IS_APRILFOOLS,
|
||||
"curr_available_maps": self.curr_available_maps_list,
|
||||
"character_stats": [x.to_dict() for x in self.characters.characters],
|
||||
"friends": self.friends,
|
||||
"settings": {
|
||||
"favorite_character": favorite_character_id,
|
||||
"is_hide_rating": self.is_hide_rating,
|
||||
"max_stamina_notification_enabled": self.max_stamina_notification_enabled,
|
||||
"mp_notification_enabled": self.mp_notification_enabled,
|
||||
},
|
||||
"user_id": self.user_id,
|
||||
"name": self.name,
|
||||
"user_code": self.user_code,
|
||||
"display_name": self.name,
|
||||
"ticket": self.ticket,
|
||||
"character": self.character.character_id,
|
||||
"is_locked_name_duplicate": False,
|
||||
"is_skill_sealed": self.is_skill_sealed,
|
||||
"current_map": self.current_map.map_id,
|
||||
"prog_boost": self.prog_boost,
|
||||
"beyond_boost_gauge": self.beyond_boost_gauge,
|
||||
"kanae_stored_prog": self.kanae_stored_prog,
|
||||
"next_fragstam_ts": self.next_fragstam_ts,
|
||||
"max_stamina_ts": self.stamina.max_stamina_ts,
|
||||
"stamina": self.stamina.stamina,
|
||||
"world_unlocks": self.world_unlocks,
|
||||
"world_songs": self.world_songs,
|
||||
"singles": self.singles,
|
||||
"packs": self.packs,
|
||||
"characters": character_list,
|
||||
"cores": self.cores,
|
||||
"recent_score": self.recent_score_list,
|
||||
"max_friend": Constant.MAX_FRIEND_COUNT,
|
||||
"rating": self.rating_ptt,
|
||||
"join_date": self.join_date,
|
||||
"global_rank": self.global_rank,
|
||||
'country': '',
|
||||
'course_banners': self.course_banners,
|
||||
'world_mode_locked_end_ts': self.world_mode_locked_end_ts,
|
||||
'locked_char_ids': [], # [1]
|
||||
'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(),
|
||||
'pick_ticket': self.pick_ticket,
|
||||
|
||||
# 'custom_banner': 'online_banner_2024_06',
|
||||
'custom_banner': self.custom_banner,
|
||||
# 'subscription_multiplier': 114,
|
||||
# 'memory_boost_ticket': 5,
|
||||
'insight_state': self.insight_state, # 0~2 不可选,3 技能激活,4 未激活,5 激活可选,6 未激活可选
|
||||
|
||||
# 'enabled_features': [
|
||||
# {
|
||||
# "metadata": ["USA"],
|
||||
# "feature": "paymentlink"
|
||||
# }
|
||||
# ],
|
||||
|
||||
# has_email: True
|
||||
}
|
||||
|
||||
def from_list(self, x: list) -> 'UserInfo':
|
||||
'''从数据库user表全部数据获取信息'''
|
||||
if not x:
|
||||
return None
|
||||
if self.user_id is None:
|
||||
self.user_id = x[0]
|
||||
self.name = x[1]
|
||||
self.join_date = int(x[3])
|
||||
self.user_code = x[4]
|
||||
self.rating_ptt = x[5]
|
||||
self.character = UserCharacter(self.c, x[6])
|
||||
self.is_skill_sealed = x[7] == 1
|
||||
self.character.is_uncapped = x[8] == 1
|
||||
self.character.is_uncapped_override = x[9] == 1
|
||||
self.is_hide_rating = x[10] == 1
|
||||
|
||||
self.recent_score.song.song_id = x[11]
|
||||
self.recent_score.song.difficulty = x[12]
|
||||
self.recent_score.set_score(
|
||||
x[13], x[14], x[15], x[16], x[17], x[18], x[19], x[20], x[21])
|
||||
self.recent_score.rating = x[22]
|
||||
|
||||
self.favorite_character = None if x[23] == - \
|
||||
1 else UserCharacter(self.c, x[23])
|
||||
self.max_stamina_notification_enabled = x[24] == 1
|
||||
self.current_map = Map(x[25]) if x[25] is not None else Map('')
|
||||
self.ticket = x[26]
|
||||
self.prog_boost = x[27] if x[27] is not None else 0
|
||||
self.email = x[28] if x[28] is not None else ''
|
||||
self.world_rank_score = x[29] if x[29] is not None else 0
|
||||
self.ban_flag = x[30] if x[30] is not None else ''
|
||||
|
||||
self.next_fragstam_ts = x[31] if x[31] else 0
|
||||
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[32], x[33])
|
||||
self.world_mode_locked_end_ts = x[34] if x[34] else -1
|
||||
self.beyond_boost_gauge = x[35] if x[35] else 0
|
||||
self.kanae_stored_prog = x[36] if x[36] else 0
|
||||
|
||||
self.mp_notification_enabled = x[37] == 1
|
||||
|
||||
self.insight_state = x[38]
|
||||
|
||||
self.custom_banner = x[39] if x[39] is not None else ''
|
||||
|
||||
return self
|
||||
|
||||
def select_user(self) -> None:
|
||||
# 查user表所有信息
|
||||
self.c.execute(
|
||||
'''select * from user where user_id = :x''', {'x': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.from_list(x)
|
||||
|
||||
def select_user_about_current_map(self) -> None:
|
||||
self.c.execute('''select current_map from user where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.current_map = Map(x[0])
|
||||
|
||||
def select_user_about_stamina(self) -> None:
|
||||
self.c.execute('''select max_stamina_ts, stamina from user where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[0], x[1])
|
||||
|
||||
def from_list_about_character(self, x: list) -> None:
|
||||
'''从数据库user表获取搭档信息'''
|
||||
self.name = x[0]
|
||||
self.character = UserCharacter(self.c, x[1], self)
|
||||
self.is_skill_sealed = x[2] == 1
|
||||
self.character.is_uncapped = x[3] == 1
|
||||
self.character.is_uncapped_override = x[4] == 1
|
||||
self.favorite_character = None if x[5] == - \
|
||||
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:
|
||||
'''
|
||||
查询user表有关世界模式打歌的信息
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge, kanae_stored_prog from user where user_id=?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.character = UserCharacter(self.c, x[0], self)
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[1], x[2])
|
||||
self.is_skill_sealed = x[3] == 1
|
||||
self.character.is_uncapped = x[4] == 1
|
||||
self.character.is_uncapped_override = x[5] == 1
|
||||
self.current_map = UserMap(self.c, x[6], self)
|
||||
self.world_mode_locked_end_ts = x[7] if x[7] else -1
|
||||
self.beyond_boost_gauge = x[8] if x[8] else 0
|
||||
self.kanae_stored_prog = x[9] if x[9] else 0
|
||||
|
||||
def select_user_about_link_play(self) -> None:
|
||||
'''
|
||||
查询 user 表有关 link play 的信息
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select name, rating_ptt, is_hide_rating from user where user_id=?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.name = x[0]
|
||||
self.rating_ptt = x[1]
|
||||
self.is_hide_rating = x[2] == 1
|
||||
|
||||
@property
|
||||
def global_rank(self) -> int:
|
||||
'''用户世界排名,如果超过设定最大值,返回0'''
|
||||
if self.world_rank_score is None:
|
||||
self.select_user_one_column('world_rank_score', 0)
|
||||
if not self.world_rank_score:
|
||||
return 0
|
||||
|
||||
self.c.execute(
|
||||
'''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,))
|
||||
y = self.c.fetchone()
|
||||
if y and y[0] + 1 <= Config.WORLD_RANK_MAX:
|
||||
return y[0] + 1
|
||||
|
||||
return 0
|
||||
|
||||
def update_global_rank(self) -> None:
|
||||
'''用户世界排名计算,有新增成绩则要更新'''
|
||||
|
||||
self.c.execute(
|
||||
'''
|
||||
with user_scores as (
|
||||
select song_id, difficulty, score_v2 from best_score where user_id = ? and difficulty in (2, 3, 4)
|
||||
)
|
||||
select sum(a) from(
|
||||
select sum(score_v2) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0)
|
||||
union
|
||||
select sum(score_v2) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0)
|
||||
union
|
||||
select sum(score_v2) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0)
|
||||
)
|
||||
''',
|
||||
(self.user_id,)
|
||||
)
|
||||
x = self.c.fetchone()
|
||||
if x[0] is None:
|
||||
return
|
||||
|
||||
self.c.execute(
|
||||
'''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id))
|
||||
self.world_rank_score = x[0]
|
||||
|
||||
def update_user_world_complete_info(self) -> None:
|
||||
'''
|
||||
更新用户的世界模式完成信息,包括两个部分
|
||||
|
||||
1. 每个章节的完成地图数量,为了 salt 技能
|
||||
2. 全世界模式完成台阶数之和,为了 fatalis 技能
|
||||
'''
|
||||
kvd = UserKVTable(self.c, self.user_id, 'world')
|
||||
|
||||
for chapter_id, map_ids in MapParser.chapter_info_without_repeatable.items():
|
||||
self.c.execute(
|
||||
f'''select map_id, curr_position from user_world where user_id = ? and map_id in ({','.join(['?']*len(map_ids))})''',
|
||||
(self.user_id, *map_ids)
|
||||
)
|
||||
x = self.c.fetchall()
|
||||
n = 0
|
||||
for map_id, curr_position in x:
|
||||
step_count = MapParser.world_info[map_id]['step_count']
|
||||
if curr_position == step_count - 1:
|
||||
n += 1
|
||||
kvd['chapter_complete_count', chapter_id] = n
|
||||
|
||||
self.c.execute(
|
||||
'''select sum(curr_position) + count(*) from user_world where user_id = ?''', (self.user_id,)
|
||||
)
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
kvd['total_step_count'] = x[0] or 0
|
||||
|
||||
def select_user_one_column(self, column_name: str, default_value=None, data_type=None) -> None:
|
||||
'''
|
||||
查询user表的某个属性
|
||||
请注意必须是一个普通属性,不能是一个类的实例
|
||||
'''
|
||||
if column_name not in self.__dict__:
|
||||
raise InputError('No such column.')
|
||||
self.c.execute(f'''select {column_name} from user where user_id = :a''', {
|
||||
'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
data = x[0] if x[0] is not None else default_value
|
||||
|
||||
if data_type is not None:
|
||||
data = data_type(data)
|
||||
|
||||
self.__dict__[column_name] = data
|
||||
|
||||
def update_user_one_column(self, column_name: str, value=None) -> None:
|
||||
'''
|
||||
更新user表的某个属性
|
||||
请注意必须是一个普通属性,不能是一个类的实例
|
||||
'''
|
||||
if column_name not in self.__dict__:
|
||||
raise InputError('No such column.')
|
||||
if value is not None:
|
||||
self.__dict__[column_name] = value
|
||||
self.c.execute(f'''update user set {column_name} = :a where user_id = :b''', {
|
||||
'a': self.__dict__[column_name], 'b': self.user_id})
|
||||
|
||||
|
||||
class UserOnline(UserInfo):
|
||||
|
||||
def __init__(self, c, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
|
||||
def change_character(self, character_id: int, skill_sealed: bool = False):
|
||||
'''用户角色改变,包括技能封印的改变'''
|
||||
self.character = UserCharacter(self.c, character_id, self)
|
||||
self.character.select_character_uncap_condition()
|
||||
self.is_skill_sealed = skill_sealed
|
||||
|
||||
self.c.execute('''update user set is_skill_sealed = :a, character_id = :b, is_char_uncapped = :c, is_char_uncapped_override = :d where user_id = :e''', {
|
||||
'a': 1 if self.is_skill_sealed else 0, 'b': self.character.character_id, 'c': self.character.is_uncapped, 'd': self.character.is_uncapped_override, 'e': self.user_id})
|
||||
|
||||
def add_friend(self, friend_id: int):
|
||||
'''加好友'''
|
||||
if self.user_id == friend_id:
|
||||
raise FriendError('Add yourself as a friend.', 604)
|
||||
|
||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute('''insert into friend values(:a, :b)''', {
|
||||
'a': self.user_id, 'b': friend_id})
|
||||
else:
|
||||
raise FriendError('The user has been your friend.', 602)
|
||||
|
||||
def delete_friend(self, friend_id: int):
|
||||
'''删好友'''
|
||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
if self.c.fetchone() == (1,):
|
||||
self.c.execute('''delete from friend where user_id_me = :x and user_id_other = :y''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
else:
|
||||
raise FriendError('No user or the user is not your friend.', 401)
|
||||
|
||||
def change_favorite_character(self, character_id: int) -> None:
|
||||
'''更改用户的favorite_character'''
|
||||
self.favorite_character = UserCharacter(self.c, character_id, self)
|
||||
self.c.execute('''update user set favorite_character = :a where user_id = :b''',
|
||||
{'a': self.favorite_character.character_id, 'b': self.user_id})
|
||||
|
||||
def toggle_invasion(self) -> None:
|
||||
self.c.execute(
|
||||
'''select insight_state from user where user_id = ?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
self.insight_state = x[0]
|
||||
rq = Constant.INSIGHT_TOGGLE_STATES
|
||||
self.insight_state = rq[(rq.index(self.insight_state) + 1) % len(rq)]
|
||||
|
||||
self.c.execute(
|
||||
'''update user set insight_state = ? where user_id = ?''', (self.insight_state, self.user_id))
|
||||
|
||||
|
||||
class UserChanger(UserInfo, UserRegister):
|
||||
|
||||
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}))
|
||||
74
latest version/core/util.py
Normal file
74
latest version/core/util.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import hashlib
|
||||
import os
|
||||
from datetime import date
|
||||
from time import mktime
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
|
||||
def aes_gcm_128_encrypt(key, plaintext, associated_data):
|
||||
iv = os.urandom(12)
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, min_tag_length=16),
|
||||
).encryptor()
|
||||
encryptor.authenticate_additional_data(associated_data)
|
||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return (iv, ciphertext, encryptor.tag)
|
||||
|
||||
|
||||
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, tag, min_tag_length=16),
|
||||
).decryptor()
|
||||
decryptor.authenticate_additional_data(associated_data)
|
||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||
|
||||
|
||||
def md5(code: str) -> str:
|
||||
# md5加密算法
|
||||
code = code.encode()
|
||||
md5s = hashlib.md5()
|
||||
md5s.update(code)
|
||||
codes = md5s.hexdigest()
|
||||
|
||||
return codes
|
||||
|
||||
|
||||
def get_file_md5(file_path: str) -> str:
|
||||
'''计算文件MD5,假设是文件'''
|
||||
myhash = hashlib.md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
b = f.read(8192)
|
||||
if not b:
|
||||
break
|
||||
myhash.update(b)
|
||||
|
||||
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()))
|
||||
|
||||
|
||||
def parse_version(s: str) -> 'list[int]':
|
||||
'''解析版本号'''
|
||||
s_number = "".join(x for x in s if x.isdigit() or x == '.')
|
||||
return list(map(int, [x for x in s_number.split('.') if x != '']))
|
||||
1241
latest version/core/world.py
Normal file
1241
latest version/core/world.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
5
latest version/database/bundle/README.md
Normal file
5
latest version/database/bundle/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## Arcaea Bundle Folder
|
||||
|
||||
[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler)
|
||||
|
||||
There must be pairs of bundle file and JSON metadata file with the same name. The bundle file's suffix is `.cb` and the metadata file's suffix is `.json`.
|
||||
@@ -1,356 +1,16 @@
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
|
||||
# 数据库初始化文件,删掉arcaea_database.db文件后运行即可,谨慎使用
|
||||
|
||||
conn = sqlite3.connect('arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
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
|
||||
);''')
|
||||
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,
|
||||
level_exp real,
|
||||
frag real,
|
||||
prog real,
|
||||
overdrive real,
|
||||
skill_id text,
|
||||
skill_unlock_level int,
|
||||
skill_requires_uncap int,
|
||||
skill_id_uncap text,
|
||||
char_type int,
|
||||
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,
|
||||
level int,
|
||||
exp real,
|
||||
level_exp real,
|
||||
frag real,
|
||||
prog real,
|
||||
overdrive real,
|
||||
skill_id text,
|
||||
skill_unlock_level int,
|
||||
skill_requires_uncap int,
|
||||
skill_id_uncap text,
|
||||
char_type int,
|
||||
uncap_cores text,
|
||||
is_uncapped int,
|
||||
is_uncapped_override int
|
||||
);''')
|
||||
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 world_songplay(user_id int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
stamina_multiply int,
|
||||
fragment_multiply int,
|
||||
prog_boost_multiply int,
|
||||
primary key(user_id, song_id, difficulty)
|
||||
);''')
|
||||
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,
|
||||
token text,
|
||||
time int,
|
||||
primary key(user_id, token, time)
|
||||
);''')
|
||||
c.execute('''create table if not exists item(item_id text,
|
||||
type text,
|
||||
is_available int,
|
||||
price int,
|
||||
orig_price int,
|
||||
discount_from int,
|
||||
discount_to 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,
|
||||
primary key(user_id, 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
|
||||
);''')
|
||||
c.execute('''create table if not exists present(present_id text primary key,
|
||||
expire_ts int,
|
||||
items text,
|
||||
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 songfile(song_id text,
|
||||
file_type int,
|
||||
md5 text,
|
||||
primary key(song_id, file_type)
|
||||
);''')
|
||||
c.execute('''create table if not exists redeem(code text primary key,
|
||||
items text,
|
||||
type int
|
||||
);''')
|
||||
c.execute('''create table if not exists user_redeem(user_id int,
|
||||
code text,
|
||||
primary key(user_id, code)
|
||||
);''')
|
||||
|
||||
def main(core_path: str = '../', db_path: str = './arcaea_database.db', init_folder_path: str = './init'):
|
||||
sys.path.append(os.path.join(sys.path[0], core_path))
|
||||
sys.path.append(os.path.join(sys.path[0], core_path, './core/'))
|
||||
init = import_module('init').DatabaseInit(db_path, init_folder_path)
|
||||
init.init()
|
||||
|
||||
|
||||
char = ['Hikari', 'Tairitsu', 'Kou', 'Sapphire', 'Lethe', '', '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']
|
||||
|
||||
skill_id = ['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', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion']
|
||||
|
||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', '', '', 'shirabe_entry_fee',
|
||||
'', '', '', '', '', '', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
|
||||
for i in range(0, 45):
|
||||
if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43]:
|
||||
sql = 'insert into character values('+str(
|
||||
i)+',"'+char[i]+'''",30,25000,25000,90,90,90,"'''+skill_id[i]+'''",0,0,"'''+skill_id_uncap[i]+'''",0,'',1,1)'''
|
||||
c.execute(sql)
|
||||
else:
|
||||
if i != 5:
|
||||
sql = 'insert into character values('+str(
|
||||
i)+',"'+char[i]+'''",30,25000,25000,90,90,90,"'''+skill_id[i]+'''",0,0,"'''+skill_id_uncap[i]+'''",0,'',0,0)'''
|
||||
c.execute(sql)
|
||||
|
||||
|
||||
def b2int(x):
|
||||
# int与布尔值转换
|
||||
if x:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
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']
|
||||
for j in i['items']:
|
||||
if "_id" not in j:
|
||||
_id = ''
|
||||
else:
|
||||
_id = j['_id']
|
||||
if j['type'] != 'character':
|
||||
c.execute('''insert into item(item_id, type, is_available, price, orig_price, discount_from, discount_to, _id) values(:a,:b,:c,:d,:e,:f,:g,:h)''', {
|
||||
'a': j['id'], 'b': j['type'], 'c': b2int(j['is_available']), 'd': i['price'], 'e': i['orig_price'], 'f': discount_from, 'g': discount_to, 'h': _id})
|
||||
|
||||
|
||||
f = open('singles.json', 'r')
|
||||
singles = json.load(f)
|
||||
f.close()
|
||||
insert_items(c, singles)
|
||||
|
||||
f = open('packs.json', 'r')
|
||||
packs = json.load(f)
|
||||
f.close()
|
||||
insert_items(c, packs)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def arc_register(name: str, password: str):
|
||||
def build_user_code(c):
|
||||
return '123456789'
|
||||
|
||||
def build_user_id(c):
|
||||
return 2000000
|
||||
|
||||
# def insert_user_char(c, user_id):
|
||||
# for i in range(0, 38):
|
||||
# if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21]:
|
||||
# sql = 'insert into user_char values('+str(user_id)+','+str(
|
||||
# i)+''',30,25000,25000,90,90,90,'',0,0,'',0,1,1)'''
|
||||
# c.execute(sql)
|
||||
# else:
|
||||
# if i != 5:
|
||||
# sql = 'insert into user_char values('+str(user_id)+','+str(
|
||||
# i)+''',30,25000,25000,90,90,90,'',0,0,'',0,0,0)'''
|
||||
# c.execute(sql)
|
||||
def insert_user_char(c, user_id):
|
||||
# 为用户添加所有可用角色
|
||||
c.execute('''select * from character''')
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
for i in x:
|
||||
c.execute('''insert into user_char values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n,:o)''', {
|
||||
'a': user_id, 'b': i[0], 'c': i[2], 'd': i[3], 'e': i[4], 'f': i[5], 'g': i[6], 'h': i[7], 'i': i[8], 'j': i[9], 'k': i[10], 'l': i[11], 'm': i[12], 'n': i[14], 'o': i[15]})
|
||||
|
||||
conn = sqlite3.connect('arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
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', 'admin123')
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
99
latest version/database/init/arc_data.py
Normal file
99
latest version/database/init/arc_data.py
Normal file
@@ -0,0 +1,99 @@
|
||||
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', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe', 'Forlorn(Ascendant - 6th Seeker)', 'Chinatsu', 'Tsumugi', 'Nai', 'Selene Sheryl (MIR-203)', 'Salt', 'Acid', 'Hikari & Selene Sheryl (Fracture & MIR-203)', 'Hikari & El Clear', 'Tairitsu & El Fail', 'Nami & Sui (Twilight)', 'Nonoka (Azure)', 'Vita (Wanderer)', 'Hikari & Tairitsu[ANS] (Breakthrough)', 'Hikari & Tairitsu[ANS] (Debut)', 'VIIM', 'Helena', 'Yuno']
|
||||
|
||||
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_aichan', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_nell', 'skill_chinatsu', 'skill_tsumugi', 'skill_nai', 'skill_selene', 'skill_salt', 'skill_acid', 'skill_hikari_selene', 'skill_hikari_clear', 'skill_tairitsu_fail', 'skill_nami_sui', 'skill_nonoka', 'skill_vita_arc', '', 'skill_hikari_tairitsu_debut', 'skill_hp_slow_drain', 'skill_lost_to_85', 'skill_hprate_based_on_hp']
|
||||
|
||||
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_maya_uncap', '', 'skill_luin_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_nonoka_uncap', '', '', '', '', '', '']
|
||||
|
||||
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
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, 45, 50, 38, 22, 63, 37, 23, 59, 45, 20, 43, 50, 22, 37, 26, 47, 34, 45, 23, 47, 50, 50]
|
||||
|
||||
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, 40, 50, 45, 41, 12, 31, 72, 40, 16, 33, 35, 23, 24, 46, 26, 49, 32, 35, 43, 45, 62, 77, 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, 35, 34, 45, 41, 12, 19, 38, 40, 26, 39, 56, 20, 25, 46, 18, 71, 29, 25, 73, 45, 62, 47, 50, 50]
|
||||
|
||||
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, 115, 70, 50, 61.6, 48, 37, 90, 60, 50, 92, 66, 44, 79, 50, 47, 55, 49, 79, 52, 90, 35, 68, 50, 50]
|
||||
|
||||
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, 80, 80, 67, 41, 55, 50, 103, 66, 35, 52, 65, 50, 43, 84, 55, 73, 59, 60, 64, 67, 94, 110, 50, 50]
|
||||
|
||||
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, 75, 54, 90, 41, 34, 30, 55, 66, 55, 62, 81, 44, 46, 84, 39, 105, 55, 43, 110, 50, 94, 68, 50, 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, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 50, 115, 80, 50, 61.6, 48, 37, 90, 60, 50, 102, 76, 44, 89, 50, 47, 55, 49, 89, 52, 90, 35, 68, 50, 50]
|
||||
|
||||
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
|
||||
80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 57, 80, 90, 67, 41, 55, 50, 103, 66, 35, 62, 75, 50, 53, 84, 55, 73, 59, 70, 64, 67, 94, 110, 50, 50]
|
||||
|
||||
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
|
||||
56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 57, 75, 64, 90, 41, 34, 30, 55, 66, 55, 72, 91, 44, 56, 84, 39, 105, 55, 53, 110, 50, 94, 68, 50, 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, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 4, 2, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0]
|
||||
|
||||
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}],
|
||||
30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}],
|
||||
34: [{'core_id': 'core_tanoc', 'amount': 15}],
|
||||
23: [{'core_id': 'core_desolate', 'amount': 5}, {'core_id': 'core_serene', 'amount': 25}],
|
||||
81: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
82: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
84: [{'core_id': 'core_maimai', 'amount': 15}],
|
||||
89: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_alluring', 'amount': 25}],
|
||||
71: [],
|
||||
}
|
||||
|
||||
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
|
||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene', 'core_maimai', 'core_alluring']
|
||||
|
||||
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy', 'hailstone', 'furetemitai', 'prayer', 'astralexe', 'trpno', 'blackmirror', 'tau', 'snowwhite3', 'altale3', 'energysynergymatrix3', 'anokumene3', 'nhelv3', 'wontbackdown', 'someday', 'disintegration', 'acheron', 'rostpagegene', 'awakening', 'mod', 'particle', 'paradoxpalette', 'xxium', 'tabootears', 'undefined', 'mask']
|
||||
|
||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_chap8", "scenery_beyond"]
|
||||
|
||||
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
|
||||
# TODO: online_banners
|
||||
|
||||
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']
|
||||
}
|
||||
494
latest version/database/init/courses.json
Normal file
494
latest version/database/init/courses.json
Normal file
@@ -0,0 +1,494 @@
|
||||
[
|
||||
{
|
||||
"course_id": "4.0-dan-1",
|
||||
"style": 1,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "inkarusi"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "snowwhite"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "sakurafubuki"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "purgatorium"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 1",
|
||||
"course_name": "新世界的第一步",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment300",
|
||||
"course_banner_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-2",
|
||||
"style": 2,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "vexaria"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "clotho"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "brandnewworld"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "oblivia"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 2",
|
||||
"course_name": "一次心跳的加速",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment400",
|
||||
"course_banner_2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-3",
|
||||
"style": 3,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "suomi"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "grimheart"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "bookmaker"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "avril"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 3",
|
||||
"course_name": "永不屈服的精神",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment500",
|
||||
"course_banner_3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-4",
|
||||
"style": 4,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "gekka"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "solitarydream"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "kanagawa"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "iconoclast"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 4",
|
||||
"course_name": "光芒的永恒之地",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment600",
|
||||
"course_banner_4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-5",
|
||||
"style": 5,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "faintlight"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "eveninginscarlet"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "kyogenkigo"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "blrink"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 5",
|
||||
"course_name": "玻璃之中的残酷",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment700",
|
||||
"course_banner_5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-6",
|
||||
"style": 6,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "vindication"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "heartjackin"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "ascent"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "macromod"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 6",
|
||||
"course_name": "沉浸于悲喜交加",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment800",
|
||||
"course_banner_6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-7",
|
||||
"style": 7,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "galaxyfriends"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "neokosmo"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "conflict"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "redolentshape"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 7",
|
||||
"course_name": "在命运到来之际",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment900",
|
||||
"course_banner_7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-8",
|
||||
"style": 8,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "corruption"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "neowings"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "valhallazero"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "izana"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 8",
|
||||
"course_name": "趋于混沌的时空",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"course_banner_8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-9",
|
||||
"style": 9,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "felis"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "lightningscrew"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "seclusion"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "livefastdieyoung"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-8",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 9",
|
||||
"course_name": "分崩离析的自我",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"core_generic_10",
|
||||
"course_banner_9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-10",
|
||||
"style": 10,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "cyaegha"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "divinelight"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "dantalion"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "aegleseeker"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-9",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 10",
|
||||
"course_name": "光与对立的洪流",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"core_generic_10",
|
||||
"course_banner_10"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-11",
|
||||
"style": 11,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "infinitestrife"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "grievouslady"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "fractureray"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "tempestissimo"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-10",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 11",
|
||||
"course_name": "创世之曙光",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment2000",
|
||||
"core_generic_20",
|
||||
"course_banner_11"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "6.0-dan-12",
|
||||
"style": 12,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "abstrusedilemma"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "worldender"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 4,
|
||||
"id": "alterego"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "designant"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-11",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 12",
|
||||
"course_name": "侵蚀万物的色彩",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment2000",
|
||||
"core_generic_20",
|
||||
"course_banner_12"
|
||||
]
|
||||
}
|
||||
]
|
||||
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;
|
||||
1082
latest version/database/init/packs.json
Normal file
1082
latest version/database/init/packs.json
Normal file
File diff suppressed because it is too large
Load Diff
2246
latest version/database/init/singles.json
Normal file
2246
latest version/database/init/singles.json
Normal file
File diff suppressed because it is too large
Load Diff
297
latest version/database/init/tables.sql
Normal file
297
latest version/database/init/tables.sql
Normal file
@@ -0,0 +1,297 @@
|
||||
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,
|
||||
kanae_stored_prog real default 0,
|
||||
mp_notification_enabled int default 1,
|
||||
insight_state default 4, -- 这里写 4 是为了避免不能切换状态和无故被侵入
|
||||
custom_banner text
|
||||
);
|
||||
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 default 0,
|
||||
score_v2 real default 0,
|
||||
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,
|
||||
r_index int,
|
||||
time_played int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
score int default 0,
|
||||
shiny_perfect_count int default 0,
|
||||
perfect_count int default 0,
|
||||
near_count int default 0,
|
||||
miss_count int default 0,
|
||||
health int default 0,
|
||||
modifier int default 0,
|
||||
clear_type int default 0,
|
||||
rating real default 0,
|
||||
primary key(user_id, r_index)
|
||||
);
|
||||
create table if not exists user_world(user_id int,
|
||||
map_id text,
|
||||
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,
|
||||
skill_chinatsu_flag text,
|
||||
invasion_flag int
|
||||
);
|
||||
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 default -1,
|
||||
rating_prs int default -1,
|
||||
rating_ftr int default -1,
|
||||
rating_byn int default -1,
|
||||
rating_etr int default -1
|
||||
);
|
||||
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 table if not exists user_mission(
|
||||
user_id int,
|
||||
mission_id text,
|
||||
status int,
|
||||
primary key(user_id, mission_id)
|
||||
);
|
||||
|
||||
-- value 无类型
|
||||
create table if not exists user_kvdata(
|
||||
user_id int,
|
||||
class text,
|
||||
key text,
|
||||
idx int,
|
||||
value,
|
||||
primary key(user_id, class, key, idx)
|
||||
);
|
||||
|
||||
|
||||
create index if not exists best_score_1 on best_score (song_id, difficulty);
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA default_cache_size = 8000;
|
||||
376
latest version/database/map/byd_bookmaker.json
Normal file
376
latest version/database/map/byd_bookmaker.json
Normal file
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "bookmaker2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-650,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 300,
|
||||
"character_affinity": [
|
||||
23,
|
||||
34,
|
||||
47
|
||||
],
|
||||
"affinity_multiplier": [
|
||||
1.3,
|
||||
5,
|
||||
3.8
|
||||
],
|
||||
"step_count": 31,
|
||||
"custom_bg": "",
|
||||
"curr_position": 3,
|
||||
"curr_capture": 35.81730523472207,
|
||||
"is_locked": false,
|
||||
"steps": [
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
],
|
||||
"restrict_ids": [
|
||||
"bookmaker",
|
||||
"viciousheroism",
|
||||
"battlenoone",
|
||||
"galaxyfriends",
|
||||
"rekkaresonance"
|
||||
],
|
||||
"restrict_type": "song_id"
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 11,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 12,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 13,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 14,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 15,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 16,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 17,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 18,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 19,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 20,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 21,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 22,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 23,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 24,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 25,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 26,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 27,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 28,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 29,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 30,
|
||||
"capture": 0,
|
||||
"items": [
|
||||
{
|
||||
"id": "bookmaker3",
|
||||
"type": "world_song"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
80
latest version/database/map/byd_inkarusi.json
Normal file
80
latest version/database/map/byd_inkarusi.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"is_legacy": false,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 2001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "inkarusi2",
|
||||
"require_type": "chart_unlock",
|
||||
"is_breached": true,
|
||||
"stamina_cost": 3,
|
||||
"new_law": "over100_step50",
|
||||
"disable_over": true,
|
||||
"coordinate": "500,0",
|
||||
"beyond_health": 200,
|
||||
"step_count": 5,
|
||||
"custom_bg": "",
|
||||
"is_beyond": true,
|
||||
"requires": [
|
||||
{
|
||||
"type": "chart_unlock",
|
||||
"id": "inkarusi2"
|
||||
}
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"position": 0,
|
||||
"capture": 50
|
||||
},
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"position": 1,
|
||||
"capture": 50,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"position": 2,
|
||||
"capture": 50,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"position": 3,
|
||||
"capture": 50,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_inkarusi",
|
||||
"position": 4,
|
||||
"capture": 0,
|
||||
"items": [
|
||||
{
|
||||
"id": "inkarusi3",
|
||||
"type": "world_song"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
78
latest version/database/map/byd_lostcivilization.json
Normal file
78
latest version/database/map/byd_lostcivilization.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"map_id": "byd_lostcivilization",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "lostcivilization2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-650,-900",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [1.9, 1.9],
|
||||
"step_count": 7,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 0,
|
||||
"capture": 85
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 1,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 2,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 3,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 4,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 5,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 525
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 6,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "lostcivilization3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
114
latest version/database/map/test3.json
Normal file
114
latest version/database/map/test3.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"map_id": "test3",
|
||||
"is_legacy": true,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "300,200",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 1,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"items": [{
|
||||
"type": "memory",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"],
|
||||
"items": [{
|
||||
"id": "supernova",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 60,
|
||||
"items": [{
|
||||
"id": "scenery_chap4",
|
||||
"type": "world_unlock"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1250
|
||||
}]
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "memory",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [{"id": "2", "type": "character"}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 50
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
[{
|
||||
"name": "core",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "core",
|
||||
"_id": "6046bab736651a07ebc537db",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 400,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "shiawase",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "shiawase",
|
||||
"_id": "6046bab736651a07ebc53823",
|
||||
"is_available": true
|
||||
}, {
|
||||
"type": "character",
|
||||
"id": "kou",
|
||||
"_id": "6046bab736651a07ebc53822",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "dynamix",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "dynamix",
|
||||
"_id": "6046bab736651a07ebc53827",
|
||||
"is_available": true
|
||||
}, {
|
||||
"type": "character",
|
||||
"id": "sapphire",
|
||||
"_id": "6046bab736651a07ebc53826",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "mirai",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "mirai",
|
||||
"_id": "6046bab736651a07ebc5381d",
|
||||
"is_available": true
|
||||
}, {
|
||||
"type": "character",
|
||||
"id": "lethe",
|
||||
"_id": "6046bab736651a07ebc5381c",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "yugamu",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "yugamu",
|
||||
"_id": "6046bab736651a07ebc537dc",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 400,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "lanota",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "lanota",
|
||||
"_id": "6046bab736651a07ebc537e8",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "nijuusei",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "nijuusei",
|
||||
"_id": "6046bab736651a07ebc537dd",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "rei",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "rei",
|
||||
"_id": "6046bab736651a07ebc537f6",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 400,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "tonesphere",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "tonesphere",
|
||||
"_id": "6046bab736651a07ebc537ea",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "groovecoaster",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "groovecoaster",
|
||||
"_id": "6046bab736651a07ebc53812",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "zettai",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "zettai",
|
||||
"_id": "6046bab736651a07ebc537f8",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "chunithm",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "chunithm",
|
||||
"_id": "6046bab736651a07ebc53806",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 300,
|
||||
"orig_price": 300
|
||||
}, {
|
||||
"name": "prelude",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "prelude",
|
||||
"_id": "6046bab736651a07ebc53813",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 400,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "omatsuri",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "omatsuri",
|
||||
"_id": "6046bab736651a07ebc537f9",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 250,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "vs",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "vs",
|
||||
"_id": "6046bab736651a07ebc53809",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 400,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "extend",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "extend",
|
||||
"_id": "6046bab736651a07ebc53816",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 500,
|
||||
"orig_price": 700,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "alice",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "alice",
|
||||
"_id": "6046bab736651a07ebc537fe",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
}, {
|
||||
"name": "alice_append_1",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "alice_append_1",
|
||||
"_id": "6046bab736651a07ebc537e6",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
}, {
|
||||
"name": "ongeki",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "ongeki",
|
||||
"_id": "6046bab736651a07ebc5380c",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
}, {
|
||||
"name": "maimai",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "maimai",
|
||||
"_id": "6046bab736651a07ebc53819",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
}, {
|
||||
"name": "chunithm_append_1",
|
||||
"items": [{
|
||||
"type": "pack",
|
||||
"id": "chunithm_append_1",
|
||||
"_id": "6046bab736651a07ebc537f2",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
}]
|
||||
@@ -1,510 +0,0 @@
|
||||
[{
|
||||
"name": "testsingle",
|
||||
"items": [{
|
||||
"id": "testsingle",
|
||||
"type": "single",
|
||||
"is_available": false
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "dataerror",
|
||||
"items": [{
|
||||
"id": "dataerror",
|
||||
"type": "single",
|
||||
"_id": "6046bab736651a07ebc537f3",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "yourvoiceso",
|
||||
"items": [{
|
||||
"id": "yourvoiceso",
|
||||
"type": "single",
|
||||
"_id": "6046bab736651a07ebc53800",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "crosssoul",
|
||||
"items": [{
|
||||
"id": "crosssoul",
|
||||
"type": "single",
|
||||
"_id": "6046bab736651a07ebc5380d",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "impurebird",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "impurebird",
|
||||
"_id": "6046bab736651a07ebc53801",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "auxesia",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "auxesia",
|
||||
"_id": "6046bab736651a07ebc5380e",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1520413239000,
|
||||
"discount_to": 1521072000000
|
||||
}, {
|
||||
"name": "modelista",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "modelista",
|
||||
"_id": "6046bab736651a07ebc537f4",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "yozakurafubuki",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "yozakurafubuki",
|
||||
"_id": "6046bab736651a07ebc53802",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "surrender",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "surrender",
|
||||
"_id": "6046bab736651a07ebc5380f",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "metallicpunisher",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "metallicpunisher",
|
||||
"_id": "6046bab736651a07ebc537f5",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "carminescythe",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "carminescythe",
|
||||
"_id": "6046bab736651a07ebc53803",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "bethere",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "bethere",
|
||||
"_id": "6046bab736651a07ebc53810",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "callmyname",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "callmyname",
|
||||
"_id": "6046bab736651a07ebc537e9",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "fallensquare",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "fallensquare",
|
||||
"_id": "6046bab736651a07ebc537de",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "dropdead",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "dropdead",
|
||||
"_id": "6046bab736651a07ebc53804",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "alexandrite",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "alexandrite",
|
||||
"_id": "6046bab736651a07ebc53811",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "astraltale",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "astraltale",
|
||||
"_id": "6046bab736651a07ebc537f7",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "phantasia",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "phantasia",
|
||||
"_id": "6046bab736651a07ebc537df",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "empireofwinter",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "empireofwinter",
|
||||
"_id": "6046bab736651a07ebc53805",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "libertas",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "libertas",
|
||||
"_id": "6046bab736651a07ebc537eb",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "dottodot",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "dottodot",
|
||||
"_id": "6046bab736651a07ebc537e0",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "dreadnought",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "dreadnought",
|
||||
"_id": "6046bab736651a07ebc537ec",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "mirzam",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "mirzam",
|
||||
"_id": "6046bab736651a07ebc537e1",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "heavenlycaress",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "heavenlycaress",
|
||||
"_id": "6046bab736651a07ebc53807",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "filament",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "filament",
|
||||
"_id": "6046bab736651a07ebc53814",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "avantraze",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "avantraze",
|
||||
"_id": "6046bab736651a07ebc537ed",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "battlenoone",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "battlenoone",
|
||||
"_id": "6046bab736651a07ebc537fa",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "saikyostronger",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "saikyostronger",
|
||||
"_id": "6046bab736651a07ebc537e2",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "izana",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "izana",
|
||||
"_id": "6046bab736651a07ebc53808",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "einherjar",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "einherjar",
|
||||
"_id": "6046bab736651a07ebc53815",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "laqryma",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "laqryma",
|
||||
"_id": "6046bab736651a07ebc537ee",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "amygdata",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "amygdata",
|
||||
"_id": "6046bab736651a07ebc537fb",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "altale",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "altale",
|
||||
"_id": "6046bab736651a07ebc537e3",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "feelssoright",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "feelssoright",
|
||||
"_id": "6046bab736651a07ebc537ef",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "scarletcage",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "scarletcage",
|
||||
"_id": "6046bab736651a07ebc537fc",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "teriqma",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "teriqma",
|
||||
"_id": "6046bab736651a07ebc537e4",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "mahoroba",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "mahoroba",
|
||||
"_id": "6046bab736651a07ebc5380a",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "badtek",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "badtek",
|
||||
"_id": "6046bab736651a07ebc537f0",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "maliciousmischance",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "maliciousmischance",
|
||||
"_id": "6046bab736651a07ebc53817",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "buchigireberserker",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "buchigireberserker",
|
||||
"_id": "6046bab736651a07ebc537e5",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "galaxyfriends",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "galaxyfriends",
|
||||
"_id": "6046bab736651a07ebc5380b",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 50,
|
||||
"orig_price": 100,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
}, {
|
||||
"name": "xeraphinite",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "xeraphinite",
|
||||
"_id": "6046bab736651a07ebc53818",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
}, {
|
||||
"name": "xanatos",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "xanatos",
|
||||
"_id": "6046bab736651a07ebc537f1",
|
||||
"is_available": true
|
||||
}],
|
||||
"price": 100,
|
||||
"orig_price": 100
|
||||
}, {
|
||||
"name": "attraqtia",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "attraqtia",
|
||||
"_id": "6046bab736651a07ebc537ff",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
}, {
|
||||
"name": "gimmedablood",
|
||||
"items": [{
|
||||
"type": "single",
|
||||
"id": "gimmedablood",
|
||||
"_id": "6046bab736651a07ebc537e7",
|
||||
"is_available": true
|
||||
}],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
}]
|
||||
27
latest version/database/songs/songlist
Normal file
27
latest version/database/songs/songlist
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"songs": [
|
||||
{
|
||||
"id": "dement",
|
||||
"set": "base",
|
||||
"purchase": "",
|
||||
"difficulties": [
|
||||
{
|
||||
"ratingClass": 0,
|
||||
"rating": 3
|
||||
},
|
||||
{
|
||||
"ratingClass": 1,
|
||||
"rating": 6
|
||||
},
|
||||
{
|
||||
"ratingClass": 2,
|
||||
"rating": 7
|
||||
},
|
||||
{
|
||||
"ratingClass": 3,
|
||||
"rating": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
latest version/linkplay_server/__init__.py
Normal file
1
latest version/linkplay_server/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .main import link_play
|
||||
24
latest version/linkplay_server/aes.py
Normal file
24
latest version/linkplay_server/aes.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from os import urandom
|
||||
from cryptography.hazmat.primitives.ciphers import (
|
||||
Cipher, algorithms, modes
|
||||
)
|
||||
|
||||
|
||||
def encrypt(key, plaintext, associated_data):
|
||||
iv = urandom(12)
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, min_tag_length=16),
|
||||
).encryptor()
|
||||
encryptor.authenticate_additional_data(associated_data)
|
||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return (iv, ciphertext, encryptor.tag)
|
||||
|
||||
|
||||
def decrypt(key, associated_data, iv, ciphertext, tag):
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, tag, min_tag_length=16),
|
||||
).decryptor()
|
||||
decryptor.authenticate_additional_data(associated_data)
|
||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||
39
latest version/linkplay_server/config.py
Normal file
39
latest version/linkplay_server/config.py
Normal file
@@ -0,0 +1,39 @@
|
||||
class Config:
|
||||
'''
|
||||
Link Play server configuration
|
||||
'''
|
||||
|
||||
'''
|
||||
服务器地址、端口号、校验码、传输加密密钥
|
||||
Server address, port, verification code, and encryption key
|
||||
'''
|
||||
HOST = '0.0.0.0'
|
||||
UDP_PORT = 10900
|
||||
TCP_PORT = 10901
|
||||
AUTHENTICATION = 'my_link_play_server'
|
||||
TCP_SECRET_KEY = '1145141919810'
|
||||
'''
|
||||
--------------------------------------------------
|
||||
'''
|
||||
|
||||
DEBUG = False
|
||||
|
||||
TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||
|
||||
TIME_LIMIT = 3600000
|
||||
|
||||
COMMAND_INTERVAL = 1000000
|
||||
|
||||
PLAYER_PRE_TIMEOUT = 3000000
|
||||
PLAYER_TIMEOUT = 15000000
|
||||
|
||||
LINK_PLAY_UNLOCK_LENGTH = 1024
|
||||
|
||||
COUNTDOWN_SONG_READY = 4 * 1000000
|
||||
COUNTDOWN_SONG_START = 6 * 1000000
|
||||
|
||||
# 计时模式
|
||||
COUNTDOWN_MATCHING = 15 * 1000000
|
||||
COUNTDOWN_SELECT_SONG = 45 * 1000000
|
||||
COUNTDOWN_SELECT_DIFFICULTY = 45 * 1000000
|
||||
COUNTDOWN_RESULT = 60 * 1000000
|
||||
114
latest version/linkplay_server/main.py
Normal file
114
latest version/linkplay_server/main.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import binascii
|
||||
import logging
|
||||
import socketserver
|
||||
import threading
|
||||
from json import dumps, loads
|
||||
|
||||
from .aes import decrypt, encrypt
|
||||
from .config import Config
|
||||
from .store import Store, TCPRouter, clear_player, clear_room
|
||||
from .udp_class import bi
|
||||
from .udp_parser import CommandParser
|
||||
|
||||
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
|
||||
class UDP_handler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
client_msg, server = self.request
|
||||
# print(client_msg)
|
||||
try:
|
||||
token = client_msg[:8]
|
||||
iv = client_msg[8:20]
|
||||
tag = client_msg[20:36]
|
||||
ciphertext = client_msg[36:]
|
||||
|
||||
user = Store.link_play_data.get(bi(token))
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
|
||||
# if Config.DEBUG:
|
||||
# logging.info(
|
||||
# f'UDP-From-{self.client_address[0]}-{binascii.b2a_hex(plaintext)}')
|
||||
|
||||
commands = CommandParser(
|
||||
user['room'], user['player_index']).get_commands(plaintext)
|
||||
|
||||
if user['room'].players[user['player_index']].player_id == 0:
|
||||
clear_player(bi(token))
|
||||
if user['room'].player_num == 0:
|
||||
clear_room(user['room'])
|
||||
commands = [i for i in commands if i[2] == 0x12]
|
||||
# 处理不能正确被踢的问题
|
||||
|
||||
for i in commands:
|
||||
iv, ciphertext, tag = encrypt(user['key'], i, b'')
|
||||
# if Config.DEBUG:
|
||||
# logging.info(
|
||||
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
||||
|
||||
server.sendto(token + iv + tag + ciphertext, self.client_address)
|
||||
|
||||
|
||||
AUTH_LEN = len(Config.AUTHENTICATION)
|
||||
TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16]
|
||||
|
||||
|
||||
class TCP_handler(socketserver.StreamRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
|
||||
self.wfile.write(b'No authentication')
|
||||
logging.warning(
|
||||
f'TCP-{self.client_address[0]}-No authentication')
|
||||
return None
|
||||
|
||||
cipher_len = int.from_bytes(self.rfile.read(8), byteorder='little')
|
||||
if cipher_len > Config.TCP_MAX_LENGTH:
|
||||
self.wfile.write(b'Body too long')
|
||||
logging.warning(f'TCP-{self.client_address[0]}-Body too long')
|
||||
return None
|
||||
|
||||
iv = self.rfile.read(12)
|
||||
tag = self.rfile.read(16)
|
||||
ciphertext = self.rfile.read(cipher_len)
|
||||
|
||||
self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||
message = self.data.decode('utf-8')
|
||||
data = loads(message)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
|
||||
if Config.DEBUG:
|
||||
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
|
||||
|
||||
r = TCPRouter(data).handle()
|
||||
try:
|
||||
r = dumps(r)
|
||||
if Config.DEBUG:
|
||||
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
|
||||
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
|
||||
r = len(ciphertext).to_bytes(
|
||||
8, byteorder='little') + iv + tag + ciphertext
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
self.wfile.write(r)
|
||||
|
||||
|
||||
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):
|
||||
udp_server = socketserver.ThreadingUDPServer((ip, udp_port), UDP_handler)
|
||||
tcp_server = socketserver.ThreadingTCPServer((ip, tcp_port), TCP_handler)
|
||||
|
||||
threads = [threading.Thread(target=udp_server.serve_forever), threading.Thread(
|
||||
target=tcp_server.serve_forever)]
|
||||
[t.start() for t in threads]
|
||||
[t.join() for t in threads]
|
||||
410
latest version/linkplay_server/store.py
Normal file
410
latest version/linkplay_server/store.py
Normal file
@@ -0,0 +1,410 @@
|
||||
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
|
||||
from .udp_sender import CommandSender
|
||||
|
||||
|
||||
class Store:
|
||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||
link_play_data = {}
|
||||
room_id_dict: "dict[int, Room]" = {} # 'room_id': Room
|
||||
room_code_dict = {} # 'room_code': Room
|
||||
player_dict = {} # 'player_id' : Player
|
||||
|
||||
share_token_dict = {} # 'share_token': Room
|
||||
|
||||
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 random_share_token():
|
||||
CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
re = ''
|
||||
for _ in range(10):
|
||||
re += CHARSET[randint(0, 35)]
|
||||
return re
|
||||
|
||||
|
||||
def unique_random(dataset, length=8, random_func=None):
|
||||
'''无重复随机,且默认非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}`')
|
||||
with Store.lock:
|
||||
if player_id in Store.player_dict:
|
||||
del Store.player_dict[player_id]
|
||||
if token in Store.link_play_data:
|
||||
del Store.link_play_data[token]
|
||||
|
||||
|
||||
def clear_room(room):
|
||||
# 清除房间信息
|
||||
room_id = room.room_id
|
||||
room_code = room.room_code
|
||||
share_token = room.share_token
|
||||
logging.info(f'Clean room `{room_code}`')
|
||||
with Store.lock:
|
||||
if room_id in Store.room_id_dict:
|
||||
del Store.room_id_dict[room_id]
|
||||
if room_code in Store.room_code_dict:
|
||||
del Store.room_code_dict[room_code]
|
||||
if share_token in Store.share_token_dict:
|
||||
del Store.share_token_dict[share_token]
|
||||
del room
|
||||
|
||||
|
||||
def memory_clean(now):
|
||||
# 内存清理,应对玩家不正常退出
|
||||
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',
|
||||
'select_room',
|
||||
'get_match_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 {'code': 999}
|
||||
try:
|
||||
r = getattr(self, self.endpoint)()
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return {'code': 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() * 1000000)
|
||||
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
|
||||
|
||||
share_token = unique_random(
|
||||
Store.share_token_dict, random_func=random_share_token)
|
||||
room.share_token = share_token
|
||||
Store.share_token_dict[share_token] = room
|
||||
|
||||
return room
|
||||
|
||||
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'])
|
||||
rating_ptt = self.data.get('rating_ptt', 0)
|
||||
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||
match_times = self.data.get('match_times', None)
|
||||
|
||||
key = urandom(16)
|
||||
with Store.lock:
|
||||
room = self.generate_room()
|
||||
player = self.generate_player(name)
|
||||
|
||||
player.song_unlock = song_unlock
|
||||
player.rating_ptt = rating_ptt
|
||||
player.is_hide_rating = is_hide_rating
|
||||
player.player_index = 0
|
||||
room.song_unlock = song_unlock
|
||||
room.host_id = player.player_id
|
||||
room.players[0] = player
|
||||
|
||||
token = room.room_id
|
||||
player.token = token
|
||||
|
||||
# 匹配模式追加
|
||||
if match_times is not None:
|
||||
room.is_public = 1
|
||||
room.round_mode = 3
|
||||
room.timed_mode = 1
|
||||
|
||||
Store.link_play_data[token] = {
|
||||
'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'])
|
||||
rating_ptt = self.data.get('rating_ptt', 0)
|
||||
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||
match_times = self.data.get('match_times', None)
|
||||
|
||||
with Store.lock:
|
||||
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 not in (0, 1, 2) or (room.is_public and match_times is None):
|
||||
# 无法加入
|
||||
return 1205
|
||||
|
||||
token = unique_random(Store.link_play_data)
|
||||
|
||||
player = self.generate_player(name)
|
||||
player.token = token
|
||||
player.song_unlock = song_unlock
|
||||
player.rating_ptt = rating_ptt
|
||||
player.is_hide_rating = is_hide_rating
|
||||
room.update_song_unlock()
|
||||
for i in range(4):
|
||||
if room.players[i].player_id == 0:
|
||||
room.players[i] = player
|
||||
player.player_index = i
|
||||
break
|
||||
Store.link_play_data[token] = {
|
||||
'key': key,
|
||||
'room': room,
|
||||
'player_index': player.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'])
|
||||
rating_ptt = self.data.get('rating_ptt', 0)
|
||||
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||
|
||||
with Store.lock:
|
||||
if token not in Store.link_play_data:
|
||||
return 108
|
||||
r = Store.link_play_data[token]
|
||||
room = r['room']
|
||||
|
||||
# 更新玩家信息
|
||||
player_index = r['player_index']
|
||||
player = room.players[player_index]
|
||||
player.rating_ptt = rating_ptt
|
||||
player.is_hide_rating = is_hide_rating
|
||||
cs = CommandSender(room)
|
||||
room.command_queue.append(cs.command_12(player_index))
|
||||
|
||||
logging.info(f'TCP-Room `{room.room_code}` info update')
|
||||
return {
|
||||
'room_code': room.room_code,
|
||||
'room_id': room.room_id,
|
||||
'key': b64encode(r['key']).decode('utf-8'),
|
||||
# changed from room.players[r['player_index']].player_id,
|
||||
'player_id': r['player_id'],
|
||||
'song_unlock': b64encode(room.song_unlock).decode('utf-8')
|
||||
}
|
||||
|
||||
def get_rooms(self) -> dict:
|
||||
# 获取房间列表与详细信息
|
||||
|
||||
offset = int(self.data.get('offset', 0))
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
limit = min(int(self.data.get('limit', 100)), 100)
|
||||
if limit < 0:
|
||||
limit = 100
|
||||
|
||||
n = 0
|
||||
m = 0
|
||||
rooms = []
|
||||
f = False
|
||||
f2 = False
|
||||
for room in Store.room_id_dict.values():
|
||||
if room.player_num == 0:
|
||||
continue
|
||||
if m < offset:
|
||||
m += 1
|
||||
continue
|
||||
if f:
|
||||
# 处理刚好有 limit 个房间的情况
|
||||
f2 = True
|
||||
break
|
||||
n += 1
|
||||
rooms.append(room.to_dict())
|
||||
if n >= limit:
|
||||
f = True
|
||||
|
||||
return {
|
||||
'amount': n,
|
||||
'offset': offset,
|
||||
'limit': limit,
|
||||
'has_more': f2,
|
||||
'rooms': rooms
|
||||
}
|
||||
|
||||
def select_room(self) -> dict:
|
||||
# 查询房间信息
|
||||
|
||||
room_code = self.data.get('room_code', None)
|
||||
share_token = self.data.get('share_token', None)
|
||||
|
||||
if room_code is not None:
|
||||
room = Store.room_code_dict.get(room_code, None)
|
||||
elif share_token is not None:
|
||||
room = Store.share_token_dict.get(share_token, None)
|
||||
if room is None:
|
||||
return 108
|
||||
|
||||
return {
|
||||
'room_id': room.room_id,
|
||||
'room_code': room.room_code,
|
||||
'share_token': room.share_token,
|
||||
'is_enterable': room.is_enterable,
|
||||
'is_matchable': room.is_matchable,
|
||||
'is_playing': room.is_playing,
|
||||
'is_public': room.is_public == 1,
|
||||
'timed_mode': room.timed_mode == 1,
|
||||
}
|
||||
|
||||
def get_match_rooms(self):
|
||||
n = 0
|
||||
rooms = []
|
||||
|
||||
for room in Store.room_id_dict.values():
|
||||
if not room.is_matchable:
|
||||
continue
|
||||
|
||||
rooms.append({
|
||||
'room_id': room.room_id,
|
||||
'room_code': room.room_code,
|
||||
'share_token': room.share_token,
|
||||
'is_matchable': room.is_matchable,
|
||||
'next_state_timestamp': room.next_state_timestamp,
|
||||
'song_unlock': b64encode(room.song_unlock).decode('utf-8'),
|
||||
'players': [{
|
||||
'player_id': i.player_id,
|
||||
'name': i.name,
|
||||
'rating_ptt': i.rating_ptt
|
||||
} for i in room.players]
|
||||
})
|
||||
if n >= 100:
|
||||
break
|
||||
return {
|
||||
'amount': n,
|
||||
'rooms': rooms
|
||||
}
|
||||
508
latest version/linkplay_server/udp_class.py
Normal file
508
latest version/linkplay_server/udp_class.py
Normal file
@@ -0,0 +1,508 @@
|
||||
import logging
|
||||
from time import time
|
||||
from random import randint
|
||||
|
||||
from .config import Config
|
||||
|
||||
|
||||
def b(value, length=1):
|
||||
return value.to_bytes(length=length, byteorder='little')
|
||||
|
||||
|
||||
def bi(value):
|
||||
return int.from_bytes(value, byteorder='little')
|
||||
|
||||
|
||||
class Score:
|
||||
def __init__(self) -> None:
|
||||
self.difficulty = 0xff
|
||||
|
||||
self.score = 0
|
||||
self.cleartype = 0
|
||||
self.timer = 0
|
||||
|
||||
self.best_score_flag = 0 # personal best
|
||||
self.best_player_flag = 0 # high score
|
||||
|
||||
# 5.10 新增
|
||||
self.shiny_perfect_count = 0 # 2 bytes
|
||||
self.perfect_count = 0 # 2 bytes
|
||||
self.near_count = 0 # 2 bytes
|
||||
self.miss_count = 0 # 2 bytes
|
||||
self.early_count = 0 # 2 bytes
|
||||
self.late_count = 0 # 2 bytes
|
||||
|
||||
self.healthy = 0 # 4 bytes signed? 不确定,但似乎没影响
|
||||
|
||||
def copy(self, x: 'Score'):
|
||||
self.difficulty = x.difficulty
|
||||
self.score = x.score
|
||||
self.cleartype = x.cleartype
|
||||
self.timer = x.timer
|
||||
self.best_score_flag = x.best_score_flag
|
||||
self.best_player_flag = x.best_player_flag
|
||||
self.shiny_perfect_count = x.shiny_perfect_count
|
||||
self.perfect_count = x.perfect_count
|
||||
self.near_count = x.near_count
|
||||
self.miss_count = x.miss_count
|
||||
self.early_count = x.early_count
|
||||
self.late_count = x.late_count
|
||||
self.healthy = x.healthy
|
||||
|
||||
def clear(self):
|
||||
self.difficulty = 0xff
|
||||
self.score = 0
|
||||
self.cleartype = 0
|
||||
self.timer = 0
|
||||
self.best_score_flag = 0
|
||||
self.best_player_flag = 0
|
||||
self.shiny_perfect_count = 0
|
||||
self.perfect_count = 0
|
||||
self.near_count = 0
|
||||
self.miss_count = 0
|
||||
self.early_count = 0
|
||||
self.late_count = 0
|
||||
self.healthy = 0
|
||||
|
||||
def __str__(self):
|
||||
return f'Score: {self.score}, Cleartype: {self.cleartype}, Difficulty: {self.difficulty}, Timer: {self.timer}, Best Score Flag: {self.best_score_flag}, Best Player Flag: {self.best_player_flag}, Shiny Perfect: {self.shiny_perfect_count}, Perfect: {self.perfect_count}, Near: {self.near_count}, Miss: {self.miss_count}, Early: {self.early_count}, Late: {self.late_count}, Healthy: {self.healthy}'
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, player_index: int = 0) -> None:
|
||||
self.player_id = 0
|
||||
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
||||
self.token = 0
|
||||
|
||||
self.character_id = 0xff
|
||||
self.is_uncapped = 0
|
||||
|
||||
self.score = Score()
|
||||
self.last_score = Score()
|
||||
|
||||
self.finish_flag = 0
|
||||
|
||||
self.player_state = 1
|
||||
self.download_percent = 0
|
||||
self.online = 0
|
||||
|
||||
self.last_timestamp = 0
|
||||
self.extra_command_queue = []
|
||||
|
||||
self.song_unlock: bytes = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.start_command_num = 0
|
||||
|
||||
# 5.10 新增
|
||||
|
||||
self.voting: int = 0x8000 # 2 bytes, song_idx, 0xffff 为不选择,0x8000 为默认值
|
||||
self.player_index: int = player_index # 1 byte 不确定对不对
|
||||
self.switch_2: int = 0 # 1 byte
|
||||
|
||||
self.rating_ptt: int = 0 # 2 bytes
|
||||
self.is_hide_rating: int = 0 # 1 byte
|
||||
self.is_staff: int = 0 # 1 byte
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.player_name.decode('ascii').rstrip('\x00')
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'multiplay_player_id': self.player_id,
|
||||
'name': self.name,
|
||||
'is_online': self.online == 1,
|
||||
'character_id': self.character_id,
|
||||
'is_uncapped': self.is_uncapped == 1,
|
||||
'rating_ptt': self.rating_ptt,
|
||||
'is_hide_rating': self.is_hide_rating == 1,
|
||||
'last_song': {
|
||||
'difficulty': self.last_score.difficulty,
|
||||
'score': self.last_score.score,
|
||||
'cleartype': self.last_score.cleartype,
|
||||
'shine_perfect': self.last_score.shiny_perfect_count,
|
||||
'perfect': self.last_score.perfect_count,
|
||||
'near': self.last_score.near_count,
|
||||
'miss': self.last_score.miss_count,
|
||||
'early': self.last_score.early_count,
|
||||
'late': self.last_score.late_count,
|
||||
},
|
||||
'song': {
|
||||
'difficulty': self.score.difficulty,
|
||||
'score': self.score.score,
|
||||
'cleartype': self.score.cleartype,
|
||||
},
|
||||
'player_state': self.player_state,
|
||||
'last_timestamp': self.last_timestamp,
|
||||
}
|
||||
|
||||
def set_player_name(self, player_name: str):
|
||||
self.player_name = player_name.encode('ascii')
|
||||
if len(self.player_name) > 16:
|
||||
self.player_name = self.player_name[:16]
|
||||
else:
|
||||
self.player_name += b'\x00' * (16 - len(self.player_name))
|
||||
|
||||
@property
|
||||
def info(self) -> bytes:
|
||||
re = bytearray()
|
||||
re.extend(b(self.player_id, 8))
|
||||
re.append(self.character_id)
|
||||
re.append(self.is_uncapped)
|
||||
re.append(self.score.difficulty)
|
||||
re.extend(b(self.score.score, 4))
|
||||
re.extend(b(self.score.timer, 4))
|
||||
re.append(self.score.cleartype)
|
||||
re.append(self.player_state)
|
||||
re.append(self.download_percent)
|
||||
re.append(self.online)
|
||||
|
||||
re.extend(b(self.voting, 2))
|
||||
re.append(self.player_index)
|
||||
re.append(self.switch_2)
|
||||
re.extend(b(self.rating_ptt, 2))
|
||||
re.append(self.is_hide_rating)
|
||||
re.append(self.is_staff)
|
||||
|
||||
return bytes(re)
|
||||
|
||||
@property
|
||||
def last_score_info(self) -> bytes:
|
||||
if self.player_id == 0:
|
||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
x = self.last_score
|
||||
re = bytearray()
|
||||
re.append(self.character_id)
|
||||
re.append(x.difficulty)
|
||||
re.extend(b(x.score, 4))
|
||||
re.append(x.cleartype)
|
||||
re.append(x.best_score_flag)
|
||||
re.append(x.best_player_flag)
|
||||
re.extend(b(x.shiny_perfect_count, 2))
|
||||
re.extend(b(x.perfect_count, 2))
|
||||
re.extend(b(x.near_count, 2))
|
||||
re.extend(b(x.miss_count, 2))
|
||||
re.extend(b(x.early_count, 2))
|
||||
re.extend(b(x.late_count, 2))
|
||||
re.extend(b(x.healthy, 4))
|
||||
|
||||
return bytes(re)
|
||||
|
||||
|
||||
class Room:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.room_id = 0
|
||||
self.room_code = 'AAAA00'
|
||||
self.share_token = 'abcde12345' # 5.10 新增
|
||||
|
||||
self.countdown = 0xffffffff
|
||||
self.timestamp = 0
|
||||
self._state = 0
|
||||
self.song_idx = 0xffff # 疑似 idx * 5
|
||||
self.last_song_idx = 0xffff # 疑似 idx * 5
|
||||
|
||||
self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.host_id = 0
|
||||
self.players = [Player(0), Player(1), Player(2), Player(3)]
|
||||
|
||||
self.interval = 1000
|
||||
self.times = 100 # ???
|
||||
|
||||
self.round_mode: int = 1 # 5.10 从 bool 修改为 int 1~3
|
||||
self.is_public = 0 # 5.10 新增
|
||||
self.timed_mode = 0 # 5.10 新增
|
||||
|
||||
self.selected_voter_player_id: int = 0 # 5.10 新增
|
||||
|
||||
self.command_queue = []
|
||||
|
||||
self.next_state_timestamp = 0 # 计时模式下一个状态时间
|
||||
|
||||
@property
|
||||
def state(self) -> int:
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, value: int):
|
||||
self._state = value
|
||||
self.countdown = 0xffffffff
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
p = [i.to_dict() for i in self.players if i.player_id != 0]
|
||||
for i in p:
|
||||
i['is_host'] = i['multiplay_player_id'] == self.host_id
|
||||
return {
|
||||
'room_id': self.room_id,
|
||||
'room_code': self.room_code,
|
||||
'share_token': self.share_token,
|
||||
'state': self.state,
|
||||
'song_idx': self.song_idx,
|
||||
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
|
||||
'host_id': self.host_id,
|
||||
'players': p,
|
||||
'round_mode': self.round_mode,
|
||||
'last_timestamp': self.timestamp,
|
||||
'is_enterable': self.is_enterable,
|
||||
'is_matchable': self.is_matchable,
|
||||
'is_playing': self.is_playing,
|
||||
'is_public': self.is_public == 1,
|
||||
'timed_mode': self.timed_mode == 1,
|
||||
}
|
||||
|
||||
@property
|
||||
def room_info(self) -> bytes:
|
||||
re = bytearray()
|
||||
re.extend(b(self.host_id, 8))
|
||||
re.append(self.state)
|
||||
re.extend(b(self.countdown, 4))
|
||||
re.extend(b(self.timestamp, 8))
|
||||
re.extend(b(self.song_idx, 2))
|
||||
re.extend(b(self.interval, 2))
|
||||
re.extend(b(self.times, 7))
|
||||
re.extend(self.get_player_last_score())
|
||||
re.extend(b(self.last_song_idx, 2))
|
||||
re.append(self.round_mode)
|
||||
re.append(self.is_public)
|
||||
re.append(self.timed_mode)
|
||||
re.extend(b(self.selected_voter_player_id, 8))
|
||||
return bytes(re)
|
||||
|
||||
@property
|
||||
def is_enterable(self) -> bool:
|
||||
return 0 < self.player_num < 4 and self.state == 2
|
||||
|
||||
@property
|
||||
def is_matchable(self) -> bool:
|
||||
return self.is_public and 0 < self.player_num < 4 and self.state == 1
|
||||
|
||||
@property
|
||||
def is_playing(self) -> bool:
|
||||
return self.state in (4, 5, 6, 7)
|
||||
|
||||
@property
|
||||
def command_queue_length(self) -> int:
|
||||
return len(self.command_queue)
|
||||
|
||||
@property
|
||||
def player_num(self) -> int:
|
||||
now = round(time() * 1000000)
|
||||
if now - self.timestamp >= 1000000:
|
||||
self.check_player_online(now)
|
||||
return sum(i.player_id != 0 for i in self.players)
|
||||
|
||||
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):
|
||||
# 获取所有玩家信息
|
||||
re = bytearray()
|
||||
for i in self.players:
|
||||
re.extend(i.info)
|
||||
re.append(0)
|
||||
re.extend(i.player_name)
|
||||
return bytes(re)
|
||||
|
||||
def get_player_last_score(self):
|
||||
# 获取上次曲目玩家分数,返回bytes
|
||||
if self.last_song_idx == 0xffff:
|
||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' * 4
|
||||
return b''.join(i.last_score_info for i in self.players)
|
||||
|
||||
def make_round(self):
|
||||
# 轮换房主
|
||||
for i in range(4):
|
||||
if self.players[i].player_id == self.host_id:
|
||||
for j in range(1, 4):
|
||||
player = self.players[(i + j) % 4]
|
||||
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
|
||||
|
||||
def delete_player(self, player_index: int):
|
||||
# 删除某个玩家
|
||||
player = self.players[player_index]
|
||||
if player.player_id == self.host_id:
|
||||
self.make_round()
|
||||
|
||||
logging.info(
|
||||
f'Player `{player.name}` leaves room `{self.room_code}`')
|
||||
|
||||
self.players[player_index].online = 0
|
||||
self.players[player_index] = Player(player_index)
|
||||
self.update_song_unlock()
|
||||
|
||||
if self.state in (2, 3):
|
||||
self.state = 1
|
||||
self.song_idx = 0xffff
|
||||
self.voting_clear()
|
||||
|
||||
if self.state in (1, 2) and self.timed_mode and self.player_num <= 1:
|
||||
self.next_state_timestamp = 0
|
||||
self.countdown = 0xffffffff
|
||||
|
||||
def update_song_unlock(self):
|
||||
# 更新房间可用歌曲
|
||||
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
for i in self.players:
|
||||
if i.player_id != 0:
|
||||
r &= bi(i.song_unlock)
|
||||
|
||||
self.song_unlock = b(r, Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
|
||||
def is_ready(self, old_state: int, player_state: int):
|
||||
# 是否全部准备就绪
|
||||
if self.state == old_state:
|
||||
for i in self.players:
|
||||
if i.player_id != 0 and (i.player_state != player_state or i.online == 0):
|
||||
return False
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_finish(self):
|
||||
# 是否全部进入结算
|
||||
if self.state != 7:
|
||||
return False
|
||||
|
||||
for i in self.players:
|
||||
if i.player_id != 0 and (i.finish_flag == 0 or i.online == 0):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def make_finish(self):
|
||||
# 结算
|
||||
self.state = 8
|
||||
self.last_song_idx = self.song_idx
|
||||
|
||||
max_score = 0
|
||||
max_score_i = []
|
||||
for i in range(4):
|
||||
player = self.players[i]
|
||||
if player.player_id == 0:
|
||||
continue
|
||||
player.finish_flag = 0
|
||||
player.last_score.copy(player.score)
|
||||
player.last_score.best_player_flag = 0
|
||||
|
||||
if player.last_score.score > max_score:
|
||||
max_score = player.last_score.score
|
||||
max_score_i = [i]
|
||||
elif player.last_score.score == max_score:
|
||||
max_score_i.append(i)
|
||||
|
||||
for i in max_score_i:
|
||||
self.players[i].last_score.best_player_flag = 1
|
||||
|
||||
self.voting_clear()
|
||||
for i in self.players:
|
||||
i.score.clear()
|
||||
|
||||
logging.info(
|
||||
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
|
||||
for i in self.players:
|
||||
if i.player_id != 0:
|
||||
logging.info(f'- Player `{i.name}` - {i.last_score}')
|
||||
|
||||
@property
|
||||
def is_all_player_voted(self) -> bool:
|
||||
# 是否所有玩家都投票
|
||||
if self.state != 2:
|
||||
return False
|
||||
|
||||
for i in self.players:
|
||||
if i.player_id != 0 and i.voting == 0x8000:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def random_song(self):
|
||||
random_list = []
|
||||
for i in range(Config.LINK_PLAY_UNLOCK_LENGTH):
|
||||
for j in range(8):
|
||||
if self.song_unlock[i] & (1 << j):
|
||||
random_list.append(i * 8 + j)
|
||||
|
||||
if not random_list:
|
||||
self.song_idx = 0
|
||||
else:
|
||||
self.song_idx = random_list[randint(0, len(random_list) - 1)]
|
||||
|
||||
def make_voting(self):
|
||||
# 投票
|
||||
self.state = 3
|
||||
self.selected_voter_player_id = 0
|
||||
|
||||
random_list = []
|
||||
random_list_player_id = []
|
||||
for i in self.players:
|
||||
if i.player_id == 0 or i.voting == 0xffff or i.voting == 0x8000:
|
||||
continue
|
||||
random_list.append(i.voting)
|
||||
random_list_player_id.append(i.player_id)
|
||||
|
||||
if random_list:
|
||||
idx = randint(0, len(random_list) - 1)
|
||||
self.song_idx = random_list[idx] * 5
|
||||
self.selected_voter_player_id = random_list_player_id[idx]
|
||||
else:
|
||||
self.random_song()
|
||||
|
||||
logging.info(
|
||||
f'Room `{self.room_code}` votes song `{self.song_idx}`')
|
||||
|
||||
def voting_clear(self):
|
||||
# 清除投票
|
||||
self.selected_voter_player_id = 0
|
||||
for i in self.players:
|
||||
i.voting = 0x8000
|
||||
|
||||
@property
|
||||
def should_next_state(self) -> bool:
|
||||
if not self.timed_mode and self.state not in (4, 5, 6):
|
||||
self.countdown = 0xffffffff
|
||||
return False
|
||||
now = round(time() * 1000000)
|
||||
if self.countdown == 0xffffffff:
|
||||
# 还没开始计时
|
||||
if self.is_public and self.state == 1:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_MATCHING
|
||||
elif self.state == 2:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_SONG
|
||||
elif self.state == 3:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_DIFFICULTY
|
||||
elif self.state == 4:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_READY
|
||||
elif self.state == 5 or self.state == 6:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_START
|
||||
elif self.state == 8:
|
||||
self.next_state_timestamp = now + Config.COUNTDOWN_RESULT
|
||||
else:
|
||||
return False
|
||||
|
||||
# 不是哥们,616 你脑子怎么长的,上个版本是毫秒时间戳,新版本变成了微秒???那你这倒计时怎么还是毫秒啊!!!
|
||||
self.countdown = (self.next_state_timestamp - now) // 1000
|
||||
if self.countdown <= 0:
|
||||
self.countdown = 0
|
||||
return True
|
||||
return False
|
||||
385
latest version/linkplay_server/udp_parser.py
Normal file
385
latest version/linkplay_server/udp_parser.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import logging
|
||||
|
||||
from .config import Config
|
||||
from .udp_class import Room, bi
|
||||
from .udp_sender import CommandSender
|
||||
|
||||
|
||||
class CommandParser:
|
||||
|
||||
route = {
|
||||
0x01: 'command_01',
|
||||
0x02: 'command_02',
|
||||
0x03: 'command_03',
|
||||
0x04: 'command_04',
|
||||
0x06: 'command_06',
|
||||
0x07: 'command_07',
|
||||
0x08: 'command_08',
|
||||
0x09: 'command_09',
|
||||
0x0a: 'command_0a',
|
||||
0x0b: 'command_0b',
|
||||
0x20: 'command_20',
|
||||
0x22: 'command_22',
|
||||
0x23: 'command_23',
|
||||
}
|
||||
|
||||
def __init__(self, room: Room, player_index: int = 0) -> None:
|
||||
self.room = room
|
||||
self.player_index = player_index
|
||||
self.s = CommandSender(self.room)
|
||||
self.command: bytes = None
|
||||
|
||||
def get_commands(self, command: bytes):
|
||||
self.command = command
|
||||
r = getattr(self, self.route[self.command[2]])()
|
||||
|
||||
re = []
|
||||
|
||||
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):
|
||||
if self.room.command_queue[i][2] == 0x13:
|
||||
if flag_13:
|
||||
break
|
||||
flag_13 = True
|
||||
re.append(self.room.command_queue[i])
|
||||
|
||||
if self.room.players[self.player_index].extra_command_queue:
|
||||
re += self.room.players[self.player_index].extra_command_queue[-12:]
|
||||
self.room.players[self.player_index].extra_command_queue = []
|
||||
|
||||
if r:
|
||||
re += r
|
||||
|
||||
return re
|
||||
|
||||
def command_01(self):
|
||||
# 给房主
|
||||
player_id = bi(self.command[24:32])
|
||||
for i in self.room.players:
|
||||
if i.player_id == player_id and i.online == 1:
|
||||
self.room.host_id = player_id
|
||||
logging.info(
|
||||
f'Player `{i.name}` becomes the host of room `{self.room.room_code}`')
|
||||
|
||||
self.s.random_code = self.command[16:24]
|
||||
self.room.command_queue.append(self.s.command_10())
|
||||
|
||||
def command_02(self):
|
||||
# 房主选歌
|
||||
if self.room.round_mode == 3:
|
||||
logging.warning('Error: round_mode == 3 in command 02')
|
||||
return None
|
||||
self.s.random_code = self.command[16:24]
|
||||
song_idx = bi(self.command[24:26])
|
||||
|
||||
flag = 5
|
||||
if self.room.state == 2:
|
||||
flag = 0
|
||||
self.room.state = 3
|
||||
self.room.song_idx = song_idx
|
||||
self.room.command_queue.append(self.s.command_11())
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
return [self.s.command_0d(flag)]
|
||||
|
||||
def command_03(self):
|
||||
# 尝试进入结算
|
||||
self.s.random_code = self.command[16:24]
|
||||
player = self.room.players[self.player_index]
|
||||
player.score.score = bi(self.command[24:28])
|
||||
player.score.cleartype = self.command[28]
|
||||
player.score.difficulty = self.command[29]
|
||||
player.score.best_score_flag = self.command[30]
|
||||
player.score.shiny_perfect_count = bi(self.command[31:33])
|
||||
player.score.perfect_count = bi(self.command[33:35])
|
||||
player.score.near_count = bi(self.command[35:37])
|
||||
player.score.miss_count = bi(self.command[37:39])
|
||||
player.score.early_count = bi(self.command[39:41])
|
||||
player.score.late_count = bi(self.command[41:43])
|
||||
player.score.healthy = bi(self.command[43:47])
|
||||
player.finish_flag = 1
|
||||
player.last_timestamp -= Config.COMMAND_INTERVAL
|
||||
self.room.last_song_idx = self.room.song_idx
|
||||
|
||||
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||
|
||||
if self.room.is_finish():
|
||||
self.room.make_finish()
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
def command_04(self):
|
||||
# 踢人
|
||||
self.s.random_code = self.command[16:24]
|
||||
player_id = bi(self.command[24:32])
|
||||
flag = 2
|
||||
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
|
||||
for i in range(4):
|
||||
if self.room.players[i].player_id == player_id:
|
||||
flag = 1
|
||||
self.room.delete_player(i)
|
||||
self.room.command_queue.append(self.s.command_12(i))
|
||||
self.room.command_queue.append(self.s.command_14())
|
||||
break
|
||||
|
||||
return [self.s.command_0d(flag)]
|
||||
|
||||
def command_06(self):
|
||||
self.s.random_code = self.command[16:24]
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
self.room.voting_clear()
|
||||
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
def command_07(self):
|
||||
self.s.random_code = self.command[16:24]
|
||||
self.room.players[self.player_index].song_unlock = self.command[24:536]
|
||||
self.room.update_song_unlock()
|
||||
|
||||
self.room.command_queue.append(self.s.command_14())
|
||||
|
||||
# 07 可能需要一个 0d 响应,code = 0x0b
|
||||
|
||||
def command_08(self):
|
||||
# 可能弃用
|
||||
logging.warning('Command 08 is outdated')
|
||||
pass
|
||||
# self.room.round_mode = bi(self.command[24:25])
|
||||
# self.s.random_code = self.command[16:24]
|
||||
# self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
def command_09(self):
|
||||
self.s.random_code = self.command[16:24]
|
||||
player = self.room.players[self.player_index]
|
||||
|
||||
if bi(self.command[12:16]) == 0:
|
||||
player.online = 1
|
||||
self.room.state = 1
|
||||
self.room.update_song_unlock()
|
||||
player.start_command_num = self.room.command_queue_length
|
||||
self.room.command_queue.append(self.s.command_15())
|
||||
return None
|
||||
|
||||
flag_0c = False
|
||||
|
||||
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
||||
flag_0c = True
|
||||
player.last_timestamp = self.s.timestamp
|
||||
|
||||
# 离线判断
|
||||
flag_13, player_index_list = self.room.check_player_online(
|
||||
self.s.timestamp)
|
||||
for i in player_index_list:
|
||||
self.room.command_queue.append(self.s.command_12(i))
|
||||
|
||||
flag_11 = False
|
||||
flag_12 = False
|
||||
|
||||
if player.online == 0:
|
||||
flag_12 = True
|
||||
player.online = 1
|
||||
|
||||
if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8:
|
||||
# 还在结算给踢了
|
||||
# 冗余,为了保险
|
||||
self.room.delete_player(self.player_index)
|
||||
self.room.command_queue.append(
|
||||
self.s.command_12(self.player_index))
|
||||
self.room.command_queue.append(self.s.command_14())
|
||||
|
||||
if self.room.is_ready(1, 1) and ((self.room.player_num > 1 and not self.room.is_public) or (self.room.is_public and self.room.player_num == 4)):
|
||||
flag_13 = True
|
||||
self.room.state = 2
|
||||
|
||||
if self.room.state == 1 and self.room.is_public and self.room.player_num > 1 and self.room.should_next_state:
|
||||
flag_0c = True
|
||||
flag_13 = True
|
||||
self.room.state = 2
|
||||
|
||||
if self.room.state in (2, 3) and self.room.player_num < 2:
|
||||
flag_13 = True
|
||||
self.room.state = 1
|
||||
|
||||
if self.room.state == 2 and self.room.should_next_state:
|
||||
flag_0c = True
|
||||
self.room.state = 3
|
||||
flag_13 = True
|
||||
if self.room.round_mode == 3:
|
||||
self.room.make_voting()
|
||||
else:
|
||||
self.room.random_song()
|
||||
|
||||
if player.player_state != self.command[32]:
|
||||
flag_12 = True
|
||||
player.player_state = self.command[32]
|
||||
|
||||
if player.score.difficulty != self.command[33] and player.player_state not in (5, 6, 7, 8):
|
||||
flag_12 = True
|
||||
player.score.difficulty = self.command[33]
|
||||
|
||||
if player.score.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
||||
flag_12 = True
|
||||
player.score.cleartype = self.command[34]
|
||||
|
||||
if player.download_percent != self.command[35]:
|
||||
flag_12 = True
|
||||
player.download_percent = self.command[35]
|
||||
|
||||
if player.character_id != self.command[36]:
|
||||
flag_12 = True
|
||||
player.character_id = self.command[36]
|
||||
|
||||
if player.is_uncapped != self.command[37]:
|
||||
flag_12 = True
|
||||
player.is_uncapped = self.command[37]
|
||||
|
||||
if self.room.state == 3 and player.score.score != bi(self.command[24:28]):
|
||||
flag_12 = True
|
||||
player.score.score = bi(self.command[24:28])
|
||||
|
||||
if self.room.is_ready(3, 4) or (self.room.state == 3 and self.room.should_next_state):
|
||||
flag_13 = True
|
||||
flag_0c = True
|
||||
self.room.state = 4
|
||||
|
||||
if self.room.round_mode == 2:
|
||||
# 将换房主时间提前到此刻
|
||||
self.room.make_round()
|
||||
logging.info(f'Room `{self.room.room_code}` starts playing')
|
||||
|
||||
for p in self.room.players:
|
||||
# 防止提前结算
|
||||
p.finish_flag = 0
|
||||
|
||||
if self.room.state == 4:
|
||||
# 这好像会误判
|
||||
# if player.download_percent < 99:
|
||||
# # 有人没下载完把他踢了!
|
||||
# self.room.delete_player(self.player_index)
|
||||
# self.room.command_queue.append(
|
||||
# self.s.command_12(self.player_index))
|
||||
# self.room.command_queue.append(self.s.command_14())
|
||||
|
||||
if self.room.should_next_state:
|
||||
self.room.state = 5
|
||||
flag_11 = True
|
||||
flag_13 = True
|
||||
|
||||
if self.room.state == 5:
|
||||
flag_13 = True
|
||||
if self.room.is_ready(5, 6):
|
||||
self.room.state = 6
|
||||
if self.room.is_ready(5, 7):
|
||||
self.room.state = 7
|
||||
|
||||
if self.room.state in (5, 6) and self.room.should_next_state:
|
||||
# 此处不清楚
|
||||
self.room.state = 7
|
||||
flag_13 = True
|
||||
|
||||
if self.room.state in (7, 8):
|
||||
player_now_timer = bi(self.command[28:32])
|
||||
if player.score.timer < player_now_timer or player_now_timer == 0 and player.score.timer != 0:
|
||||
player.last_score.timer = player.score.timer
|
||||
player.last_score.score = player.score.score
|
||||
player.score.timer = player_now_timer
|
||||
player.score.score = bi(self.command[24:28])
|
||||
|
||||
if player.score.timer != 0 or self.room.state != 8:
|
||||
for i in self.room.players:
|
||||
i.extra_command_queue.append(
|
||||
self.s.command_0e(self.player_index))
|
||||
|
||||
if self.room.is_ready(8, 1):
|
||||
flag_13 = True
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
|
||||
if self.room.state == 8 and self.room.should_next_state:
|
||||
flag_0c = True
|
||||
flag_13 = True
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
|
||||
if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8:
|
||||
# 还在结算给踢了
|
||||
self.room.delete_player(self.player_index)
|
||||
self.room.command_queue.append(
|
||||
self.s.command_12(self.player_index))
|
||||
self.room.command_queue.append(self.s.command_14())
|
||||
|
||||
if self.room.is_finish():
|
||||
# 有人退房导致的结算
|
||||
self.room.make_finish()
|
||||
flag_13 = True
|
||||
|
||||
if flag_11:
|
||||
self.room.command_queue.append(self.s.command_11())
|
||||
if flag_12:
|
||||
self.room.command_queue.append(
|
||||
self.s.command_12(self.player_index))
|
||||
if flag_13:
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
if flag_0c:
|
||||
return [self.s.command_0c()]
|
||||
|
||||
def command_0a(self):
|
||||
# 退出房间
|
||||
self.room.delete_player(self.player_index)
|
||||
|
||||
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||
|
||||
# self.room.command_queue.append(self.s.command_11())
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
self.room.command_queue.append(self.s.command_14())
|
||||
|
||||
def command_0b(self):
|
||||
# 推荐歌曲
|
||||
song_idx = bi(self.command[16:18])
|
||||
for i in range(4):
|
||||
if self.player_index != i and self.room.players[i].online == 1:
|
||||
self.room.players[i].extra_command_queue.append(
|
||||
self.s.command_0f(self.player_index, song_idx))
|
||||
|
||||
def command_20(self):
|
||||
# 表情
|
||||
sticker_id = bi(self.command[16:18])
|
||||
for i in range(4):
|
||||
if self.player_index != i and self.room.players[i].online == 1:
|
||||
self.room.players[i].extra_command_queue.append(
|
||||
self.s.command_21(self.player_index, sticker_id))
|
||||
|
||||
def command_22(self):
|
||||
# 房间设置,懒得判断房主
|
||||
self.s.random_code = self.command[16:24]
|
||||
self.room.is_public = self.command[25]
|
||||
if self.room.is_public == 0:
|
||||
self.room.round_mode = self.command[24]
|
||||
self.room.timed_mode = self.command[26]
|
||||
else:
|
||||
self.room.round_mode = 3
|
||||
self.room.timed_mode = 1
|
||||
self.room.state = 1
|
||||
self.room.command_queue.append(self.s.command_11())
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
return [self.s.command_0d(1)]
|
||||
|
||||
def command_23(self):
|
||||
# 歌曲投票
|
||||
self.s.random_code = self.command[16:24]
|
||||
if self.room.player_num < 2:
|
||||
return [self.s.command_0d(6)]
|
||||
if self.room.state != 2:
|
||||
return [self.s.command_0d(5)]
|
||||
player = self.room.players[self.player_index]
|
||||
player.voting = bi(self.command[24:26])
|
||||
logging.info(
|
||||
f'Player `{player.name}` votes for song `{player.voting}`')
|
||||
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||
|
||||
if self.room.is_all_player_voted:
|
||||
self.room.make_voting()
|
||||
self.room.command_queue.append(self.s.command_13())
|
||||
|
||||
return [self.s.command_0d(1)]
|
||||
89
latest version/linkplay_server/udp_sender.py
Normal file
89
latest version/linkplay_server/udp_sender.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from os import urandom
|
||||
from time import time
|
||||
|
||||
from .udp_class import Room, b
|
||||
|
||||
|
||||
PADDING = [b(i) * i for i in range(16)] + [b'']
|
||||
|
||||
|
||||
class CommandSender:
|
||||
|
||||
PROTOCOL_NAME = b'\x06\x16'
|
||||
PROTOCOL_VERSION = b'\x0E'
|
||||
|
||||
def __init__(self, room: Room = None) -> None:
|
||||
self.room = room
|
||||
self.timestamp = round(time() * 1000000)
|
||||
self.room.timestamp = self.timestamp + 1
|
||||
|
||||
self._random_code = None
|
||||
|
||||
@property
|
||||
def random_code(self):
|
||||
if self._random_code is None:
|
||||
self._random_code = urandom(4) + b'\x00\x00\x00\x00'
|
||||
return self._random_code
|
||||
|
||||
@random_code.setter
|
||||
def random_code(self, value):
|
||||
self._random_code = value
|
||||
|
||||
@staticmethod
|
||||
def command_encode(t: tuple):
|
||||
r = b''.join(t)
|
||||
x = 16 - len(r) % 16
|
||||
return r + PADDING[x]
|
||||
|
||||
def command_prefix(self, command: bytes):
|
||||
length = self.room.command_queue_length
|
||||
if b'\x10' <= command <= b'\x1f':
|
||||
length += 1
|
||||
|
||||
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
||||
|
||||
def command_0c(self):
|
||||
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):
|
||||
# 3 你不是房主
|
||||
# 5 有玩家目前无法开始
|
||||
# 6 需要更多玩家以开始
|
||||
# 7 有玩家无法游玩这首歌
|
||||
|
||||
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
|
||||
|
||||
def command_0e(self, player_index: int):
|
||||
# 分数广播
|
||||
# 我猜,616 写错了,首先 4 个 00 大概是分数使用了 8 bytes 转换,其次上一个分数根本就不需要哈哈哈哈哈哈!
|
||||
player = self.room.players[player_index]
|
||||
return self.command_encode((*self.command_prefix(b'\x0e'), player.info, b(player.last_score.score, 4), b'\x00' * 4, b(player.last_score.timer, 4), b'\x00' * 4))
|
||||
|
||||
def command_0f(self, player_index: int, song_idx: int):
|
||||
# 歌曲推荐
|
||||
player = self.room.players[player_index]
|
||||
return self.command_encode((*self.command_prefix(b'\x0f'), b(player.player_id, 8), b(song_idx, 2)))
|
||||
|
||||
def command_10(self):
|
||||
# 房主宣告
|
||||
return self.command_encode((*self.command_prefix(b'\x10'), self.random_code, b(self.room.host_id, 8)))
|
||||
|
||||
def command_11(self):
|
||||
return self.command_encode((*self.command_prefix(b'\x11'), self.random_code, self.room.get_players_info()))
|
||||
|
||||
def command_12(self, player_index: int):
|
||||
player = self.room.players[player_index]
|
||||
return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), player.info))
|
||||
|
||||
def command_13(self):
|
||||
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, self.room.room_info))
|
||||
|
||||
def command_14(self):
|
||||
return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
|
||||
|
||||
def command_15(self):
|
||||
return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, self.room.room_info))
|
||||
|
||||
def command_21(self, player_index: int, sticker_id: int):
|
||||
player = self.room.players[player_index]
|
||||
return self.command_encode((*self.command_prefix(b'\x21'), b(player.player_id, 8), b(sticker_id, 2)))
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,72 +1,62 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from flask import Flask, request, jsonify, make_response, send_from_directory
|
||||
from logging.config import dictConfig
|
||||
from setting import Config
|
||||
import base64
|
||||
import server.auth
|
||||
import server.info
|
||||
import server.setme
|
||||
import server.arcscore
|
||||
import web.login
|
||||
import web.index
|
||||
import server.arcworld
|
||||
import server.arcdownload
|
||||
import server.arcpurchase
|
||||
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
|
||||
from logging.config import dictConfig
|
||||
from multiprocessing import Process, set_start_method
|
||||
from traceback import format_exc
|
||||
|
||||
from flask import Flask, make_response, request, send_from_directory
|
||||
|
||||
import api
|
||||
import server
|
||||
import web.index
|
||||
import web.login
|
||||
# import webapi
|
||||
from core.bundle import BundleDownload
|
||||
from core.constant import Constant
|
||||
from core.download import UserDownload
|
||||
from core.error import ArcError, NoAccess, RateLimit
|
||||
from core.init import FileChecker
|
||||
from core.sql import Connect
|
||||
from server.func import error_return
|
||||
|
||||
app = Flask(__name__)
|
||||
wsgi_app = app.wsgi_app
|
||||
|
||||
if Config.USE_PROXY_FIX:
|
||||
# 代理修复
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
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)
|
||||
|
||||
|
||||
def error_return(error_code): # 错误返回
|
||||
# -7 处理交易时发生了错误
|
||||
# -5 所有的曲目都已经下载完毕
|
||||
# -4 您的账号已在别处登录
|
||||
# -3 无法连接至服务器
|
||||
# 2 Arcaea服务器正在维护
|
||||
# 5 请更新Arcaea到最新版本
|
||||
# 100 无法在此ip地址下登录游戏
|
||||
# 101 用户名占用
|
||||
# 102 电子邮箱已注册
|
||||
# 103 已有一个账号由此设备创建
|
||||
# 104 用户名密码错误
|
||||
# 105 24小时内登入两台设备
|
||||
# 106 账户冻结
|
||||
# 107 你没有足够的体力
|
||||
# 113 活动已结束
|
||||
# 114 该活动已结束,您的成绩不会提交
|
||||
# 120 封号警告
|
||||
# 121 账户冻结
|
||||
# 122 账户暂时冻结
|
||||
# 123 账户被限制
|
||||
# 124 你今天不能再使用这个IP地址创建新的账号
|
||||
# 150 非常抱歉您已被限制使用此功能
|
||||
# 151 目前无法使用此功能
|
||||
# 401 用户不存在
|
||||
# 403 无法连接至服务器
|
||||
# 501 502 -6 此物品目前无法获取
|
||||
# 504 无效的序列码
|
||||
# 505 此序列码已被使用
|
||||
# 506 你已拥有了此物品
|
||||
# 601 好友列表已满
|
||||
# 602 此用户已是好友
|
||||
# 604 你不能加自己为好友
|
||||
# 903 下载量超过了限制,请24小时后重试
|
||||
# 905 请在再次使用此功能前等待24小时
|
||||
# 1001 设备数量达到上限
|
||||
# 1002 此设备已使用过此功能
|
||||
# 9801 下载歌曲时发生问题,请再试一次
|
||||
# 9802 保存歌曲时发生问题,请检查设备空间容量
|
||||
# 9905 没有在云端发现任何数据
|
||||
# 9907 更新数据时发生了问题
|
||||
# 9908 服务器只支持最新的版本,请更新Arcaea
|
||||
# 其它 发生未知错误
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error_code": error_code
|
||||
})
|
||||
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
|
||||
|
||||
|
||||
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.register_blueprint(web.login.bp)
|
||||
app.register_blueprint(web.index.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
list(map(app.register_blueprint, server.get_bps()))
|
||||
# app.register_blueprint(webapi.bp)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@@ -83,441 +73,104 @@ def favicon():
|
||||
return app.send_static_file('favicon.ico')
|
||||
|
||||
|
||||
@app.route('/latte/13/auth/login', methods=['POST']) # 登录接口
|
||||
def login():
|
||||
headers = request.headers
|
||||
id_pwd = headers['Authorization']
|
||||
id_pwd = base64.b64decode(id_pwd[6:]).decode()
|
||||
name, password = id_pwd.split(':', 1)
|
||||
device_id = headers['DeviceId']
|
||||
token, error_code = server.auth.arc_login(name, password, device_id)
|
||||
if not error_code:
|
||||
r = {"success": True, "token_type": "Bearer"}
|
||||
r['access_token'] = token
|
||||
return jsonify(r)
|
||||
else:
|
||||
return error_return(error_code)
|
||||
|
||||
|
||||
@app.route('/latte/13/user/', methods=['POST']) # 注册接口
|
||||
def register():
|
||||
name = request.form['name']
|
||||
password = request.form['password']
|
||||
device_id = request.form['device_id']
|
||||
user_id, token, error_code = server.auth.arc_register(
|
||||
name, password, device_id)
|
||||
if user_id is not None:
|
||||
r = {"success": True, "value": {
|
||||
'user_id': user_id, 'access_token': token}}
|
||||
return jsonify(r)
|
||||
else:
|
||||
return error_return(error_code) # 应该是101,用户名被占用,毕竟电子邮箱没记录
|
||||
|
||||
|
||||
# 集成式请求,没想到什么好办法处理,就先这样写着
|
||||
@app.route('/latte/13/compose/aggregate', methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def aggregate(user_id):
|
||||
calls = request.args.get('calls')
|
||||
if calls == '[{ "endpoint": "/user/me", "id": 0 }]': # 极其沙雕的判断,我猜get的参数就两种
|
||||
r = server.info.arc_aggregate_small(user_id)
|
||||
else:
|
||||
r = server.info.arc_aggregate_big(user_id)
|
||||
return jsonify(r)
|
||||
|
||||
|
||||
@app.route('/latte/13/user/me/character', methods=['POST']) # 角色切换
|
||||
@server.auth.auth_required(request)
|
||||
def character_change(user_id):
|
||||
character_id = request.form['character']
|
||||
skill_sealed = request.form['skill_sealed']
|
||||
|
||||
flag = server.setme.change_char(user_id, character_id, skill_sealed)
|
||||
if flag:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"character": character_id
|
||||
}
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/<path:path>/toggle_uncap', methods=['POST']) # 角色觉醒切换
|
||||
@server.auth.auth_required(request)
|
||||
def character_uncap(user_id, path):
|
||||
while '//' in path:
|
||||
path = path.replace('//', '/')
|
||||
character_id = int(path[21:])
|
||||
r = server.setme.change_char_uncap(user_id, character_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"character": [r]
|
||||
}
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/friend/me/add', methods=['POST']) # 加好友
|
||||
@server.auth.auth_required(request)
|
||||
def add_friend(user_id):
|
||||
friend_code = request.form['friend_code']
|
||||
friend_id = server.auth.code_get_id(friend_code)
|
||||
if friend_id is not None:
|
||||
r = server.setme.arc_add_friend(user_id, friend_id)
|
||||
if r is not None and r != 602 and r != 604:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": r
|
||||
}
|
||||
})
|
||||
else:
|
||||
if r is not None:
|
||||
return error_return(r)
|
||||
else:
|
||||
return error_return(108)
|
||||
else:
|
||||
return error_return(401)
|
||||
|
||||
|
||||
@app.route('/latte/13/friend/me/delete', methods=['POST']) # 删好友
|
||||
@server.auth.auth_required(request)
|
||||
def delete_friend(user_id):
|
||||
friend_id = int(request.form['friend_id'])
|
||||
if friend_id is not None:
|
||||
r = server.setme.arc_delete_friend(user_id, friend_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": r
|
||||
}
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
else:
|
||||
return error_return(401)
|
||||
|
||||
|
||||
@app.route('/latte/13/score/song/friend', methods=['GET']) # 好友排名,默认最多50
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_friend(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_friend(user_id, song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/score/song/me', methods=['GET']) # 我的排名,默认最多20
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_me(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_me(user_id, song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/score/song', methods=['GET']) # TOP20
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_top(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_top(song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/score/song', methods=['POST']) # 成绩上传
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_post(user_id):
|
||||
song_token = request.form['song_token']
|
||||
song_hash = request.form['song_hash']
|
||||
song_id = request.form['song_id']
|
||||
difficulty = int(request.form['difficulty'])
|
||||
score = int(request.form['score'])
|
||||
shiny_perfect_count = int(request.form['shiny_perfect_count'])
|
||||
perfect_count = int(request.form['perfect_count'])
|
||||
near_count = int(request.form['near_count'])
|
||||
miss_count = int(request.form['miss_count'])
|
||||
health = int(request.form['health'])
|
||||
modifier = int(request.form['modifier'])
|
||||
beyond_gauge = int(request.form['beyond_gauge'])
|
||||
clear_type = int(request.form['clear_type'])
|
||||
submission_hash = request.form['submission_hash']
|
||||
|
||||
# 增加成绩校验
|
||||
if not server.arcscore.arc_score_check(user_id, song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type, song_token, song_hash, submission_hash):
|
||||
return error_return(107)
|
||||
|
||||
r, re = server.arcscore.arc_score_post(user_id, song_id, difficulty, score, shiny_perfect_count,
|
||||
perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type)
|
||||
if r is not None:
|
||||
if re:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": re
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {"user_rating": r}
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/score/token', methods=['GET']) # 成绩上传所需的token,显然我不想验证
|
||||
def score_token():
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"token": "1145141919810"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# 世界模式成绩上传所需的token,无验证
|
||||
@app.route('/latte/13/score/token/world', methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def score_token_world(user_id):
|
||||
args = request.args
|
||||
server.arcworld.play_world_song(user_id, args)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"stamina": 12,
|
||||
"max_stamina_ts": 1599547603825,
|
||||
"token": "13145201919810"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/user/me/save', methods=['GET']) # 从云端同步
|
||||
@server.auth.auth_required(request)
|
||||
def cloud_get(user_id):
|
||||
r = server.arcscore.arc_all_get(user_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/user/me/save', methods=['POST']) # 向云端同步
|
||||
@server.auth.auth_required(request)
|
||||
def cloud_post(user_id):
|
||||
scores_data = request.form['scores_data']
|
||||
clearlamps_data = request.form['clearlamps_data']
|
||||
clearedsongs_data = request.form['clearedsongs_data']
|
||||
unlocklist_data = request.form['unlocklist_data']
|
||||
installid_data = request.form['installid_data']
|
||||
devicemodelname_data = request.form['devicemodelname_data']
|
||||
story_data = request.form['story_data']
|
||||
|
||||
server.arcscore.arc_all_post(user_id, scores_data, clearlamps_data, clearedsongs_data,
|
||||
unlocklist_data, installid_data, devicemodelname_data, story_data)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/purchase/me/redeem', methods=['POST']) # 兑换码
|
||||
@server.auth.auth_required(request)
|
||||
def redeem(user_id):
|
||||
code = request.form['code']
|
||||
fragment, error_code = server.arcpurchase.claim_user_redeem(
|
||||
user_id, code)
|
||||
if not error_code:
|
||||
if fragment > 0:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {"coupon": "fragment"+str(fragment)}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {"coupon": ""}
|
||||
})
|
||||
else:
|
||||
return error_return(error_code)
|
||||
|
||||
|
||||
# 礼物确认
|
||||
@app.route('/latte/13/present/me/claim/<present_id>', methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def claim_present(user_id, present_id):
|
||||
flag = server.arcpurchase.claim_user_present(user_id, present_id)
|
||||
if flag:
|
||||
return jsonify({
|
||||
"success": True
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route('/latte/13/purchase/me/item', methods=['POST']) # 购买,world模式boost
|
||||
@server.auth.auth_required(request)
|
||||
def prog_boost(user_id):
|
||||
re = {"success": False}
|
||||
if 'item_id' in request.form:
|
||||
if request.form['item_id'] == 'prog_boost_300':
|
||||
ticket, error_code = server.arcpurchase.get_prog_boost(user_id)
|
||||
if error_code:
|
||||
return error_return(error_code)
|
||||
|
||||
re = {
|
||||
"success": True,
|
||||
"value": {'ticket': ticket}
|
||||
}
|
||||
return jsonify(re)
|
||||
|
||||
|
||||
@app.route('/latte/13/purchase/me/pack', methods=['POST']) # 曲包和单曲购买
|
||||
@server.auth.auth_required(request)
|
||||
def pack(user_id):
|
||||
if 'pack_id' in request.form:
|
||||
return jsonify(server.arcpurchase.buy_pack(user_id, request.form['pack_id']))
|
||||
if 'single_id' in request.form:
|
||||
return jsonify(server.arcpurchase.buy_single(user_id, request.form['single_id']))
|
||||
|
||||
return jsonify({
|
||||
"success": True
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/purchase/bundle/single', methods=['GET']) # 单曲购买信息获取
|
||||
def single():
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": server.arcpurchase.get_single_purchase()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/world/map/me', methods=['GET']) # 获得世界模式信息,所有地图
|
||||
@server.auth.auth_required(request)
|
||||
def world_all(user_id):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"current_map": server.arcworld.get_current_map(user_id),
|
||||
"user_id": user_id,
|
||||
"maps": server.arcworld.get_world_all(user_id)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/world/map/me/', methods=['POST']) # 进入地图
|
||||
@server.auth.auth_required(request)
|
||||
def world_in(user_id):
|
||||
map_id = request.form['map_id']
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": server.arcworld.get_user_world(user_id, map_id)
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/world/map/me/<map_id>', methods=['GET']) # 获得单个地图完整信息
|
||||
@server.auth.auth_required(request)
|
||||
def world_one(user_id, map_id):
|
||||
server.arcworld.change_user_current_map(user_id, map_id)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"current_map": map_id,
|
||||
"maps": [server.arcworld.get_user_world_info(user_id, map_id)]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/latte/13/serve/download/me/song', methods=['GET']) # 歌曲下载
|
||||
@server.auth.auth_required(request)
|
||||
def download_song(user_id):
|
||||
song_ids = request.args.getlist('sid')
|
||||
if server.arcdownload.is_able_download(user_id):
|
||||
re = {}
|
||||
if not song_ids:
|
||||
re = server.arcdownload.get_all_songs(user_id)
|
||||
else:
|
||||
re = server.arcdownload.get_some_songs(user_id, song_ids)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": re
|
||||
})
|
||||
else:
|
||||
return error_return(903)
|
||||
|
||||
|
||||
@app.route('/download/<path:file_path>', methods=['GET']) # 下载
|
||||
def download(file_path):
|
||||
try:
|
||||
t = request.args.get('t')
|
||||
message = server.arcdownload.is_token_able_download(t)
|
||||
if message == 0:
|
||||
path = os.path.join('./database/songs', file_path)
|
||||
if os.path.isfile(path) and not('../' in path or '..\\' in path):
|
||||
return send_from_directory('./database/songs', file_path, as_attachment=True)
|
||||
else:
|
||||
return error_return(109)
|
||||
with Connect(in_memory=True) as c:
|
||||
try:
|
||||
x = UserDownload(c)
|
||||
x.token = request.args.get('t')
|
||||
x.song_id, x.file_name = file_path.split('/', 1)
|
||||
x.select_for_check()
|
||||
if x.is_limited:
|
||||
raise RateLimit(
|
||||
f'User `{x.user.user_id}` has reached the download limit.', 903)
|
||||
if not x.is_valid:
|
||||
raise NoAccess('Expired token.')
|
||||
x.download_hit()
|
||||
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||
# nginx X-Accel-Redirect
|
||||
response = make_response()
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.headers['X-Accel-Redirect'] = Config.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)
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
app.logger.warning(format_exc())
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
|
||||
|
||||
@app.route('/bundle_download/<string:token>', methods=['GET']) # 热更新下载
|
||||
def bundle_download(token: str):
|
||||
with Connect(in_memory=True) as c_m:
|
||||
try:
|
||||
file_path = BundleDownload(c_m).get_path_by_token(
|
||||
token, request.remote_addr)
|
||||
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||
# nginx X-Accel-Redirect
|
||||
response = make_response()
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.headers['X-Accel-Redirect'] = Config.BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX + file_path
|
||||
return response
|
||||
return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
app.logger.warning(format_exc())
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
|
||||
|
||||
if Config.DEPLOY_MODE == 'waitress':
|
||||
# 给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():
|
||||
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
|
||||
WSGIServer(host_port, app, log=app.logger).serve_forever()
|
||||
elif Config.DEPLOY_MODE == 'waitress':
|
||||
# waitress WSGI server
|
||||
import logging
|
||||
from waitress import serve # type: ignore
|
||||
logger = logging.getLogger('waitress')
|
||||
logger.setLevel(logging.INFO)
|
||||
serve(app, host=Config.HOST, port=Config.PORT)
|
||||
else:
|
||||
if Config.SSL_CERT and Config.SSL_KEY:
|
||||
app.run(Config.HOST, Config.PORT, ssl_context=(
|
||||
Config.SSL_CERT, Config.SSL_KEY))
|
||||
else:
|
||||
return error_return(message)
|
||||
except:
|
||||
return error_return(108)
|
||||
app.run(Config.HOST, Config.PORT)
|
||||
|
||||
|
||||
@app.route('/latte/<path:path>', methods=['POST']) # 三个设置,写在最后降低优先级
|
||||
@server.auth.auth_required(request)
|
||||
def sys_set(user_id, path):
|
||||
set_arg = path[10:]
|
||||
value = request.form['value']
|
||||
server.setme.arc_sys_set(user_id, value, set_arg)
|
||||
r = server.info.arc_aggregate_small(user_id)
|
||||
r['value'] = r['value'][0]['value']
|
||||
return jsonify(r)
|
||||
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():
|
||||
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.register_blueprint(web.login.bp)
|
||||
app.register_blueprint(web.index.bp)
|
||||
|
||||
def pre_main():
|
||||
log_dict = {
|
||||
'version': 1,
|
||||
'root': {
|
||||
@@ -530,15 +183,7 @@ def main():
|
||||
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||
'formatter': 'default'
|
||||
},
|
||||
"error_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "ERROR",
|
||||
"formatter": "default",
|
||||
"filename": "./log/error.log"
|
||||
}
|
||||
"error_file": generate_log_file_dict('ERROR', './log/error.log')
|
||||
},
|
||||
'formatters': {
|
||||
'default': {
|
||||
@@ -546,34 +191,45 @@ def main():
|
||||
}
|
||||
}
|
||||
}
|
||||
if Config.ALLOW_LOG_INFO:
|
||||
log_dict['root']['handlers'] = ['wsgi', 'info_file', 'error_file']
|
||||
log_dict['handlers']['info_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "INFO",
|
||||
"formatter": "default",
|
||||
"filename": "./log/info.log"
|
||||
}
|
||||
if Config.ALLOW_INFO_LOG:
|
||||
log_dict['root']['handlers'].append('info_file')
|
||||
log_dict['handlers']['info_file'] = generate_log_file_dict(
|
||||
'INFO', './log/info.log')
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
log_dict['root']['handlers'].append('warning_file')
|
||||
log_dict['handlers']['warning_file'] = generate_log_file_dict(
|
||||
'WARNING', './log/warning.log')
|
||||
|
||||
dictConfig(log_dict)
|
||||
|
||||
app.logger.info("Start to initialize data in 'songfile' table...")
|
||||
try:
|
||||
error = server.arcdownload.initialize_songfile()
|
||||
except:
|
||||
error = 'Something wrong.'
|
||||
if error:
|
||||
app.logger.warning(error)
|
||||
Connect.logger = app.logger
|
||||
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.')
|
||||
sys.exit()
|
||||
|
||||
|
||||
def main():
|
||||
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
||||
from linkplay_server import link_play
|
||||
process = [Process(target=link_play, args=(
|
||||
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
||||
[p.start() for p in process]
|
||||
app.logger.info(
|
||||
f"Link Play UDP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_UDP_PORT} ...")
|
||||
app.logger.info(
|
||||
f"Link Play TCP server is running on {Config.LINKPLAY_HOST}:{Config.LINKPLAY_TCP_PORT} ...")
|
||||
tcp_server_run()
|
||||
[p.join() for p in process]
|
||||
else:
|
||||
app.logger.info('Complete!')
|
||||
tcp_server_run()
|
||||
|
||||
app.run(Config.HOST, Config.PORT)
|
||||
|
||||
# must run for init
|
||||
pre_main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
set_start_method("spawn")
|
||||
main()
|
||||
|
||||
|
||||
|
||||
3
latest version/requirements.txt
Normal file
3
latest version/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask>=2.0.2
|
||||
cryptography>=35.0.0
|
||||
limits>=2.7.0
|
||||
@@ -1 +1,4 @@
|
||||
python -B main.py
|
||||
cd /d %~dp0
|
||||
:: Develop server
|
||||
python -B main.py
|
||||
pause
|
||||
|
||||
9
latest version/run_linkplay_server.py
Normal file
9
latest version/run_linkplay_server.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import linkplay_server
|
||||
|
||||
os.chdir(sys.path[0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
linkplay_server.link_play()
|
||||
39
latest version/server/__init__.py
Normal file
39
latest version/server/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
from core.config_manager import Config
|
||||
|
||||
from . import (auth, course, friend, multiplayer, others, present, purchase,
|
||||
score, user, world, mission)
|
||||
|
||||
|
||||
__bp_old = Blueprint('old_server', __name__)
|
||||
|
||||
|
||||
@__bp_old.route('/<path:any>', methods=['GET', 'POST']) # 旧版 API 提示
|
||||
def server_hello(any):
|
||||
return jsonify({"success": False, "error_code": 5})
|
||||
|
||||
|
||||
def get_bps():
|
||||
def string_to_list(s):
|
||||
if isinstance(s, str):
|
||||
s = [s]
|
||||
elif not isinstance(s, list):
|
||||
s = []
|
||||
return s
|
||||
|
||||
bp = Blueprint('server', __name__)
|
||||
list(map(bp.register_blueprint, [user.bp2, user.bp, auth.bp, friend.bp, score.bp,
|
||||
world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp, mission.bp]))
|
||||
|
||||
bps = [Blueprint(x, __name__, url_prefix=x)
|
||||
for x in string_to_list(Config.GAME_API_PREFIX)]
|
||||
for x in bps:
|
||||
x.register_blueprint(bp)
|
||||
|
||||
old_bps = [Blueprint(x, __name__, url_prefix=x)
|
||||
for x in string_to_list(Config.OLD_GAME_API_PREFIX)]
|
||||
for x in old_bps:
|
||||
x.register_blueprint(__bp_old)
|
||||
|
||||
return bps + old_bps
|
||||
@@ -1,175 +0,0 @@
|
||||
import os
|
||||
import hashlib
|
||||
from flask import url_for
|
||||
import sqlite3
|
||||
from server.sql import Connect
|
||||
import time
|
||||
from setting import Config
|
||||
|
||||
time_limit = Config.DOWNLOAD_TIMES_LIMIT # 每个玩家24小时下载次数限制
|
||||
time_gap_limit = Config.DOWNLOAD_TIME_GAP_LIMIT # 下载链接有效秒数
|
||||
|
||||
|
||||
def get_file_md5(file_path):
|
||||
# 计算文件MD5
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
myhash = hashlib.md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
b = f.read(8096)
|
||||
if not b:
|
||||
break
|
||||
myhash.update(b)
|
||||
|
||||
return myhash.hexdigest()
|
||||
|
||||
|
||||
def get_one_song(c, user_id, song_id, file_dir='./database/songs'):
|
||||
# 获取一首歌的下载链接,返回字典
|
||||
dir_list = os.listdir(os.path.join(file_dir, song_id))
|
||||
re = {}
|
||||
now = int(time.time())
|
||||
c.execute('''delete from download_token where user_id=:a and song_id=:b''', {
|
||||
'a': user_id, 'b': song_id})
|
||||
|
||||
for i in dir_list:
|
||||
if os.path.isfile(os.path.join(file_dir, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg']:
|
||||
token = hashlib.md5(
|
||||
(str(user_id) + song_id + i + str(now)).encode(encoding='UTF-8')).hexdigest()
|
||||
token = token[:8]
|
||||
|
||||
if i == 'base.ogg':
|
||||
c.execute(
|
||||
'''select md5 from songfile where song_id=:a and file_type=-1''', {'a': song_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
checksum = x[0]
|
||||
else:
|
||||
checksum = get_file_md5(os.path.join(
|
||||
file_dir, song_id, 'base.ogg'))
|
||||
re['audio'] = {"checksum": checksum,
|
||||
"url": url_for('download', file_path=song_id+'/base.ogg', t=token, _external=True)}
|
||||
else:
|
||||
if 'chart' not in re:
|
||||
re['chart'] = {}
|
||||
c.execute(
|
||||
'''select md5 from songfile where song_id=:a and file_type=:b''', {'a': song_id, 'b': int(i[0])})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
checksum = x[0]
|
||||
else:
|
||||
checksum = get_file_md5(os.path.join(file_dir, song_id, i))
|
||||
re['chart'][i[0]] = {"checksum": checksum,
|
||||
"url": url_for('download', file_path=song_id+'/'+i, t=token, _external=True)}
|
||||
|
||||
c.execute('''insert into download_token values(:a,:b,:c,:d,:e)''', {
|
||||
'a': user_id, 'b': song_id, 'c': i, 'd': token, 'e': now})
|
||||
|
||||
return {song_id: re}
|
||||
|
||||
|
||||
def get_all_songs(user_id, file_dir='./database/songs'):
|
||||
# 获取所有歌的下载链接,返回字典
|
||||
dir_list = os.listdir(file_dir)
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
for i in dir_list:
|
||||
if os.path.isdir(os.path.join(file_dir, i)):
|
||||
re.update(get_one_song(c, user_id, i, file_dir))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_some_songs(user_id, song_ids):
|
||||
# 获取一些歌的下载链接,返回字典
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
for song_id in song_ids:
|
||||
re.update(get_one_song(c, user_id, song_id))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def is_token_able_download(t):
|
||||
# token是否可以下载,返回错误码,0即可以
|
||||
errorcode = 108
|
||||
with Connect() as c:
|
||||
c.execute('''select * from download_token where token = :t limit 1''',
|
||||
{'t': t})
|
||||
x = c.fetchone()
|
||||
now = int(time.time())
|
||||
if x and now - x[4] <= time_gap_limit:
|
||||
c.execute(
|
||||
'''select count(*) from user_download where user_id = :a''', {'a': x[0]})
|
||||
y = c.fetchone()
|
||||
if y and y[0] <= time_limit:
|
||||
c.execute('''insert into user_download values(:a,:b,:c)''', {
|
||||
'a': x[0], 'b': x[3], 'c': now})
|
||||
errorcode = 0
|
||||
else:
|
||||
errorcode = 903
|
||||
else:
|
||||
errorcode = 108
|
||||
|
||||
return errorcode
|
||||
|
||||
|
||||
def is_able_download(user_id):
|
||||
# 是否可以下载,返回布尔值
|
||||
f = False
|
||||
with Connect() as c:
|
||||
now = int(time.time())
|
||||
c.execute(
|
||||
'''delete from user_download where user_id = :a and time <= :b''', {'a': user_id, 'b': now - 24*3600})
|
||||
c.execute(
|
||||
'''select count(*) from user_download where user_id = :a''', {'a': user_id})
|
||||
y = c.fetchone()
|
||||
if y and y[0] <= time_limit:
|
||||
f = True
|
||||
else:
|
||||
f = False
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def initialize_one_songfile(c, song_id, file_dir='./database/songs'):
|
||||
# 计算并添加歌曲md5到表中,无返回
|
||||
dir_list = os.listdir(os.path.join(file_dir, song_id))
|
||||
for i in dir_list:
|
||||
if os.path.isfile(os.path.join(file_dir, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg']:
|
||||
if i == 'base.ogg':
|
||||
c.execute('''insert into songfile values(:a,-1,:c)''', {
|
||||
'a': song_id, 'c': get_file_md5(os.path.join(file_dir, song_id, 'base.ogg'))})
|
||||
else:
|
||||
c.execute('''insert into songfile values(:a,:b,:c)''', {
|
||||
'a': song_id, 'b': int(i[0]), 'c': get_file_md5(os.path.join(file_dir, song_id, i))})
|
||||
|
||||
return
|
||||
|
||||
|
||||
def initialize_songfile(file_dir='./database/songs'):
|
||||
# 初始化歌曲数据的md5表,返回错误信息
|
||||
error = None
|
||||
try:
|
||||
dir_list = os.listdir(file_dir)
|
||||
except:
|
||||
error = 'OS error!'
|
||||
return error
|
||||
try:
|
||||
conn = sqlite3.connect('./database/arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
except:
|
||||
error = 'Database error!'
|
||||
return error
|
||||
try:
|
||||
c.execute('''delete from songfile''')
|
||||
for i in dir_list:
|
||||
if os.path.isdir(os.path.join(file_dir, i)):
|
||||
initialize_one_songfile(c, i, file_dir)
|
||||
except:
|
||||
error = 'Initialization error!'
|
||||
finally:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return error
|
||||
@@ -1,219 +0,0 @@
|
||||
from server.sql import Connect
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_item(c, type='pack'):
|
||||
# 读取packs内容,返回字典列表
|
||||
c.execute('''select * from item where type = :a''', {'a': type})
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return []
|
||||
|
||||
re = []
|
||||
for i in x:
|
||||
r = {"name": i[0],
|
||||
"items": [{
|
||||
"type": i[1],
|
||||
"id": i[0],
|
||||
"is_available": int2b(i[2])
|
||||
}],
|
||||
"price": i[3],
|
||||
"orig_price": i[4]}
|
||||
|
||||
if i[5] > 0:
|
||||
r['discount_from'] = i[5]
|
||||
if i[6] > 0:
|
||||
r['discount_to'] = i[6]
|
||||
|
||||
re.append(r)
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_single_purchase():
|
||||
# main里面没开数据库,这里写一下代替
|
||||
re = []
|
||||
with Connect() as c:
|
||||
re = get_item(c, type='single')
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def buy_item(c, user_id, price):
|
||||
# 购买接口,返回成功与否标识符和剩余源点数
|
||||
c.execute('''select ticket from user where user_id = :a''',
|
||||
{'a': user_id})
|
||||
ticket = c.fetchone()
|
||||
if ticket:
|
||||
ticket = ticket[0]
|
||||
else:
|
||||
ticket = 0
|
||||
|
||||
if ticket < price:
|
||||
return False, ticket
|
||||
|
||||
c.execute('''update user set ticket = :b where user_id = :a''',
|
||||
{'a': user_id, 'b': ticket-price})
|
||||
|
||||
return True, ticket - price
|
||||
|
||||
|
||||
def buy_pack(user_id, pack_id):
|
||||
# 曲包购买,返回字典
|
||||
re = {"success": False}
|
||||
with Connect() as c:
|
||||
c.execute('''select price from item where item_id = :a''',
|
||||
{'a': pack_id})
|
||||
price = c.fetchone()
|
||||
if price:
|
||||
price = price[0]
|
||||
else:
|
||||
price = 0
|
||||
|
||||
flag, ticket = buy_item(c, user_id, price)
|
||||
|
||||
if flag:
|
||||
c.execute('''insert into user_item values(:a,:b,'pack')''',
|
||||
{'a': user_id, 'b': pack_id})
|
||||
|
||||
re = {"success": True}
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def buy_single(user_id, single_id):
|
||||
# 单曲购买,返回字典
|
||||
re = {"success": False}
|
||||
with Connect() as c:
|
||||
c.execute('''select price from item where item_id = :a''',
|
||||
{'a': single_id})
|
||||
price = c.fetchone()
|
||||
if price:
|
||||
price = price[0]
|
||||
else:
|
||||
price = 0
|
||||
|
||||
flag, ticket = buy_item(c, user_id, price)
|
||||
|
||||
if flag:
|
||||
c.execute('''insert into user_item values(:a,:b,'single')''',
|
||||
{'a': user_id, 'b': single_id})
|
||||
re = {"success": True}
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_prog_boost(user_id):
|
||||
# 世界模式源韵强化,扣50源点,返回剩余源点数
|
||||
|
||||
ticket = -1
|
||||
with Connect() as c:
|
||||
flag, ticket = buy_item(c, user_id, 50)
|
||||
|
||||
if flag:
|
||||
c.execute('''update user set prog_boost = 1 where user_id = :a''', {
|
||||
'a': user_id})
|
||||
if ticket >= 0:
|
||||
return ticket, None
|
||||
else:
|
||||
return 0, 108
|
||||
|
||||
|
||||
def get_user_present(c, user_id):
|
||||
# 获取用户奖励,返回字典列表
|
||||
c.execute(
|
||||
'''select * from present where present_id in (select present_id from user_present where user_id=:a)''', {'a': user_id})
|
||||
x = c.fetchall()
|
||||
re = []
|
||||
now = int(time.time() * 1000)
|
||||
if x:
|
||||
for i in x:
|
||||
if now <= int(i[1]):
|
||||
re.append({'expire_ts': i[1],
|
||||
'description': i[3],
|
||||
'present_id': i[0],
|
||||
'items': json.loads(i[2])
|
||||
})
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def claim_user_present(user_id, present_id):
|
||||
# 确认并删除用户奖励,返回成功与否的布尔值
|
||||
flag = False
|
||||
with Connect() as c:
|
||||
c.execute('''select exists(select * from user_present where user_id=:a and present_id=:b)''',
|
||||
{'a': user_id, 'b': present_id})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''delete from user_present where user_id=:a and present_id=:b''',
|
||||
{'a': user_id, 'b': present_id})
|
||||
c.execute('''select * from present where present_id=:b''',
|
||||
{'b': present_id})
|
||||
x = c.fetchone()
|
||||
now = int(time.time() * 1000)
|
||||
if now <= int(x[1]):
|
||||
# 处理memory
|
||||
items = json.loads(x[2])
|
||||
for i in items:
|
||||
if i['id'] == 'memory':
|
||||
c.execute('''select ticket from user where user_id=:a''', {
|
||||
'a': user_id})
|
||||
ticket = int(c.fetchone()[0])
|
||||
ticket += int(i['amount'])
|
||||
c.execute('''update user set ticket=:b where user_id=:a''', {
|
||||
'a': user_id, 'b': ticket})
|
||||
flag = True
|
||||
else:
|
||||
# 过期
|
||||
flag = False
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
def claim_user_redeem(user_id, code):
|
||||
# 处理兑换码,返回碎片数量和错误码
|
||||
fragment = 0
|
||||
error_code = 108
|
||||
with Connect() as c:
|
||||
c.execute('''select * from redeem where code=:a''', {'a': code})
|
||||
x = c.fetchone()
|
||||
if not x:
|
||||
return 0, 504
|
||||
|
||||
if x[2] == 0: # 一次性
|
||||
c.execute(
|
||||
'''select exists(select * from user_redeem where code=:a)''', {'a': code})
|
||||
if c.fetchone() == (1,):
|
||||
return 0, 505
|
||||
elif x[2] == 1: # 每个玩家一次
|
||||
c.execute('''select exists(select * from user_redeem where code=:a and user_id=:b)''',
|
||||
{'a': code, 'b': user_id})
|
||||
if c.fetchone() == (1,):
|
||||
return 0, 506
|
||||
|
||||
c.execute('''insert into user_redeem values(:b,:a)''',
|
||||
{'a': code, 'b': user_id})
|
||||
|
||||
items = json.loads(x[1])
|
||||
for i in items:
|
||||
if i['type'] == 'fragment':
|
||||
fragment = i['amount']
|
||||
if i['type'] == 'memory':
|
||||
c.execute('''select ticket from user where user_id=:a''', {
|
||||
'a': user_id})
|
||||
ticket = int(c.fetchone()[0])
|
||||
ticket += int(i['amount'])
|
||||
c.execute('''update user set ticket=:b where user_id=:a''', {
|
||||
'a': user_id, 'b': ticket})
|
||||
error_code = None
|
||||
|
||||
return fragment, error_code
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,247 +0,0 @@
|
||||
import json
|
||||
from server.sql import Connect
|
||||
import os
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_world_name(file_dir='./database/map'):
|
||||
# 获取所有地图名称,返回列表
|
||||
L = []
|
||||
for root, dirs, files in os.walk(file_dir):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] == '.json':
|
||||
L.append(os.path.splitext(file)[0])
|
||||
return L
|
||||
|
||||
|
||||
def get_world_info(map_id):
|
||||
# 读取json文件内容,返回字典
|
||||
world_info = {}
|
||||
with open('./database/map/'+map_id+'.json', 'r') as f:
|
||||
world_info = json.load(f)
|
||||
|
||||
return world_info
|
||||
|
||||
|
||||
def get_user_world_info(user_id, map_id):
|
||||
# 读取json文件内容,加上用户信息,返回字典
|
||||
info = get_world_info(map_id)
|
||||
with Connect() as c:
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
info['curr_position'] = x[2]
|
||||
info['curr_capture'] = x[3]
|
||||
info['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,0)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def get_current_map(user_id):
|
||||
# 得到user的当前图,返回字符串
|
||||
re = ''
|
||||
with Connect() as c:
|
||||
c.execute('''select current_map from user where user_id = :a''',
|
||||
{'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
re = x[0]
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_world_all(user_id):
|
||||
# 读取所有地图信息并处理,返回字典列表
|
||||
re = []
|
||||
with Connect() as c:
|
||||
worlds = get_world_name()
|
||||
for map_id in worlds:
|
||||
info = get_world_info(map_id)
|
||||
steps = info['steps']
|
||||
del info['steps']
|
||||
rewards = []
|
||||
for step in steps:
|
||||
if 'items' in step:
|
||||
rewards.append(
|
||||
{'items': step['items'], 'position': step['position']})
|
||||
info['rewards'] = rewards
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
info['curr_position'] = x[2]
|
||||
info['curr_capture'] = x[3]
|
||||
info['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,0)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
re.append(info)
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_user_world(user_id, map_id):
|
||||
# 获取用户图信息,返回字典
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
re = {
|
||||
"user_id": user_id,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": False,
|
||||
"map_id": map_id
|
||||
}
|
||||
if x:
|
||||
re['curr_position'] = x[2]
|
||||
re['curr_capture'] = x[3]
|
||||
re['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,0)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def change_user_current_map(user_id, map_id):
|
||||
# 改变用户当前图
|
||||
with Connect() as c:
|
||||
c.execute('''update user set current_map = :a where user_id=:b''', {
|
||||
'a': map_id, 'b': user_id})
|
||||
return None
|
||||
|
||||
|
||||
def play_world_song(user_id, args):
|
||||
# 声明是世界模式的打歌,并且记录加成信息
|
||||
with Connect() as c:
|
||||
stamina_multiply = 1
|
||||
fragment_multiply = 100
|
||||
prog_boost_multiply = 0
|
||||
if 'stamina_multiply' in args:
|
||||
stamina_multiply = int(args['stamina_multiply'])
|
||||
if 'fragment_multiply' in args:
|
||||
fragment_multiply = int(args['fragment_multiply'])
|
||||
if 'prog_boost_multiply' in args:
|
||||
c.execute('''select prog_boost from user where user_id=:a''', {
|
||||
'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x and x[0] == 1:
|
||||
prog_boost_multiply = 300
|
||||
|
||||
c.execute('''delete from world_songplay where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
'a': user_id, 'b': args['song_id'], 'c': args['difficulty']})
|
||||
c.execute('''insert into world_songplay values(:a,:b,:c,:d,:e,:f)''', {
|
||||
'a': user_id, 'b': args['song_id'], 'c': args['difficulty'], 'd': stamina_multiply, 'e': fragment_multiply, 'f': prog_boost_multiply})
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def climb_step(user_id, map_id, step, prev_capture, prev_position):
|
||||
# 爬梯子,返回奖励列表,台阶列表,当前的位置和坐标,图信息
|
||||
info = get_world_info(map_id)
|
||||
step_count = int(info['step_count'])
|
||||
|
||||
restrict_ids = [[]] * step_count
|
||||
capture = [0] * step_count
|
||||
reward_bundle = [""] * step_count # 暂且不用
|
||||
restrict_id = [""] * step_count
|
||||
restrict_type = [""] * step_count
|
||||
items = [[]] * step_count
|
||||
step_type = [[]] * step_count
|
||||
speed_limit_value = [0] * step_count
|
||||
plus_stamina_value = [0] * step_count
|
||||
|
||||
for i in info['steps']:
|
||||
capture[i['position']] = i['capture']
|
||||
if 'items' in i:
|
||||
items[i['position']] = i['items']
|
||||
if 'restrict_id' in i:
|
||||
restrict_id[i['position']] = i['restrict_id']
|
||||
if 'restrict_ids' in i:
|
||||
restrict_ids[i['position']] = i['restrict_ids']
|
||||
if 'restrict_type' in i:
|
||||
restrict_type[i['position']] = i['restrict_type']
|
||||
if 'step_type' in i:
|
||||
step_type[i['position']] = i['step_type']
|
||||
if "speedlimit" in i['step_type']:
|
||||
speed_limit_value[i['position']] = i['speed_limit_value']
|
||||
if "plusstamina" in i['step_type']:
|
||||
plus_stamina_value[i['position']] = i['plus_stamina_value']
|
||||
|
||||
if info['is_beyond']: # beyond判断
|
||||
dt = info['beyond_health'] - prev_capture
|
||||
if dt >= step:
|
||||
curr_capture = prev_capture + step
|
||||
else:
|
||||
curr_capture = info['beyond_health']
|
||||
i = 0
|
||||
t = prev_capture + step
|
||||
while i < step_count and t > 0:
|
||||
dt = capture[i]
|
||||
if dt > t:
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
i += 1
|
||||
if i >= step_count:
|
||||
curr_position = step_count - 1
|
||||
else:
|
||||
curr_position = i
|
||||
|
||||
else:
|
||||
i = prev_position
|
||||
j = prev_capture
|
||||
t = step
|
||||
while t > 0 and i < step_count:
|
||||
dt = capture[i] - j
|
||||
if dt > t:
|
||||
j += t
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
j = 0
|
||||
i += 1
|
||||
if i >= step_count:
|
||||
curr_position = step_count - 1
|
||||
curr_capture = 0
|
||||
else:
|
||||
curr_position = i
|
||||
curr_capture = j
|
||||
|
||||
rewards = []
|
||||
steps = []
|
||||
for i in range(prev_position, curr_position+1):
|
||||
if items[i]:
|
||||
rewards.append({'position': i, 'items': items[i]})
|
||||
x = {
|
||||
"map_id": map_id,
|
||||
"position": i,
|
||||
"restrict_ids": restrict_ids[i],
|
||||
"capture": capture[i],
|
||||
"reward_bundle": reward_bundle[i],
|
||||
"restrict_id": restrict_id[i],
|
||||
"restrict_type": restrict_type[i]
|
||||
}
|
||||
if step_type[i]:
|
||||
x['step_type'] = step_type[i]
|
||||
if speed_limit_value[i]:
|
||||
x['speed_limit_value'] = speed_limit_value[i]
|
||||
if plus_stamina_value[i]:
|
||||
x['plus_stamina_value'] = plus_stamina_value[i]
|
||||
steps.append(x)
|
||||
|
||||
return rewards, steps, curr_position, curr_capture, info
|
||||
@@ -1,181 +1,67 @@
|
||||
import hashlib
|
||||
import time
|
||||
import server.arcworld
|
||||
from server.sql import Connect
|
||||
import functools
|
||||
from setting import Config
|
||||
import base64
|
||||
from functools import wraps
|
||||
|
||||
from flask import Blueprint, current_app, g, jsonify, request
|
||||
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.sql import Connect
|
||||
from core.user import UserAuth, UserLogin
|
||||
|
||||
from .func import arc_try, error_return, header_check
|
||||
|
||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
|
||||
def arc_login(name: str, password: str, device_id: str): # 登录判断
|
||||
# 查询数据库中的user表,验证账号密码,返回并记录token,多返回个error code
|
||||
# token采用user_id和时间戳连接后hash生成(真的是瞎想的,没用bear)
|
||||
# 密码和token的加密方式为 SHA-256
|
||||
@bp.route('/login', methods=['POST']) # 登录接口
|
||||
@arc_try
|
||||
def login():
|
||||
headers = request.headers
|
||||
e = header_check(request)
|
||||
if e is not None:
|
||||
raise e
|
||||
|
||||
error_code = 108
|
||||
token = None
|
||||
request.form['grant_type']
|
||||
with Connect() as c:
|
||||
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
c.execute('''select user_id, password from user where name = :name''', {
|
||||
'name': name})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
if x[1] == '':
|
||||
# 账号封禁
|
||||
error_code = 106
|
||||
elif x[1] == hash_pwd:
|
||||
user_id = str(x[0])
|
||||
now = int(time.time() * 1000)
|
||||
token = hashlib.sha256(
|
||||
(user_id + str(now)).encode("utf8")).hexdigest()
|
||||
c.execute(
|
||||
'''select login_device from login where user_id = :user_id''', {"user_id": user_id})
|
||||
y = c.fetchall()
|
||||
if y:
|
||||
device_list = []
|
||||
for i in y:
|
||||
if i[0]:
|
||||
device_list.append(i[0])
|
||||
else:
|
||||
device_list.append('')
|
||||
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if not Config.ALLOW_LOGIN_SAME_DEVICE:
|
||||
if device_id in device_list: # 对相同设备进行删除
|
||||
c.execute('''delete from login where login_device=:a''', {
|
||||
'a': device_id})
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - device_list.count(device_id) - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if should_delete_num >= 1: # 删掉多余token
|
||||
c.execute('''delete from login where rowid in (select rowid from login where user_id=:user_id limit :a);''',
|
||||
{'user_id': user_id, 'a': int(should_delete_num)})
|
||||
|
||||
c.execute('''insert into login(access_token, user_id, login_device) values(:access_token, :user_id, :device_id)''', {
|
||||
'user_id': user_id, 'access_token': token, 'device_id': device_id})
|
||||
error_code = None
|
||||
else:
|
||||
# 密码错误
|
||||
error_code = 104
|
||||
id_pwd = headers['Authorization']
|
||||
id_pwd = base64.b64decode(id_pwd[6:]).decode()
|
||||
name, password = id_pwd.split(':', 1)
|
||||
if 'DeviceId' in headers:
|
||||
device_id = headers['DeviceId']
|
||||
else:
|
||||
# 用户名错误
|
||||
error_code = 104
|
||||
device_id = 'low_version'
|
||||
|
||||
return token, error_code
|
||||
user = UserLogin(c)
|
||||
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})
|
||||
|
||||
|
||||
def arc_register(name: str, password: str, device_id: str): # 注册
|
||||
# 账号注册,只记录hash密码和用户名,生成user_id和user_code,自动登录返回token
|
||||
# token和密码的处理同登录部分
|
||||
|
||||
def build_user_code(c):
|
||||
# 生成9位的user_code,用的自然是随机
|
||||
import random
|
||||
flag = True
|
||||
while flag:
|
||||
user_code = ''.join([str(random.randint(0, 9)) for i in range(9)])
|
||||
c.execute('''select exists(select * from user where user_code = :user_code)''',
|
||||
{'user_code': user_code})
|
||||
if c.fetchone() == (0,):
|
||||
flag = False
|
||||
return user_code
|
||||
|
||||
def build_user_id(c):
|
||||
# 生成user_id,往后加1
|
||||
c.execute('''select max(user_id) from user''')
|
||||
x = c.fetchone()
|
||||
if x[0] is not None:
|
||||
return x[0] + 1
|
||||
else:
|
||||
return 2000001
|
||||
|
||||
def insert_user_char(c, user_id):
|
||||
# 为用户添加所有可用角色
|
||||
c.execute('''select * from character''')
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
for i in x:
|
||||
c.execute('''insert into user_char values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n,:o)''', {
|
||||
'a': user_id, 'b': i[0], 'c': i[2], 'd': i[3], 'e': i[4], 'f': i[5], 'g': i[6], 'h': i[7], 'i': i[8], 'j': i[9], 'k': i[10], 'l': i[11], 'm': i[12], 'n': i[14], 'o': i[15]})
|
||||
|
||||
user_id = None
|
||||
token = None
|
||||
error_code = 108
|
||||
with Connect() as c:
|
||||
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
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, 0, 0, 0, 0, 0, 0, -1, 0, '', 0)
|
||||
''', {'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})
|
||||
|
||||
token = hashlib.sha256(
|
||||
(str(user_id) + str(now)).encode("utf8")).hexdigest()
|
||||
c.execute('''insert into login(access_token, user_id, login_device) values(:access_token, :user_id, :device_id)''', {
|
||||
'user_id': user_id, 'access_token': token, 'device_id': device_id})
|
||||
|
||||
insert_user_char(c, user_id)
|
||||
error_code = 0
|
||||
else:
|
||||
error_code = 101
|
||||
|
||||
return user_id, token, error_code
|
||||
|
||||
|
||||
def token_get_id(token: str):
|
||||
# 用token获取id,没有考虑不同用户token相同情况,说不定会有bug
|
||||
|
||||
user_id = None
|
||||
with Connect() as c:
|
||||
c.execute('''select user_id from login where access_token = :token''', {
|
||||
'token': token})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
user_id = x[0]
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def code_get_id(user_code):
|
||||
# 用user_code获取id
|
||||
|
||||
user_id = None
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select user_id from user where user_code = :a''',
|
||||
{'a': user_code})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
user_id = x[0]
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def auth_required(request):
|
||||
def auth_required(req):
|
||||
# arcaea登录验证,写成了修饰器
|
||||
def decorator(view):
|
||||
@functools.wraps(view)
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
user_id = None
|
||||
with Connect() as c:
|
||||
headers = request.headers
|
||||
token = headers['Authorization']
|
||||
token = token[7:]
|
||||
user_id = token_get_id(token)
|
||||
headers = req.headers
|
||||
|
||||
if user_id is not None:
|
||||
return view(user_id, *args, **kwargs)
|
||||
else:
|
||||
return '''{"success":false,"error_code":108}'''
|
||||
e = header_check(req)
|
||||
if e is not None:
|
||||
current_app.logger.warning(
|
||||
f' - {e.error_code}|{e.api_error_code}: {e}')
|
||||
return error_return(e)
|
||||
|
||||
with Connect() as c:
|
||||
try:
|
||||
user = UserAuth(c)
|
||||
token = headers.get('Authorization')
|
||||
if not token:
|
||||
raise NoAccess('No token.', -4)
|
||||
user.token = token[7:]
|
||||
user_id = user.token_get_id()
|
||||
g.user = user
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return view(user_id, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
30
latest version/server/course.py
Normal file
30
latest version/server/course.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.constant import Constant
|
||||
from core.course import UserCourseList
|
||||
from core.item import ItemCore
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('course', __name__, url_prefix='/course')
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def course_me(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
core = ItemCore(c)
|
||||
core.item_id = 'core_course_skip_purchase'
|
||||
core.select_user_item(user)
|
||||
x = UserCourseList(c, user)
|
||||
x.select_all()
|
||||
return success_return({
|
||||
'courses': x.to_dict_list(),
|
||||
"stamina_cost": Constant.COURSE_STAMINA_COST,
|
||||
"course_skip_purchase_ticket": core.amount
|
||||
})
|
||||
44
latest version/server/friend.py
Normal file
44
latest version/server/friend.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline, code_get_id
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('friend', __name__, url_prefix='/friend')
|
||||
|
||||
|
||||
@bp.route('/me/add', methods=['POST']) # 加好友
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def add_friend(user_id):
|
||||
with Connect() as c:
|
||||
friend_code = request.form['friend_code']
|
||||
friend_id = code_get_id(c, friend_code)
|
||||
user = UserOnline(c, user_id)
|
||||
user.add_friend(friend_id)
|
||||
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": user.friends
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/delete', methods=['POST']) # 删好友
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def delete_friend(user_id):
|
||||
with Connect() as c:
|
||||
friend_id = int(request.form['friend_id'])
|
||||
user = UserOnline(c, user_id)
|
||||
user.delete_friend(friend_id)
|
||||
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": user.friends
|
||||
})
|
||||
119
latest version/server/func.py
Normal file
119
latest version/server/func.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from functools import wraps
|
||||
from traceback import format_exc
|
||||
|
||||
from flask import current_app, g, jsonify
|
||||
|
||||
from core.bundle import BundleParser
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError, LowVersion, 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)
|
||||
|
||||
|
||||
def error_return(e: ArcError = default_error): # 错误返回
|
||||
# -7 处理交易时发生了错误
|
||||
# -5 所有的曲目都已经下载完毕
|
||||
# -4 您的账号已在别处登录
|
||||
# -3 无法连接至服务器
|
||||
# 2 Arcaea服务器正在维护
|
||||
# 5 请更新 Arcaea 到最新版本
|
||||
# 9 新版本请等待几分钟
|
||||
# 11 有游戏内容需要更新,即将返回主界面
|
||||
# 100 无法在此ip地址下登录游戏
|
||||
# 101 用户名占用
|
||||
# 102 电子邮箱已注册
|
||||
# 103 已有一个账号由此设备创建
|
||||
# 104 用户名密码错误
|
||||
# 105 24小时内登入两台设备
|
||||
# 106 121 账户冻结
|
||||
# 107 你没有足够的体力
|
||||
# 113 活动已结束
|
||||
# 114 该活动已结束,您的成绩不会提交
|
||||
# 115 请输入有效的电子邮箱地址
|
||||
# 120 封号警告
|
||||
# 122 账户暂时冻结
|
||||
# 123 账户被限制
|
||||
# 124 你今天不能再使用这个IP地址创建新的账号
|
||||
# 150 非常抱歉您已被限制使用此功能
|
||||
# 151 目前无法使用此功能
|
||||
# 160 账户未邮箱认证,请检查邮箱
|
||||
# 161 账户认证过期,请重新注册
|
||||
# 401 用户不存在
|
||||
# 403 无法连接至服务器
|
||||
# 501 502 -6 此物品目前无法获取
|
||||
# 504 无效的序列码
|
||||
# 505 此序列码已被使用
|
||||
# 506 你已拥有了此物品
|
||||
# 601 好友列表已满
|
||||
# 602 此用户已是好友
|
||||
# 604 你不能加自己为好友
|
||||
# 903 下载量超过了限制,请24小时后重试
|
||||
# 905 请在再次使用此功能前等待24小时
|
||||
# 910 重新请求验证邮件前需等待x分钟 extra: retry_at
|
||||
# 1001 设备数量达到上限
|
||||
# 1002 此设备已使用过此功能
|
||||
# 1201 房间已满
|
||||
# 1202 房间号码无效
|
||||
# 1203 请将Arcaea更新至最新版本
|
||||
# 1205 此房间目前无法加入
|
||||
# 9801 下载歌曲时发生问题,请再试一次
|
||||
# 9802 保存歌曲时发生问题,请检查设备空间容量
|
||||
# 9803 下载已取消
|
||||
# 9905 没有在云端发现任何数据
|
||||
# 9907 更新数据时发生了问题
|
||||
# 9908 服务器只支持最新的版本,请更新 Arcaea
|
||||
# 其它 发生未知错误
|
||||
r = {"success": False, "error_code": e.error_code}
|
||||
if e.extra_data:
|
||||
r['extra'] = e.extra_data
|
||||
|
||||
return jsonify(r), e.status
|
||||
|
||||
|
||||
def success_return(value=None):
|
||||
r = {"success": True}
|
||||
if value is not None:
|
||||
r['value'] = value
|
||||
return jsonify(r)
|
||||
|
||||
|
||||
def arc_try(view):
|
||||
'''替代try/except,记录`ArcError`为warning'''
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
data = view(*args, **kwargs)
|
||||
if data is None:
|
||||
return error_return()
|
||||
return data
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
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 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 LowVersion('Invalid app version', 5)
|
||||
if request.method == 'GET' and 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version.get(headers.get('AppVersion', ''), '0.0.0'):
|
||||
return LowVersion('Invalid content bundle version', 11)
|
||||
|
||||
if has_arc_hash and not ArcHashChecker(request).check():
|
||||
return NoAccess('Invalid request')
|
||||
|
||||
return None
|
||||
@@ -1,355 +0,0 @@
|
||||
from server.sql import Connect
|
||||
import server.arcworld
|
||||
import server.arcpurchase
|
||||
import time
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_recent_score(c, user_id):
|
||||
# 得到用户最近一次的成绩,返回列表
|
||||
c.execute('''select * from user where user_id = :x''', {'x': user_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
if x[11] is not None:
|
||||
c.execute('''select best_clear_type from best_score where user_id=:u and song_id=:s and difficulty=:d''', {
|
||||
'u': user_id, 's': x[11], 'd': x[12]})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
best_clear_type = y[0]
|
||||
else:
|
||||
best_clear_type = x[21]
|
||||
|
||||
return [{
|
||||
"rating": x[22],
|
||||
"modifier": x[19],
|
||||
"time_played": x[20],
|
||||
"health": x[18],
|
||||
"best_clear_type": best_clear_type,
|
||||
"clear_type": x[21],
|
||||
"miss_count": x[17],
|
||||
"near_count": x[16],
|
||||
"perfect_count": x[15],
|
||||
"shiny_perfect_count": x[14],
|
||||
"score": x[13],
|
||||
"difficulty": x[12],
|
||||
"song_id": x[11]
|
||||
}]
|
||||
return []
|
||||
|
||||
|
||||
def get_user_character(c, user_id):
|
||||
# 得到用户拥有的角色列表,返回列表
|
||||
c.execute('''select * from user_char where user_id = :user_id''',
|
||||
{'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
s = []
|
||||
for i in x:
|
||||
char_name = ''
|
||||
c.execute(
|
||||
'''select name from character where character_id = :x''', {'x': i[1]})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
char_name = y[0]
|
||||
char = {
|
||||
"is_uncapped_override": int2b(i[14]),
|
||||
"is_uncapped": int2b(i[13]),
|
||||
"uncap_cores": [],
|
||||
"char_type": i[12],
|
||||
"skill_id_uncap": i[11],
|
||||
"skill_requires_uncap": int2b(i[10]),
|
||||
"skill_unlock_level": i[9],
|
||||
"skill_id": i[8],
|
||||
"overdrive": i[7],
|
||||
"prog": i[6],
|
||||
"frag": i[5],
|
||||
"level_exp": i[4],
|
||||
"exp": i[3],
|
||||
"level": i[2],
|
||||
"name": char_name,
|
||||
"character_id": i[1]
|
||||
}
|
||||
if i[1] == 21:
|
||||
char["voice"] = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
s.append(char)
|
||||
|
||||
return s
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def get_user_friend(c, user_id):
|
||||
# 得到用户的朋友列表,返回列表
|
||||
c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
|
||||
'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
s = []
|
||||
if x != [] and x[0][0] is not None:
|
||||
|
||||
for i in x:
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': i[0], 'y': user_id})
|
||||
if c.fetchone() == (1,):
|
||||
is_mutual = True
|
||||
else:
|
||||
is_mutual = False
|
||||
|
||||
c.execute('''select * from user where user_id = :x''', {'x': i[0]})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
character = y[6]
|
||||
is_char_uncapped = int2b(y[8])
|
||||
is_char_uncapped_override = int2b(y[9])
|
||||
if y[23] != -1:
|
||||
character = y[23]
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id=:a and character_id=:b''', {
|
||||
'a': i[0], 'b': character})
|
||||
z = c.fetchone()
|
||||
if z:
|
||||
is_char_uncapped = int2b(z[0])
|
||||
is_char_uncapped_override = int2b(z[1])
|
||||
|
||||
s.append({
|
||||
"is_mutual": is_mutual,
|
||||
"is_char_uncapped_override": is_char_uncapped_override,
|
||||
"is_char_uncapped": is_char_uncapped,
|
||||
"is_skill_sealed": int2b(y[7]),
|
||||
"rating": y[5],
|
||||
"join_date": int(y[3]),
|
||||
"character": character,
|
||||
"recent_score": get_recent_score(c, i[0]),
|
||||
"name": y[1],
|
||||
"user_id": i[0]
|
||||
})
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def get_user_singles(c, user_id):
|
||||
# 得到用户的单曲,返回列表
|
||||
c.execute('''select * from user_item where user_id = :user_id and type = "single"''',
|
||||
{'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return []
|
||||
|
||||
re = []
|
||||
for i in x:
|
||||
re.append(i[1])
|
||||
return re
|
||||
|
||||
|
||||
def get_user_packs(c, user_id):
|
||||
# 得到用户的曲包,返回列表
|
||||
c.execute('''select * from user_item where user_id = :user_id and type = "pack"''',
|
||||
{'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return []
|
||||
|
||||
re = []
|
||||
for i in x:
|
||||
re.append(i[1])
|
||||
return re
|
||||
|
||||
|
||||
def get_value_0(c, user_id):
|
||||
# 构造value id=0的数据,返回字典
|
||||
c.execute('''select * from user where user_id = :x''', {'x': user_id})
|
||||
x = c.fetchone()
|
||||
r = {}
|
||||
if x is not None:
|
||||
user_character = get_user_character(c, user_id)
|
||||
characters = []
|
||||
for i in user_character:
|
||||
characters.append(i['character_id'])
|
||||
prog_boost = 0
|
||||
if x[27] and x[27] != 0:
|
||||
prog_boost = 300
|
||||
|
||||
r = {"is_aprilfools": False,
|
||||
"curr_available_maps": [],
|
||||
"character_stats": user_character,
|
||||
"friends": get_user_friend(c, user_id),
|
||||
"settings": {
|
||||
"favorite_character": x[23],
|
||||
"is_hide_rating": int2b(x[10]),
|
||||
"max_stamina_notification_enabled": int2b(x[24])
|
||||
},
|
||||
"user_id": user_id,
|
||||
"name": x[1],
|
||||
"user_code": x[4],
|
||||
"display_name": x[1],
|
||||
"ticket": x[26],
|
||||
"character": x[6],
|
||||
"is_locked_name_duplicate": False,
|
||||
"is_skill_sealed": int2b(x[7]),
|
||||
"current_map": x[25],
|
||||
"prog_boost": prog_boost,
|
||||
"next_fragstam_ts": -1,
|
||||
"max_stamina_ts": 1586274871917,
|
||||
"stamina": 12,
|
||||
"world_unlocks": ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5"],
|
||||
"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"],
|
||||
"singles": get_user_singles(c, user_id),
|
||||
"packs": get_user_packs(c, user_id),
|
||||
"characters": characters,
|
||||
"cores": [],
|
||||
"recent_score": get_recent_score(c, user_id),
|
||||
"max_friend": 50,
|
||||
"rating": x[5],
|
||||
"join_date": int(x[3])
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_aggregate_small(user_id):
|
||||
# 返回用户数据
|
||||
r = {"success": False}
|
||||
with Connect() as c:
|
||||
r = {"success": True,
|
||||
"value": [{
|
||||
"id": 0,
|
||||
"value": get_value_0(c, user_id)
|
||||
}]}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_aggregate_big(user_id):
|
||||
# 返回用户数据和地图歌曲信息
|
||||
r = {"success": False}
|
||||
with Connect() as c:
|
||||
r = {"success": True,
|
||||
"value": [{
|
||||
"id": 0,
|
||||
"value": get_value_0(c, user_id)
|
||||
}, {
|
||||
"id": 1,
|
||||
"value": server.arcpurchase.get_item(c, 'pack')
|
||||
}, {
|
||||
"id": 2,
|
||||
"value": {}
|
||||
}, {
|
||||
"id": 3,
|
||||
"value": {
|
||||
"max_stamina": 12,
|
||||
"stamina_recover_tick": 1800000,
|
||||
"core_exp": 250,
|
||||
"curr_ts": int(time.time())*1000,
|
||||
"level_steps": [{
|
||||
"level": 1,
|
||||
"level_exp": 0
|
||||
}, {
|
||||
"level": 2,
|
||||
"level_exp": 50
|
||||
}, {
|
||||
"level": 3,
|
||||
"level_exp": 100
|
||||
}, {
|
||||
"level": 4,
|
||||
"level_exp": 150
|
||||
}, {
|
||||
"level": 5,
|
||||
"level_exp": 200
|
||||
}, {
|
||||
"level": 6,
|
||||
"level_exp": 300
|
||||
}, {
|
||||
"level": 7,
|
||||
"level_exp": 450
|
||||
}, {
|
||||
"level": 8,
|
||||
"level_exp": 650
|
||||
}, {
|
||||
"level": 9,
|
||||
"level_exp": 900
|
||||
}, {
|
||||
"level": 10,
|
||||
"level_exp": 1200
|
||||
}, {
|
||||
"level": 11,
|
||||
"level_exp": 1600
|
||||
}, {
|
||||
"level": 12,
|
||||
"level_exp": 2100
|
||||
}, {
|
||||
"level": 13,
|
||||
"level_exp": 2700
|
||||
}, {
|
||||
"level": 14,
|
||||
"level_exp": 3400
|
||||
}, {
|
||||
"level": 15,
|
||||
"level_exp": 4200
|
||||
}, {
|
||||
"level": 16,
|
||||
"level_exp": 5100
|
||||
}, {
|
||||
"level": 17,
|
||||
"level_exp": 6100
|
||||
}, {
|
||||
"level": 18,
|
||||
"level_exp": 7200
|
||||
}, {
|
||||
"level": 19,
|
||||
"level_exp": 8500
|
||||
}, {
|
||||
"level": 20,
|
||||
"level_exp": 10000
|
||||
}, {
|
||||
"level": 21,
|
||||
"level_exp": 11500
|
||||
}, {
|
||||
"level": 22,
|
||||
"level_exp": 13000
|
||||
}, {
|
||||
"level": 23,
|
||||
"level_exp": 14500
|
||||
}, {
|
||||
"level": 24,
|
||||
"level_exp": 16000
|
||||
}, {
|
||||
"level": 25,
|
||||
"level_exp": 17500
|
||||
}, {
|
||||
"level": 26,
|
||||
"level_exp": 19000
|
||||
}, {
|
||||
"level": 27,
|
||||
"level_exp": 20500
|
||||
}, {
|
||||
"level": 28,
|
||||
"level_exp": 22000
|
||||
}, {
|
||||
"level": 29,
|
||||
"level_exp": 23500
|
||||
}, {
|
||||
"level": 30,
|
||||
"level_exp": 25000
|
||||
}],
|
||||
"world_ranking_enabled": False,
|
||||
"is_byd_chapter_unlocked": True
|
||||
}
|
||||
}, {
|
||||
"id": 4,
|
||||
"value": server.arcpurchase.get_user_present(c, user_id)
|
||||
}, {
|
||||
"id": 5,
|
||||
"value": {
|
||||
"current_map": server.arcworld.get_current_map(user_id),
|
||||
"user_id": user_id,
|
||||
"maps": server.arcworld.get_world_all(user_id)
|
||||
}
|
||||
}
|
||||
]}
|
||||
|
||||
return r
|
||||
68
latest version/server/mission.py
Normal file
68
latest version/server/mission.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.error import NoData
|
||||
from core.mission import MISSION_DICT
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('mission', __name__, url_prefix='/mission')
|
||||
|
||||
|
||||
def parse_mission_form(multidict) -> list:
|
||||
r = []
|
||||
|
||||
x = multidict.get('mission_1')
|
||||
i = 1
|
||||
while x:
|
||||
r.append(x)
|
||||
x = multidict.get(f'mission_{i + 1}')
|
||||
i += 1
|
||||
return r
|
||||
|
||||
|
||||
@bp.route('/me/clear', methods=['POST']) # 新手任务确认完成
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def mission_clear(user_id):
|
||||
m = parse_mission_form(request.form)
|
||||
r = []
|
||||
for i, mission_id in enumerate(m):
|
||||
if mission_id not in MISSION_DICT:
|
||||
return NoData(f'Mission `{mission_id}` not found')
|
||||
with Connect() as c:
|
||||
x = MISSION_DICT[mission_id](c)
|
||||
x.user_clear_mission(UserOnline(c, user_id))
|
||||
d = x.to_dict()
|
||||
d['request_id'] = i + 1
|
||||
r.append(d)
|
||||
|
||||
return success_return({'missions': r})
|
||||
|
||||
|
||||
@bp.route('/me/claim', methods=['POST']) # 领取新手任务奖励
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def mission_claim(user_id):
|
||||
m = parse_mission_form(request.form)
|
||||
r = []
|
||||
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
|
||||
for i, mission_id in enumerate(m):
|
||||
if mission_id not in MISSION_DICT:
|
||||
return NoData(f'Mission `{mission_id}` not found')
|
||||
|
||||
x = MISSION_DICT[mission_id](c)
|
||||
x.user_claim_mission(user)
|
||||
d = x.to_dict(has_items=True)
|
||||
d['request_id'] = i + 1
|
||||
r.append(d)
|
||||
|
||||
return success_return({
|
||||
'missions': r,
|
||||
'user': user.to_dict()
|
||||
})
|
||||
172
latest version/server/multiplayer.py
Normal file
172
latest version/server/multiplayer.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError
|
||||
from core.linkplay import MatchStore, Player, RemoteMultiPlayer, Room
|
||||
from core.notification import RoomInviteNotification
|
||||
from core.sql import Connect
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('multiplayer', __name__, url_prefix='/multiplayer')
|
||||
|
||||
|
||||
@bp.route('/me/room/create', methods=['POST']) # 创建房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_create(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.get_song_unlock(request.json['clientSongMap'])
|
||||
x.create_room(user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/room/join/<room_code>', methods=['POST']) # 加入房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_join(user_id, room_code):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.get_song_unlock(request.json['clientSongMap'])
|
||||
room = Room()
|
||||
room.room_code = room_code
|
||||
x.join_room(room, user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/update', methods=['POST']) # 更新房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def multiplayer_update(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.token = int(request.json['token'])
|
||||
x.update_room(user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/room/<room_code>/invite', methods=['POST']) # 邀请
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_invite(user_id, room_code):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
other_user_id = request.form.get('to', type=int)
|
||||
|
||||
x = RemoteMultiPlayer()
|
||||
share_token = x.select_room(room_code=room_code)['share_token']
|
||||
|
||||
with Connect(in_memory=True) as c_m:
|
||||
with Connect() as c:
|
||||
sender = Player(c, user_id)
|
||||
sender.select_user_about_link_play()
|
||||
n = RoomInviteNotification.from_sender(
|
||||
sender, Player(c, other_user_id), share_token, c_m)
|
||||
n.insert()
|
||||
|
||||
return success_return({}) # 无返回
|
||||
|
||||
|
||||
@bp.route('/me/room/status', methods=['POST']) # 房间号码获取
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_status(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
share_token = request.form.get('shareToken', type=str)
|
||||
|
||||
x = RemoteMultiPlayer()
|
||||
room_code = x.select_room(share_token=share_token)['room_code']
|
||||
|
||||
return success_return({
|
||||
'roomId': room_code,
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/matchmaking/join/', methods=['POST']) # 匹配
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def matchmaking_join(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
user = Player(None, user_id)
|
||||
user.get_song_unlock(request.json['clientSongMap'])
|
||||
|
||||
x = MatchStore(c)
|
||||
x.init_player(user)
|
||||
r = x.match(user_id)
|
||||
|
||||
if r is None:
|
||||
return success_return({
|
||||
'userId': user_id,
|
||||
'status': 2,
|
||||
})
|
||||
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/matchmaking/status/', methods=['POST']) # 匹配状态,5s 一次
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def matchmaking_status(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
|
||||
r = MatchStore(c).match(user_id)
|
||||
if r is None:
|
||||
return success_return({
|
||||
'userId': user_id,
|
||||
'status': 0,
|
||||
})
|
||||
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/matchmaking/leave/', methods=['POST']) # 退出匹配
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def matchmaking_leave(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
MatchStore().clear_player(user_id)
|
||||
|
||||
return success_return({})
|
||||
205
latest version/server/others.py
Normal file
205
latest version/server/others.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from core.bundle import BundleDownload
|
||||
from core.character import UserCharacter
|
||||
from core.download import DownloadList
|
||||
from core.error import ArcError, RateLimit
|
||||
from core.item import ItemCharacter
|
||||
from core.notification import NotificationFactory
|
||||
from core.sql import Connect
|
||||
from core.system import GameInfo
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, error_return, success_return
|
||||
from .present import present_info
|
||||
from .purchase import bundle_bundle, bundle_pack, get_single
|
||||
from .score import song_score_friend
|
||||
from .user import user_me
|
||||
from .world import world_all
|
||||
|
||||
bp = Blueprint('others', __name__)
|
||||
|
||||
|
||||
@bp.route('/game/info', methods=['GET']) # 系统信息
|
||||
def game_info():
|
||||
return success_return(GameInfo().to_dict())
|
||||
|
||||
|
||||
@bp.route('/notification/me', methods=['GET']) # 通知
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def notification_me(user_id):
|
||||
with Connect(in_memory=True) as c_m:
|
||||
x = NotificationFactory(c_m, UserOnline(c_m, user_id))
|
||||
return success_return([i.to_dict() for i in x.get_notification()])
|
||||
|
||||
|
||||
@bp.route('/game/content_bundle', methods=['GET']) # 热更新
|
||||
@arc_try
|
||||
def game_content_bundle():
|
||||
# error code 5, 9 work
|
||||
app_version = request.headers.get('AppVersion')
|
||||
bundle_version = request.headers.get('ContentBundle')
|
||||
device_id = request.headers.get('DeviceId')
|
||||
with Connect(in_memory=True) as c_m:
|
||||
x = BundleDownload(c_m)
|
||||
x.set_client_info(app_version, bundle_version, device_id)
|
||||
return success_return({
|
||||
'orderedResults': x.get_bundle_list()
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def download_song(user_id):
|
||||
with Connect(in_memory=True) as c_m:
|
||||
with Connect() as c:
|
||||
x = DownloadList(c_m, UserOnline(c, user_id))
|
||||
x.song_ids = request.args.getlist('sid')
|
||||
x.url_flag = json.loads(request.args.get('url', 'true'))
|
||||
if x.url_flag and x.is_limited:
|
||||
raise RateLimit('You have reached the download limit.', 903)
|
||||
|
||||
x.add_songs()
|
||||
return success_return(x.urls)
|
||||
|
||||
|
||||
@bp.route('/finale/progress', methods=['GET'])
|
||||
def finale_progress():
|
||||
# 世界boss血条
|
||||
return success_return({'percentage': 100000})
|
||||
|
||||
|
||||
@bp.route('/finale/finale_start', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def finale_start(user_id):
|
||||
# testify开始,对立再见
|
||||
# 但是对立不再见
|
||||
|
||||
with Connect() as c:
|
||||
item = ItemCharacter(c)
|
||||
item.set_id('55') # Hikari (Fatalis)
|
||||
item.user_claim_item(UserOnline(c, user_id))
|
||||
return success_return({})
|
||||
|
||||
|
||||
@bp.route('/finale/finale_end', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def finale_end(user_id):
|
||||
|
||||
with Connect() as c:
|
||||
item = ItemCharacter(c)
|
||||
item.set_id('5') # Hikari & Tairitsu (Reunion)
|
||||
item.user_claim_item(UserOnline(c, user_id))
|
||||
return success_return({})
|
||||
|
||||
|
||||
@bp.route('/insight/me/complete/<string:pack_id>', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def insight_complete(user_id, pack_id):
|
||||
# insight state change
|
||||
with Connect() as c:
|
||||
u = UserOnline(c, user_id)
|
||||
if pack_id == 'eden_append_1':
|
||||
item = ItemCharacter(c)
|
||||
item.set_id('72') # Insight (Ascendant - 8th Seeker)
|
||||
item.user_claim_item(u)
|
||||
u.update_user_one_column('insight_state', 1)
|
||||
elif pack_id == 'lephon':
|
||||
u.update_user_one_column('insight_state', 3)
|
||||
else:
|
||||
raise ArcError('Invalid pack_id', 151, status=404)
|
||||
|
||||
return success_return({
|
||||
'insight_state': u.insight_state
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/unlock/me/awaken_maya', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def awaken_maya(user_id):
|
||||
with Connect() as c:
|
||||
ch = UserCharacter(c, 71, UserOnline(c, user_id))
|
||||
ch.select_character_info()
|
||||
try:
|
||||
ch.character_uncap()
|
||||
# ensure no error raised
|
||||
except ArcError:
|
||||
pass
|
||||
|
||||
return success_return({
|
||||
'user_id': user_id,
|
||||
'updated_characters': [ch.to_dict()]
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/applog/me/log', methods=['POST'])
|
||||
def applog_me():
|
||||
# 异常日志,不处理
|
||||
return success_return({})
|
||||
|
||||
|
||||
map_dict = {
|
||||
'/user/me': user_me,
|
||||
'/purchase/bundle/pack': bundle_pack,
|
||||
'/serve/download/me/song': download_song,
|
||||
'/game/info': game_info,
|
||||
'/present/me': present_info,
|
||||
'/world/map/me': world_all,
|
||||
'/score/song/friend': song_score_friend,
|
||||
'/purchase/bundle/bundle': bundle_bundle,
|
||||
'/finale/progress': finale_progress,
|
||||
'/purchase/bundle/single': get_single
|
||||
}
|
||||
|
||||
|
||||
@bp.route('/compose/aggregate', methods=['GET']) # 集成式请求
|
||||
def aggregate():
|
||||
try:
|
||||
# global request
|
||||
finally_response = {'success': True, 'value': []}
|
||||
# request_ = request
|
||||
get_list = json.loads(request.args.get('calls'))
|
||||
if len(get_list) > 10:
|
||||
# 请求太多驳回
|
||||
return error_return()
|
||||
|
||||
for i in get_list:
|
||||
endpoint = i['endpoint']
|
||||
request.args = ImmutableMultiDict(
|
||||
{key: value[0] for key, value in parse_qs(urlparse(endpoint).query).items()})
|
||||
|
||||
resp_t = map_dict[urlparse(endpoint).path]()
|
||||
if isinstance(resp_t, tuple):
|
||||
# The response may be a tuple, if it is an error response
|
||||
resp_t = resp_t[0]
|
||||
|
||||
if hasattr(resp_t, "response"):
|
||||
resp_t = resp_t.response[0].decode().rstrip('\n')
|
||||
resp = json.loads(resp_t)
|
||||
|
||||
if hasattr(resp, 'get') and resp.get('success') is False:
|
||||
finally_response = {'success': False, 'error_code': resp.get(
|
||||
'error_code'), 'id': i['id']}
|
||||
if "extra" in resp:
|
||||
finally_response['extra'] = resp['extra']
|
||||
# request = request_
|
||||
return jsonify(finally_response)
|
||||
|
||||
finally_response['value'].append(
|
||||
{'id': i.get('id'), 'value': resp['value'] if hasattr(resp, 'get') else resp})
|
||||
|
||||
# request = request_
|
||||
return jsonify(finally_response)
|
||||
except KeyError:
|
||||
return error_return()
|
||||
32
latest version/server/present.py
Normal file
32
latest version/server/present.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.present import UserPresent, UserPresentList
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('present', __name__, url_prefix='/present')
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET']) # 用户奖励信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def present_info(user_id):
|
||||
with Connect() as c:
|
||||
x = UserPresentList(c, UserOnline(c, user_id))
|
||||
x.select_user_presents()
|
||||
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/me/claim/<present_id>', methods=['POST']) # 礼物确认
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def claim_present(user_id, present_id):
|
||||
with Connect() as c:
|
||||
x = UserPresent(c, UserOnline(c, user_id))
|
||||
x.claim_user_present(present_id)
|
||||
|
||||
return success_return()
|
||||
156
latest version/server/purchase.py
Normal file
156
latest version/server/purchase.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.constant import Constant
|
||||
from core.error import InputError, ItemUnavailable, PostError
|
||||
from core.item import ItemFactory, Stamina6
|
||||
from core.purchase import Purchase, PurchaseList
|
||||
from core.redeem import UserRedeem
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('purchase', __name__, url_prefix='/purchase')
|
||||
|
||||
|
||||
@bp.route('/bundle/pack', methods=['GET']) # 曲包信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def bundle_pack(user_id):
|
||||
with Connect() as c:
|
||||
x = PurchaseList(c, UserOnline(c, user_id)
|
||||
).select_from_type('pack')
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/bundle/single', methods=['GET']) # 单曲购买信息获取
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def get_single(user_id):
|
||||
with Connect() as c:
|
||||
x = PurchaseList(c, UserOnline(c, user_id)
|
||||
).select_from_type('single')
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/bundle/bundle', methods=['GET']) # 捆绑包
|
||||
def bundle_bundle():
|
||||
return success_return([])
|
||||
# 感觉上是定死的,没意义啊
|
||||
# return success_return([{
|
||||
# "name": "chronicles",
|
||||
# "items": [{
|
||||
# "type": "bundle",
|
||||
# "id": "core"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "prelude"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "rei"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "yugamu"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "vs"
|
||||
# }],
|
||||
# "orig_price": 2000,
|
||||
# "price": 2000,
|
||||
# "discount_from": 1657152000000,
|
||||
# "discount_to": 1758447999000,
|
||||
# "discount_reason": "chronicle"
|
||||
# }])
|
||||
|
||||
|
||||
@bp.route('/me/pack', methods=['POST']) # 曲包和单曲购买
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def buy_pack_or_single(user_id):
|
||||
with Connect() as c:
|
||||
if 'pack_id' in request.form:
|
||||
purchase_name = request.form['pack_id']
|
||||
elif 'single_id' in request.form:
|
||||
purchase_name = request.form['single_id']
|
||||
else:
|
||||
return success_return()
|
||||
|
||||
x = Purchase(c, UserOnline(c, user_id)).select(purchase_name)
|
||||
x.buy()
|
||||
|
||||
return success_return({
|
||||
'user_id': x.user.user_id,
|
||||
'ticket': x.user.ticket,
|
||||
'packs': x.user.packs,
|
||||
'singles': x.user.singles,
|
||||
'characters': x.user.characters_list
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/item', methods=['POST']) # 特殊购买,world模式boost和stamina
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def buy_special(user_id):
|
||||
with Connect() as c:
|
||||
if 'item_id' not in request.form:
|
||||
raise PostError('`item_id` is required')
|
||||
item_id = request.form['item_id']
|
||||
|
||||
x = Purchase(c, UserOnline(c, user_id))
|
||||
x.purchase_name = item_id
|
||||
x.price = 50
|
||||
x.orig_price = 50
|
||||
x.discount_from = -1
|
||||
x.discount_to = -1
|
||||
x.items = [ItemFactory(c).get_item(item_id)]
|
||||
# request.form['ticket_used'] == 'true'
|
||||
# memory_boost_ticket: x-1
|
||||
x.buy()
|
||||
|
||||
r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}
|
||||
if item_id == 'stamina6':
|
||||
r['stamina'] = x.user.stamina.stamina
|
||||
r['max_stamina_ts'] = x.user.stamina.max_stamina_ts
|
||||
r['world_mode_locked_end_ts'] = -1
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/stamina/<buy_stamina_type>', methods=['POST']) # 购买体力
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def purchase_stamina(user_id, buy_stamina_type):
|
||||
with Connect() as c:
|
||||
if buy_stamina_type != 'fragment':
|
||||
raise InputError('Invalid type of buying stamina')
|
||||
|
||||
user = UserOnline(c, user_id)
|
||||
user.select_user_one_column('next_fragstam_ts', -1)
|
||||
now = int(time()*1000)
|
||||
if user.next_fragstam_ts > now:
|
||||
return ItemUnavailable('Buying stamina by fragment is not available yet.', 905)
|
||||
|
||||
user.update_user_one_column(
|
||||
'next_fragstam_ts', now + Constant.FRAGSTAM_RECOVER_TICK)
|
||||
s = Stamina6(c)
|
||||
s.user_claim_item(user)
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"stamina": user.stamina.stamina,
|
||||
"max_stamina_ts": user.stamina.max_stamina_ts,
|
||||
"next_fragstam_ts": user.next_fragstam_ts,
|
||||
'world_mode_locked_end_ts': -1
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/redeem', methods=['POST']) # 兑换码
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def redeem(user_id):
|
||||
with Connect() as c:
|
||||
x = UserRedeem(c, UserOnline(c, user_id))
|
||||
x.claim_user_redeem(request.form['code'])
|
||||
|
||||
return success_return({"coupon": "fragment" + str(x.fragment) if x.fragment > 0 else ""})
|
||||
180
latest version/server/score.py
Normal file
180
latest version/server/score.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from random import randint
|
||||
from time import time
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.constant import Constant
|
||||
from core.course import CoursePlay
|
||||
from core.error import InputError
|
||||
from core.rank import RankList
|
||||
from core.score import UserPlay
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('score', __name__, url_prefix='/score')
|
||||
|
||||
|
||||
@bp.route('/token', methods=['GET']) # 成绩上传所需的token,显然我不想验证
|
||||
def score_token():
|
||||
return success_return({'token': '1145141919810'})
|
||||
|
||||
|
||||
@bp.route('/token/world', methods=['GET']) # 世界模式成绩上传所需的token
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def score_token_world(user_id):
|
||||
|
||||
d = request.args.get
|
||||
|
||||
stamina_multiply = d('stamina_multiply', 1, type=int)
|
||||
fragment_multiply = d('fragment_multiply', 100, type=int)
|
||||
prog_boost_multiply = d('prog_boost_multiply', 0, type=int)
|
||||
beyond_boost_gauge_use = d('beyond_boost_gauge_use', 0, type=int)
|
||||
skill_cytusii_flag = None
|
||||
skill_chinatsu_flag = None
|
||||
skill_id = d('skill_id')
|
||||
|
||||
if (skill_id == 'skill_ilith_ivy' or skill_id == 'skill_hikari_vanessa') and d('is_skill_sealed') == 'false':
|
||||
# 处理 ivy 技能或者 vanessa 技能
|
||||
# TODO: 需要重构整个 user_play,世界模式 / 课题模式,所以现在临时 work 一下
|
||||
skill_cytusii_flag = ''.join([str(randint(0, 2)) for _ in range(5)])
|
||||
|
||||
if skill_id == 'skill_chinatsu' and d('is_skill_sealed') == 'false':
|
||||
skill_chinatsu_flag = ''.join([str(randint(0, 2)) for _ in range(7)])
|
||||
|
||||
skill_flag = skill_cytusii_flag or skill_chinatsu_flag
|
||||
|
||||
with Connect() as c:
|
||||
x = UserPlay(c, UserOnline(c, user_id))
|
||||
x.song.set_chart(d('song_id'), d('difficulty', type=int))
|
||||
x.set_play_state_for_world(
|
||||
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag, skill_chinatsu_flag)
|
||||
|
||||
r = {
|
||||
"stamina": x.user.stamina.stamina,
|
||||
"max_stamina_ts": x.user.stamina.max_stamina_ts,
|
||||
"token": x.song_token,
|
||||
'play_parameters': {},
|
||||
}
|
||||
if skill_flag and skill_id:
|
||||
r['play_parameters'] = {
|
||||
skill_id: list(
|
||||
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_flag)),
|
||||
}
|
||||
|
||||
if x.invasion_flag == 1:
|
||||
r['play_parameters']['invasion_start'] = True
|
||||
elif x.invasion_flag == 2:
|
||||
r['play_parameters']['invasion_hard'] = True
|
||||
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def score_token_course(user_id):
|
||||
with Connect() as c:
|
||||
use_course_skip_purchase = request.args.get(
|
||||
'use_course_skip_purchase', 'false') == 'true'
|
||||
|
||||
user = UserOnline(c, user_id)
|
||||
user_play = UserPlay(c, user)
|
||||
user_play.song_token = request.args.get('previous_token', None)
|
||||
user_play.get_play_state()
|
||||
|
||||
status = 'created'
|
||||
if user_play.course_play_state == -1:
|
||||
# 没有token,课题模式刚开始
|
||||
course_play = CoursePlay(c, user, user_play)
|
||||
course_play.course_id = request.args['course_id']
|
||||
user_play.course_play = course_play
|
||||
user_play.set_play_state_for_course(
|
||||
use_course_skip_purchase)
|
||||
elif 0 <= user_play.course_play_state <= 3:
|
||||
# 验证token
|
||||
user_play.update_token_for_course()
|
||||
else:
|
||||
# 课题模式已经结束
|
||||
user_play.clear_play_state()
|
||||
user.select_user_about_stamina()
|
||||
status = 'cleared' if user_play.course_play_state == 4 else 'failed'
|
||||
|
||||
return success_return({
|
||||
"stamina": user.stamina.stamina,
|
||||
"max_stamina_ts": user.stamina.max_stamina_ts,
|
||||
"token": user_play.song_token,
|
||||
'status': status
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/song', methods=['POST']) # 成绩上传
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_post(user_id):
|
||||
with Connect() as c:
|
||||
x = UserPlay(c, UserOnline(c, user_id))
|
||||
f = request.form
|
||||
x.song_token = f['song_token']
|
||||
x.song_hash = f['song_hash']
|
||||
x.song.set_chart(f['song_id'], f['difficulty'])
|
||||
x.set_score(f['score'], f['shiny_perfect_count'], f['perfect_count'], f['near_count'],
|
||||
f['miss_count'], f['health'], f['modifier'], int(time() * 1000), f['clear_type'])
|
||||
x.beyond_gauge = int(f['beyond_gauge'])
|
||||
x.submission_hash = f['submission_hash']
|
||||
x.combo_interval_bonus = f.get('combo_interval_bonus', type=int)
|
||||
x.hp_interval_bonus = f.get('hp_interval_bonus', type=int)
|
||||
# visible_map_count
|
||||
x.fever_bonus = f.get('fever_bonus', type=int)
|
||||
x.rank_bonus = f.get('rank_bonus', type=int)
|
||||
x.maya_gauge = f.get('maya_gauge', type=int)
|
||||
x.nextstage_bonus = f.get('nextstage_bonus', type=int)
|
||||
x.highest_health = f.get("highest_health", type=int)
|
||||
x.lowest_health = f.get("lowest_health", type=int)
|
||||
x.room_code = f.get('room_code')
|
||||
x.room_total_score = f.get('room_total_score', type=int)
|
||||
x.room_total_players = f.get('room_total_players', type=int)
|
||||
|
||||
if not x.is_valid:
|
||||
raise InputError('Invalid score.', 107)
|
||||
x.upload_score()
|
||||
return success_return(x.to_dict())
|
||||
|
||||
|
||||
@bp.route('/song', methods=['GET']) # TOP20
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_top(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_top()
|
||||
return success_return(rank_list.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/song/me', methods=['GET']) # 我的排名,默认最多20
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_me(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_me(UserOnline(c, user_id))
|
||||
return success_return(rank_list.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/song/friend', methods=['GET']) # 好友排名,默认最多50
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_friend(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_friend(UserOnline(c, user_id))
|
||||
return success_return(rank_list.to_dict_list())
|
||||
@@ -1,142 +0,0 @@
|
||||
from server.sql import Connect
|
||||
import server.info
|
||||
|
||||
|
||||
def b2int(x):
|
||||
# int与布尔值转换
|
||||
if x:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def change_char(user_id, character_id, skill_sealed):
|
||||
# 角色改变,包括技能封印的改变,返回成功与否的布尔值
|
||||
re = False
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
if skill_sealed == 'false':
|
||||
skill_sealed = False
|
||||
else:
|
||||
skill_sealed = True
|
||||
c.execute('''update user set is_skill_sealed = :a, character_id = :b, is_char_uncapped = :c, is_char_uncapped_override = :d where user_id = :e''', {
|
||||
'a': b2int(skill_sealed), 'b': character_id, 'c': x[0], 'd': x[1], 'e': user_id})
|
||||
|
||||
re = True
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def change_char_uncap(user_id, character_id):
|
||||
# 角色觉醒改变,返回字典
|
||||
r = None
|
||||
with Connect() as c:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
x = c.fetchone()
|
||||
|
||||
if x is not None and x[0] == 1:
|
||||
c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
|
||||
'a': b2int(x[1] == 0), 'b': user_id})
|
||||
c.execute('''update user_char set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||
'a': b2int(x[1] == 0), 'b': user_id, 'c': character_id})
|
||||
c.execute('''select * from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
y = c.fetchone()
|
||||
c.execute(
|
||||
'''select name from character where character_id = :x''', {'x': y[1]})
|
||||
z = c.fetchone()
|
||||
if z is not None:
|
||||
char_name = z[0]
|
||||
if y is not None:
|
||||
r = {
|
||||
"is_uncapped_override": int2b(y[14]),
|
||||
"is_uncapped": int2b(y[13]),
|
||||
"uncap_cores": [],
|
||||
"char_type": y[12],
|
||||
"skill_id_uncap": y[11],
|
||||
"skill_requires_uncap": int2b(y[10]),
|
||||
"skill_unlock_level": y[9],
|
||||
"skill_id": y[8],
|
||||
"overdrive": y[7],
|
||||
"prog": y[6],
|
||||
"frag": y[5],
|
||||
"level_exp": y[4],
|
||||
"exp": y[3],
|
||||
"level": y[2],
|
||||
"name": char_name,
|
||||
"character_id": y[1]
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_sys_set(user_id, value, set_arg):
|
||||
# 三个设置,PTT隐藏、体力满通知、最爱角色,无返回
|
||||
with Connect() as c:
|
||||
if 'favorite_character' in set_arg:
|
||||
value = int(value)
|
||||
c.execute('''update user set favorite_character = :a where user_id = :b''', {
|
||||
'a': value, 'b': user_id})
|
||||
|
||||
else:
|
||||
if value == 'false':
|
||||
value = False
|
||||
else:
|
||||
value = True
|
||||
|
||||
if 'is_hide_rating' in set_arg:
|
||||
c.execute('''update user set is_hide_rating = :a where user_id = :b''', {
|
||||
'a': b2int(value), 'b': user_id})
|
||||
if 'max_stamina_notification_enabled' in set_arg:
|
||||
c.execute('''update user set max_stamina_notification_enabled = :a where user_id = :b''', {
|
||||
'a': b2int(value), 'b': user_id})
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def arc_add_friend(user_id, friend_id):
|
||||
# 加好友,返回好友列表,或者是错误码602、604
|
||||
if user_id == friend_id:
|
||||
return 604
|
||||
|
||||
r = None
|
||||
with Connect() as c:
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into friend values(:a, :b)''',
|
||||
{'a': user_id, 'b': friend_id})
|
||||
r = server.info.get_user_friend(c, user_id)
|
||||
else:
|
||||
r = 602
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_delete_friend(user_id, friend_id):
|
||||
# 删好友,返回好友列表
|
||||
r = None
|
||||
with Connect() as c:
|
||||
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''delete from friend where user_id_me = :x and user_id_other = :y''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
|
||||
r = server.info.get_user_friend(c, user_id)
|
||||
|
||||
return r
|
||||
@@ -1,31 +0,0 @@
|
||||
import sqlite3
|
||||
from flask import current_app
|
||||
import traceback
|
||||
|
||||
|
||||
class Connect():
|
||||
# 数据库连接类,上下文管理
|
||||
|
||||
def __init__(self, file_path='./database/arcaea_database.db'):
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db
|
||||
接受:文件路径
|
||||
返回: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):
|
||||
if self.conn:
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
if exc_type is not None:
|
||||
current_app.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
|
||||
return True
|
||||
200
latest version/server/user.py
Normal file
200
latest version/server/user.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from flask import Blueprint, current_app, request
|
||||
|
||||
from core.character import UserCharacter
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError
|
||||
from core.item import ItemCore
|
||||
from core.operation import DeleteOneUser
|
||||
from core.save import SaveData
|
||||
from core.sql import Connect
|
||||
from core.user import User, UserLogin, UserOnline, UserRegister
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, header_check, success_return
|
||||
|
||||
bp = Blueprint('user', __name__, url_prefix='/user')
|
||||
|
||||
bp2 = Blueprint('account', __name__, url_prefix='/account')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST']) # 注册接口
|
||||
@bp2.route('', methods=['POST'])
|
||||
@arc_try
|
||||
def register():
|
||||
error = header_check(request)
|
||||
if error is not None:
|
||||
raise error
|
||||
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
new_user.set_name(request.form['name'])
|
||||
new_user.set_password(request.form['password'])
|
||||
new_user.set_email(request.form['email'])
|
||||
if 'device_id' in request.form:
|
||||
device_id = request.form['device_id']
|
||||
else:
|
||||
device_id = 'low_version'
|
||||
|
||||
ip = request.remote_addr
|
||||
new_user.register(device_id, ip)
|
||||
|
||||
# 注册后自动登录
|
||||
user = UserLogin(c)
|
||||
user.login(new_user.name, new_user.password, device_id, ip)
|
||||
current_app.logger.info(f'New user `{user.user_id}` registered')
|
||||
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET']) # 用户信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def user_me(user_id):
|
||||
with Connect() as c:
|
||||
return success_return(UserOnline(c, user_id).to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/toggle_invasion', methods=['POST']) # insight skill
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def toggle_invasion(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.toggle_invasion()
|
||||
return success_return({'user_id': user.user_id, 'insight_state': user.insight_state})
|
||||
|
||||
|
||||
@bp.route('/me/character', methods=['POST']) # 角色切换
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_change(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.change_character(
|
||||
int(request.form['character']), request.form['skill_sealed'] == 'true')
|
||||
|
||||
return success_return({'user_id': user.user_id, 'character': user.character.character_id})
|
||||
|
||||
|
||||
# 角色觉醒切换
|
||||
@bp.route('/me/character/<int:character_id>/toggle_uncap', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def toggle_uncap(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
character = UserCharacter(c, character_id)
|
||||
character.change_uncap_override(user)
|
||||
character.select_character_info(user)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()]})
|
||||
|
||||
|
||||
# 角色觉醒
|
||||
@bp.route('/me/character/<int:character_id>/uncap', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_first_uncap(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
character = UserCharacter(c, character_id)
|
||||
character.select_character_info(user)
|
||||
character.character_uncap(user)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
|
||||
|
||||
|
||||
# 角色使用以太之滴
|
||||
@bp.route('/me/character/<int:character_id>/exp', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_exp(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
character = UserCharacter(c, character_id)
|
||||
character.select_character_info(user)
|
||||
core = ItemCore(c)
|
||||
core.amount = - int(request.form['amount'])
|
||||
core.item_id = 'core_generic'
|
||||
character.upgrade_by_core(user, core)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
|
||||
|
||||
|
||||
@bp.route('/me/save', methods=['GET']) # 从云端同步
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def cloud_get(user_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
save = SaveData(c)
|
||||
save.select_all(user)
|
||||
return success_return(save.to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/save', methods=['POST']) # 向云端同步
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def cloud_post(user_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
save = SaveData(c)
|
||||
save.set_value(
|
||||
'scores_data', request.form['scores_data'], request.form['scores_checksum'])
|
||||
save.set_value(
|
||||
'clearlamps_data', request.form['clearlamps_data'], request.form['clearlamps_checksum'])
|
||||
save.set_value(
|
||||
'clearedsongs_data', request.form['clearedsongs_data'], request.form['clearedsongs_checksum'])
|
||||
save.set_value(
|
||||
'unlocklist_data', request.form['unlocklist_data'], request.form['unlocklist_checksum'])
|
||||
save.set_value(
|
||||
'installid_data', request.form['installid_data'], request.form['installid_checksum'])
|
||||
save.set_value('devicemodelname_data',
|
||||
request.form['devicemodelname_data'], request.form['devicemodelname_checksum'])
|
||||
save.set_value(
|
||||
'story_data', request.form['story_data'], request.form['story_checksum'])
|
||||
save.set_value(
|
||||
'finalestate_data', request.form.get('finalestate_data'), request.form.get('finalestate_checksum'))
|
||||
|
||||
save.update_all(user)
|
||||
return success_return({'user_id': user.user_id})
|
||||
|
||||
|
||||
@bp.route('/me/setting/<set_arg>', methods=['POST']) # 三个设置
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def sys_set(user_id, set_arg):
|
||||
with Connect() as c:
|
||||
value = request.form['value']
|
||||
user = UserOnline(c, user_id)
|
||||
if 'favorite_character' == set_arg:
|
||||
user.change_favorite_character(int(value))
|
||||
else:
|
||||
value = 'true' == value
|
||||
if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled', 'mp_notification_enabled'):
|
||||
user.update_user_one_column(set_arg, value)
|
||||
return success_return(user.to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/request_delete', methods=['POST']) # 删除账号
|
||||
@bp2.route('/me/request_delete', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def user_delete(user_id):
|
||||
if not Config.ALLOW_SELF_ACCOUNT_DELETE:
|
||||
raise ArcError('Cannot delete the account.', 151, status=404)
|
||||
DeleteOneUser().set_params(user_id).run()
|
||||
return success_return({'user_id': user_id})
|
||||
|
||||
|
||||
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
|
||||
@bp2.route('/email/resend_verify', methods=['POST'])
|
||||
@arc_try
|
||||
def email_resend_verify():
|
||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||
|
||||
|
||||
@bp.route('/verify', methods=['POST']) # 邮箱验证状态查询
|
||||
@bp2.route('/verify', methods=['POST'])
|
||||
@arc_try
|
||||
def email_verify():
|
||||
raise ArcError('Email verification unavailable.', 151, status=404)
|
||||
48
latest version/server/world.py
Normal file
48
latest version/server/world.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from flask import Blueprint, request
|
||||
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from core.world import MapParser, UserMap
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('world', __name__, url_prefix='/world')
|
||||
|
||||
|
||||
@bp.route('/map/me', methods=['GET']) # 获得世界模式信息,所有地图
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_all(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.select_user_about_current_map()
|
||||
return success_return({
|
||||
"current_map": user.current_map.map_id,
|
||||
"user_id": user_id,
|
||||
"maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in MapParser.get_world_all(c, user)]
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/map/me', methods=['POST']) # 进入地图
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_in(user_id):
|
||||
with Connect() as c:
|
||||
arcmap = UserMap(c, request.form['map_id'], UserOnline(c, user_id))
|
||||
if arcmap.unlock():
|
||||
return success_return(arcmap.to_dict())
|
||||
|
||||
|
||||
@bp.route('/map/me/<map_id>', methods=['GET']) # 获得单个地图完整信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_one(user_id, map_id):
|
||||
with Connect() as c:
|
||||
arcmap = UserMap(c, map_id, UserOnline(c, user_id))
|
||||
arcmap.change_user_current_map()
|
||||
return success_return({
|
||||
"user_id": user_id,
|
||||
"current_map": map_id,
|
||||
"maps": [arcmap.to_dict(has_map_info=True, has_steps=True)]
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
class Config():
|
||||
'''
|
||||
This is the setting file. You can change some parameters here.
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
主机的地址和端口号
|
||||
Host and port of your server
|
||||
'''
|
||||
HOST = '192.168.1.105'
|
||||
PORT = '80'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的用户名和密码
|
||||
Username and password of web background management page
|
||||
'''
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'admin'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的session秘钥,如果不知道是什么,请不要修改
|
||||
Session key of web background management page
|
||||
If you don't know what it is, please don't modify it.
|
||||
'''
|
||||
SECRET_KEY = '1145141919810'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
玩家歌曲下载的24小时次数限制,每个文件算一次
|
||||
Player's song download limit times in 24 hours, once per file
|
||||
'''
|
||||
DOWNLOAD_TIMES_LIMIT = 3000
|
||||
'''
|
||||
歌曲下载链接的有效时长,单位:秒
|
||||
Effective duration of song download link, unit: seconds
|
||||
'''
|
||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Arcaea登录的最大允许设备数量,最小值为1
|
||||
The maximum number of devices allowed to log in Arcaea, minimum: 1
|
||||
'''
|
||||
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||
'''
|
||||
是否允许同设备多应用共存登录
|
||||
If logging in from multiple applications on the same device is allowed
|
||||
'''
|
||||
ALLOW_LOGIN_SAME_DEVICE = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否记录详细的服务器日志
|
||||
If recording detailed server logs is enabled
|
||||
'''
|
||||
ALLOW_LOG_INFO = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user