71 Commits

Author SHA1 Message Date
Lost-MSth
5a4ff11578 Merge pull request #114 from Lost-MSth/dev
Dev
2023-05-25 22:44:22 +08:00
Lost-MSth
18e79a7799 Update to v2.11.2
- Unlock the character **Nami (Twilight)**
- Update README.md
2023-05-25 22:42:03 +08:00
Lost-MSth
e548d5639f [Enhance] API for getting PTT record
- Add an API endpoint for getting the user's rating records.
- more detailed INFO messages for  login and register
- Add `finale/finale_end` endpoint #110
2023-05-23 21:25:27 +08:00
Lost-MSth
bd74d96250 [Enhance] PTT record & log DB cleaner
- Add support for recording users' potential each day
- Add a log database cleaner tool
- A small change: `/user/<user_id> PUT` API can ban user now.
2023-05-03 00:37:41 +08:00
Lost-MSth
880b66a995 [Enhance] Add background task
- Add background task module for log database
- Update some data for Arcaea v4.4.4
2023-04-28 14:27:11 +08:00
Lost-MSth
8efd11cd02 [Bug fix] Character 5 value error
- Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. #100
2023-04-01 17:11:45 +08:00
Lost-MSth
1c58aeb526 Merge pull request #99 from Lost-MSth/dev
Update to v2.11.1
2023-03-24 19:11:32 +08:00
Lost-MSth
6f82e9b269 Update to v2.11.1 2023-03-24 19:09:47 +08:00
Lost-MSth
f92d8a9642 [Enhance] Skill of Mithra
- Add support for the skill of Mithra
2023-03-23 23:17:02 +08:00
Lost-MSth
1672d337ff [Enhance] [Refactor] Add API about characters
- Add API endpoints about characters
- Some small changes in refactoring
2023-03-22 22:27:21 +08:00
Lost-MSth
70d27be7c7 Merge pull request #94 from Lost-MSth/dev
Update to v2.11.0
2023-03-05 22:58:19 +08:00
Lost-MSth
8d856696ca Update to v2.11.0
- Change characters' value
2023-03-05 22:56:55 +08:00
Lost-MSth
ed8d0aa73c [Enhance] BYD boost & BYD chain maps
- Add support for beyond gauge boost
- Add support for beyond chain maps
- Add support skills of uncapped ilith and mika
- Unlock four chars and uncapped ilith
- Some changes in some values' algorithms
2023-03-02 23:25:38 +08:00
Lost-MSth
a84ec560dd [Enhance] API for redeem
- Add API endpoints for redeem system
- Continue to fix the bug mentioned in 930faf508d
2023-02-28 18:28:09 +08:00
Lost-MSth
930faf508d [Refactor] Refactor for link play
- Refactor simply for link play subprogram
- Fix a logic bug that the room without anyone can be entered
2023-02-27 23:41:32 +08:00
Lost-MSth
7ece2598d1 [Enhance] API for presents & character values
- Add API endpoints for presents
- Change character value algorithm
- Update character values (I forgot in v2.10.3)
2023-02-10 18:15:53 +08:00
Lost-MSth
6f39274b99 [Enhance] API for purchases, items, operations
- Add API endpoints for purchases, items, and operations
- Header checker? :)
2023-02-08 18:18:04 +08:00
Lost-MSth
e21cf89b1b Merge pull request #90 from Lost-MSth/dev
Dev
2023-01-26 21:19:32 +08:00
Lost-MSth
fbd5d83626 Update to v2.10.3 [Enhance] map local restrict
- Add support for locally restricting songs or challenges in the map of world mode
2023-01-26 21:15:09 +08:00
Lost-MSth
9fbdcd5edb [Refactor][Enhance] unlock items & Steps' difficulty restrict
- Refactor some codes about unlocking or locking some users' packs and singles
- Add support for restricting songs' difficulty in the map's steps of world mode
2023-01-24 16:45:09 +08:00
Lost-MSth
88d949fc18 [Bug fix] block headers without app version
- Fix a bug that headers without `AppVersion` are allowed in client version checking.
2023-01-22 22:09:15 +08:00
Lost-MSth
9636722709 [Enhance] Add API about songs
- Add some API endpoints, including creating, changing, deleting song info.
2023-01-22 18:39:15 +08:00
Lost-MSth
9c90d6ef89 [Enhance] Add an option about file hash pre-calc
- Add an option to disable song file hash pre-calculation
2023-01-14 18:32:53 +08:00
Lost-MSth
af03a48134 [Enhance] Cloud save update scores & capture auth error
- Restore the feature that cloud save can be used to cover best scores
- Capture error that the request does not have `Authorization` in header
2023-01-13 17:50:01 +08:00
Lost-MSth
1fde2801ad Merge pull request #84 from Lost-MSth/dev
Update to v2.10.2
2022-12-22 16:06:02 +08:00
Lost-MSth
f250701eca Update to v2.10.2
close #77 #83 #78 #64 #80 #81 #83
2022-12-22 16:02:44 +08:00
Lost-MSth
627985d1af [Doc] Game download link & some small changes 2022-12-19 20:38:37 +08:00
Lost-MSth
7324c9bd76 [Enhance] API song ranklist
- Add an API endpoint for getting the rank list of a song's chart #81
- Make the `run.bat` script pause when meeting an error #82
2022-12-19 20:20:31 +08:00
Lost-MSth
426f65ea9e [Enhance][Refactor] unranked score & warning log
- Add an option that can be used to forbid some illegal scores
- Add brief warning logs for custom exceptions
2022-11-28 21:58:06 +08:00
Lost-MSth
a7a9a4ba3d [Enhance][Bug fix] Add log db & log bug
- Add a log database to record all playing scores
- Fix a bug that if an exception is raised before flask app runs, logger will not work well.
2022-11-26 21:03:29 +08:00
Lost-MSth
84b0e869a5 [Refactor] Init files Encoding & Refresh rating
- Now initial files can be other encoding types which are supported by JSON module
- Code refactor for refreshing all scores' rating
2022-11-24 21:40:44 +08:00
Lost-MSth
bd9eb28832 Merge pull request #75 from Lost-MSth/dev
Update to v2.10.1
2022-11-10 18:33:50 +08:00
Lost-MSth
e3d5c19569 Update to v2.10.1 2022-11-10 18:32:53 +08:00
Lost-MSth
e1ce4d9ec4 [Enhance][Bug fix] download songlist & new skill & API null recent
- Add `amane` and add support for `skill_amane`
- Add a parser for `songlist` to specify downloadable files
- Fix a bug that users with no recent scores cannot get `recent30` via API
2022-11-10 18:00:27 +08:00
Lost-MSth
b70bfd6081 [Enhance] API user change
- Add a method of API to change the user's info
2022-11-07 22:52:42 +08:00
Lost-MSth
7cc17181e1 [Bug fix][Refactor] Course scores & Download
- Fix a bug that scores in course mode cannot calculate rating
- Make some optimization for downloading songs
2022-11-07 21:14:05 +08:00
Lost-MSth
e22485e4e0 [Bug fix] Course mode cannot start
- Fix a bug due to 4da3f38f6e that course mode cannot start
2022-11-06 15:06:01 +08:00
Lost-MSth
65aed56c3e Merge pull request #73 from Lost-MSth/dev
Dev
2022-11-05 21:33:57 +08:00
Lost-MSth
e6cb0f8981 Update README.md 2022-11-05 21:31:31 +08:00
Lost-MSth
4a5083c597 [Bug Fix][Enhance] Endless loop & waitress log
- Fix a bug due to 6fcca17918 that there is a endless loop in calculating recent 10 updating

- Add info log for `waitress`
2022-11-05 21:17:44 +08:00
Lost-MSth
78d28f8886 [Enhance][Bug fix] Waitress & API token
- Fix a bug that using API token in config file will not success
- Add deployment mode `waitress`
2022-11-03 13:42:39 +08:00
Lost-MSth
3fde61c639 [Bug fix] Hot fix for a funny thing
- Fix a bug that the order of the users on the score ranks is chaotic
2022-11-01 18:46:36 +08:00
Lost-MSth
cf350842ac [Refactor] Performance optimization
- Put download token in memory database
- Accelerate multiple querying in `best_score` table
2022-11-01 18:08:02 +08:00
Lost-MSth
e04c8ecf2d [Bug fix] Hot fix for typing error & import error 2022-10-31 19:29:31 +08:00
Lost-MSth
4da3f38f6e [Enhance] Performance optimization
- Change journal mode to WAL and enlarge cache size for sqlite3 database
- Use monkey patch to do asynchrony
- Make gevent use Flask's logger
2022-10-31 17:49:19 +08:00
Lost-MSth
40630fff4d [Refactor] Database initialization and migration
- Code refactoring for database initialization and migration
2022-10-21 18:19:37 +08:00
Lost-MSth
ca03360f0c [Enhance] API query param
- Add support for the `query` param in `GET` requests
2022-10-19 20:08:41 +08:00
Lost-MSth
d150553e6b [Enhance] Some config options & Login limiter
- Add limiter for login and API login
- Add some config options

- Delete `setting.py` files
2022-10-16 17:07:32 +08:00
Lost-MSth
ba36190f30 [Enhance][Bug fix] Improve setting file & ...
- Fix a small bug that `best30` of API cannot have scores whose songs are not in database
- At present the setting file can be a module or a file with some of options
- Limiter can have multiple rules together now
2022-10-16 15:49:49 +08:00
Lost-MSth
68a83a29d2 [Enhance] Add limiter & Building API
- Add a custom limiter and use it for limiting users' download instead of using database
> So add a requirement `limits`.

- Fix a character's value
- Change the email max length to 64
- Change something about API's roles and powers
- Add an API endpoint for getting users' roles and powers
2022-10-12 15:27:45 +08:00
Lost-MSth
2a3ee0f0c7 Merge pull request #69 from Lost-MSth/dev
Update to v2.10.0
2022-10-04 18:55:55 +08:00
Lost-MSth
a04df8bba6 Update README.md 2022-10-04 18:53:40 +08:00
Lost-MSth
fb110fd01c Update to v2.10.0 2022-10-04 18:51:49 +08:00
Lost-MSth
7a614eaa57 [Enhance] Add download error status code 2022-10-04 00:28:19 +08:00
Lost-MSth
8f1c28eb5c [Enhance] API & LinkPlay & something else
- Add song names for getting the score list (For API)
- Try to ensure thread safety in Link Play (twice again)
- Small change about database timeout
- Have a try to use `nginx` and `gevent` (Yes. I ensure it works well. This will not be viewed as a formal and necessary update.)
2022-10-03 23:46:52 +08:00
Lost-MSth
299c8c582c [Bug Fix][Enhance] About Link Play
- Fix a bug that the room host will be changed late when finishing a song
- Add logging module for linkplay server
2022-09-19 23:13:51 +08:00
Lost-MSth
6298b37ea2 Rearrange Link Play module
- Try to ensure thread safety by using TCP socket
2022-09-19 01:46:01 +08:00
Lost-MSth
b1575af0db Update the tool update_song.py 2022-09-10 20:34:21 +08:00
Lost-MSth
7bf43c3e07 Database performance optimization for large quantities
- Add two indices for avoiding possible slow queries

> This is an experimental handle for the database which has some tables over one hundred thousand lines.
2022-08-29 22:41:31 +08:00
Lost-MSth
12cd187563 Fix my rank
- ditto
2022-08-25 14:57:31 +08:00
Lost-MSth
574d2c9d9f Fix my rank
- Fix a bug that `my rank` doesn't work correctly (There may still be the problem)
2022-08-25 13:40:16 +08:00
Lost-MSth
732947a3f0 Fix some bugs
- Second try to fix a bug about API
- Fix a bug that the `prog boost` shows the incorrect value
- Fix a bug that the rewards can be get repeatedly in World Mode
- Fix a bug that second tube of blood of the world boss won't change
2022-08-11 22:38:26 +08:00
Lost-MSth
27ee147989 Fix two small bugs
- Fix a bug that the level of the character can exceed the max level
- Fix a bug that the response is incorrect when upgrading the characters by `generic core`
2022-08-03 22:00:42 +08:00
Lost-MSth
b38dbf1a55 Fix two small bugs
- Fix a bug that something of the finale challenge cannot be unlocked
- Fix a bug that the users' items will not be zero, which will disturb some purchase operations
2022-08-03 15:44:46 +08:00
Lost-MSth
d852be8029 Fix a bug about course system
- Fix a bug that the hidden courses cannot appear even if their requirements are satisfied
2022-07-30 17:55:01 +08:00
Lost-MSth
4f52df35ce Fix a small bug
- Fix a bug that the clients under v4.0.0 cannot upload the cloud save
2022-07-29 21:31:38 +08:00
Lost-MSth
cbd2198e43 Update issue templates 2022-07-29 21:22:47 +08:00
Lost-MSth
cdf76fb93a Just add something for unlocking songs
#62
2022-07-23 19:03:22 +08:00
Lost-MSth
5fba93524c Add a new feature
- Add support for specifying the songs of which only the `beyond` chart should be served #60
2022-07-22 17:20:13 +08:00
Lost-MSth
93f4ad4999 Fix some bugs
- Fix a bug that `GET` requests without data will report an error in API
- Fix a bug that `aggregate` requests will get an error when the inner function raises an error
- Fix a bug that the charts of a course cannot be the same because of the incorrect primary keys
- Fix a bug that global ranking scores cannot be calculated if there are no chart in the database #61
- The first try to fix #60 (I don't think it can be done so easily.)
2022-07-20 21:59:26 +08:00
Lost-MSth
47f05cdf1e Fix a bug and add a new thing
- Add support for logging Arcaea's errors
- Fix a bug when world maps' data don't have some unnecessary parts the client of iOS may break down
2022-07-16 19:50:07 +08:00
88 changed files with 6041 additions and 3074 deletions

42
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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
View 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...

18
.gitignore vendored
View File

@@ -1,5 +1,23 @@
# log files
*.log *.log
*.log.*
# SSL cert
*.pem
*.key
# sqlite3 database
*.db
*.db.bak*
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# setting/config files
latest version/config/
latest version/config.py
# song data
latest version/database/songs/

114
README.md
View File

@@ -1,7 +1,9 @@
# Arcaea-server # Arcaea-server
一个微型的Arcaea本地服务器 A small local server for Arcaea 一个微型的Arcaea本地服务器 A small local server for Arcaea
## 简介 Introduction ## 简介 Introduction
这是基于Python以及Flask的微型本地Arcaea服务器可以模拟游戏的主要功能。这可能是我第一次写这种大程序若有不妥之处敬请谅解。 这是基于Python以及Flask的微型本地Arcaea服务器可以模拟游戏的主要功能。这可能是我第一次写这种大程序若有不妥之处敬请谅解。
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。 本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
@@ -14,71 +16,92 @@ This procedure is mainly used for study and research, and shall not be used for
> It looks stupid, but it works! > It looks stupid, but it works!
## 特性 Features ## 特性 Features
有以下 We have
:x: : 不支持 Not supported
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
:construction: : 建设中 In construction
- 登录、注册 Login and registration - 登录、注册 Login and registration
- 多设备登录 Multi device login - 多设备自动封号 Auto-ban of multiple devices
- :warning: 多设备登录 Multi device login
- 登录频次限制 Login rate limit
- :x: 销号 Destroy account
- 成绩上传 Score upload - 成绩上传 Score upload
- PTT - 成绩校验 Score check
- 世界排名 Global rank - 成绩排名 Score rank
- 排名 Rank - 潜力值机制 Potential
- Best 30
- :warning: Recent Top 10
- :warning: 世界排名 Global rank
- 段位系统 Course system - 段位系统 Course system
- Link Play - :warning: Link Play
- 好友系统 Friends - 好友系统 Friends
- 数据同步 Data synchronization - :x: 好友位提升 Max friend number increase
- 爬梯 Climbing steps - 云端存档 Cloud save
- 自定义世界模式 Customizable World Mode - 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
- 自定义歌曲下载 Customizable songs download - 世界模式 World mode
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless) - 体力系统 Stamina system
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
- :warning: 角色技能 Character skills
- 歌曲下载 Songs downloading
- :x: 加密下载 Encrypted downloading
- 下载校验 Download check
- 下载频次限制 Download rate limit
- 购买系统 Purchase system
- 单曲和曲包 Single & Pack
- :x: 捆绑包 Bundle
- 折扣 Discount
- 五周年兑换券 5-th anniversary ticket
- :x: Extend 包自动降价 Extend pack automatic price reduction
- 奖励系统 Present system - 奖励系统 Present system
- 兑换码系统 Redeem code system - 兑换码系统 Redeem code system
- 角色系统 Character system - 角色系统 Character system
- 全剧情解锁 Unlock all the stories - 数据记录 Data recording
- 后台查询 Background search - 用户成绩 Users' scores
- 后台自定义信息 Customize some things in the background - 用户每日潜力值 Users' daily potential
- 成绩校验 Score check - :wastebasket: 简单的网页管理后台 Simple web admin backend
- 下载校验 Download check - :construction: API
- 服务器日志 Server log - 服务器日志 Server log
没有以下 We don't have
- 服务器安全性保证 Server security assurance
可能有问题 There may be problems
- Recent 30
- 一些歌曲的解锁 Some songs' unlocking
- 同设备多共存登录 Multiple app logins on the same device
## 说明 Statement ## 说明 Statement
只是很有趣,用处探索中。 只是很有趣,用处探索中。
It is just so interesting. What it can do is under exploration. It is just so interesting. What it can do is under exploration.
## 下载 Download ## 下载 Download
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases) [这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
[Arcaea-CN official](https://arcaea.lowiro.com/zh) [Arcaea-CN official](https://arcaea.lowiro.com/zh)
[Arcaea-lowi.ro](https://lowi.ro) [Arcaea-Konmai Academy](https://616.sb)
[Arcaea-RhyDown](https://rhydown.com)
## 更新日志 Update log ## 更新日志 Update log
只保留最新版本 Only keep the latest version. 只保留最新版本 Only keep the latest version.
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。 > 提醒:更新时请注意保留原先的数据库,以防数据丢失。
>
> Tips: When updating, please keep the original database in case of data loss. > Tips: When updating, please keep the original database in case of data loss.
>
> 其它小改动请参考各个 commit 信息
> Please refer to the commit messages for other minor changes.
### Version 2.11.2
### Version 2.9.1 - 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6
- 适用于Arcaea 4.0.255版本 For Arcaea 4.0.255 - 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**.
-搭档 **光&对立(Reunion)** 已解锁 Unlock the character **Hikari & Tairitsu(Reunion)**. -增用户潜力值每日记录功能 Add support for recording users' potential each day.
- 修复角色数值问题 Fix a bug that the characters have wrong values. - 修复搭档 **光 & 对立Reunion** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100)
- 修复用户物品相关问题 Fix a bug about users' items. - 添加 `finale/finale_end` 接口尝试修复最终挑战无法解锁结局的问题 Add the `finale/finale_end` endpoint to try to fix the problem that the endings cannot be unlocked correctly in the finale challenge. (#110)
- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records.
## 运行环境与依赖 Running environment and requirements ## 运行环境与依赖 Running environment and requirements
- Windows/Linux/Mac OS/Android...
- Python 3 - Windows / Linux / Mac OS / Android...
- Flask module >= 2.0, Cryptography module - Python >= 3.6
- Flask >= 2.0
- Cryptography >= 3.0.0
- limits >= 2.7.0
- Charles, IDA, proxy app... (optional) - Charles, IDA, proxy app... (optional)
<!-- <!--
@@ -88,34 +111,41 @@ It is just so interesting. What it can do is under exploration.
--> -->
## 使用说明 Instruction for use ## 使用说明 Instruction for use
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use)
## 注意 Attentions ## 注意 Attentions
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F) [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
## Q&A ## Q&A
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
[中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
## 鸣谢 Thanks ## 鸣谢 Thanks
~~歌曲数据库来自 Using song database from
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~ 歌曲数据库来自 Using song database from
~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
[ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase)
> 从v2.9开始不再提供歌曲数据 > 从v2.9开始不再提供歌曲数据
> Since v2.9, song data will not be provided. > Since v2.9, song data will not be provided.
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369) 网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
## 联系方式 Contact ## 联系方式 Contact
如有必要,可以联系本人 Contact me if necessary 如有必要,可以联系本人 Contact me if necessary
邮箱 Emailth84292@foxmail.com 邮箱 Emailth84292@foxmail.com
## 支持一下 Support me ## 支持一下 Support me
生活不易。 Life is not easy. 生活不易。 Life is not easy.
[支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg) [支付宝 Alipay](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/Alipay.jpg)
[微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png) [微信 WeChat](https://github.com/Lost-MSth/Arcaea-server/blob/master/pic/WeChat.png)
## 使用许可 Use license ## 使用许可 Use license
[MIT](LICENSE) © Lost [MIT](LICENSE) © Lost

View File

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

View File

@@ -1,19 +1,26 @@
import functools 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.api_user import APIUser
from core.error import ArcError, NoAccess, PostError from core.config_manager import Config
from core.error import ArcError, InputError, NoAccess, PostError
from core.sql import Connect from core.sql import Connect
from setting import Config
from .api_code import error_return from .api_code import error_return
from .constant import Constant
def role_required(request, powers=[]): def role_required(request, powers=[]):
'''api token验证写成了修饰器''' '''api token验证写成了修饰器'''
def decorator(view): def decorator(view):
@functools.wraps(view) @wraps(view)
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):
try: try:
if request.data:
request.json # 检查请求json格式 request.json # 检查请求json格式
except: except:
return error_return(PostError('Payload must be a valid json', api_error_code=-1), 400) return error_return(PostError('Payload must be a valid json', api_error_code=-1), 400)
@@ -22,20 +29,17 @@ def role_required(request, powers=[]):
return error_return(PostError('No token', api_error_code=-1), 401) return error_return(PostError('No token', api_error_code=-1), 401)
user = APIUser() user = APIUser()
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
user.user_id = 0
elif powers == []:
# 无powers则非本地权限API_TOKEN规定的无法访问
return error_return(NoAccess('No permission', api_error_code=-1), 403)
else:
with Connect() as c: with Connect() as c:
try:
user.c = c user.c = c
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
user.set_role_system()
else:
try:
user.select_user_id_from_api_token( user.select_user_id_from_api_token(
request.headers['Token']) request.headers['Token'])
user.select_role_and_powers() user.select_role_and_powers()
if not any([y in [x.power_name for x in user.role.powers] for y in powers]): if not any(user.role.has_power(y) for y in powers):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
except ArcError as e: except ArcError as e:
return error_return(e, 401) return error_return(e, 401)
@@ -46,30 +50,74 @@ def role_required(request, powers=[]):
return decorator return decorator
def request_json_handle(request, required_keys=[], optional_keys=[]): def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False):
''' '''
提取post参数返回dict写成了修饰器\ 提取post参数返回dict写成了修饰器
parameters: \
`request`: `Request` - 当前请求\ parameters:
`required_keys`: `list` - 必须的参数\ `request`: `Request` - 当前请求
`required_keys`: `list` - 必须的参数
`optional_keys`: `list` - 可选的参数 `optional_keys`: `list` - 可选的参数
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
''' '''
def decorator(view): def decorator(view):
@functools.wraps(view) @wraps(view)
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):
data = {} data = {}
for key in required_keys: if request.data:
if key not in request.json: json_data = request.get_json()
return error_return(PostError('Missing parameter: ' + key, api_error_code=-100)) else:
data[key] = request.json[key] 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: for key in optional_keys:
if key in request.json: if key in json_data:
data[key] = request.json[key] data[key] = json_data[key]
if must_change and not data:
return error_return(InputError('No change', api_error_code=-100))
return view(data, *args, **kwargs) return view(data, *args, **kwargs)
return wrapped_view return wrapped_view
return decorator 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

View File

@@ -1,25 +1,37 @@
from core.error import ArcError
from flask import jsonify from flask import jsonify
from core.error import ArcError
default_error = ArcError('Unknown Error') default_error = ArcError('Unknown Error')
CODE_MSG = { CODE_MSG = {
0: '', 0: '',
-1: 'See status code', -1: 'See status code', # 基础错误
-2: 'No data', -2: 'No data',
-3: 'No data or user', -3: 'No data or user', # 不确定是无数据还是无用户
-4: 'No user_id', -4: 'Data exist',
-100: 'Wrong post data', -100: 'Invalid post data', # 1xx数据错误
-101: 'Wrong data type', -101: 'Invalid data type',
-102: 'Wrong query parameter', -102: 'Invalid query parameter',
-103: 'Wrong sort parameter', -103: 'Invalid sort parameter',
-104: 'Wrong sort order parameter', -104: 'Invalid sort order parameter',
-200: 'No permission', -105: 'Invalid URL parameter',
-110: 'Invalid user_id',
-120: 'Invalid item type',
-121: 'No such item',
-122: 'Item already exists',
-123: 'The collection already has this item',
-124: 'The collection does not have this item',
-130: 'No such character',
-131: 'Invalid skill ID',
-200: 'No permission', # 2xx用户相关错误
-201: 'Wrong username or password', -201: 'Wrong username or password',
-202: 'User is banned', -202: 'User is banned',
-203: 'Username exists', -203: 'Too many login attempts',
-204: 'Email address exists', -210: 'Username exists',
-211: 'Email address exists',
-212: 'User code exists',
-999: 'Unknown error' -999: 'Unknown error'
} }

View 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())

View File

@@ -1,2 +1,7 @@
class Constant: class Constant:
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort'] QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
PATCH_KEYS = ['create', 'update', 'remove']
SKILL_IDS = ['gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight',
'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa']

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

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
from core.error import ArcError, NoData
from core.song import Song
from core.sql import Connect, Query, Sql
from flask import Blueprint, request from flask import Blueprint, request
from .api_auth import request_json_handle, role_required from core.error import DataExist, InputError, NoData
from .api_code import error_return, success_return 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 from .constant import Constant
bp = Blueprint('songs', __name__, url_prefix='/songs') bp = Blueprint('songs', __name__, url_prefix='/songs')
@@ -12,28 +14,58 @@ bp = Blueprint('songs', __name__, url_prefix='/songs')
@bp.route('/<string:song_id>', methods=['GET']) @bp.route('/<string:song_id>', methods=['GET'])
@role_required(request, ['select', 'select_song_info']) @role_required(request, ['select', 'select_song_info'])
@api_try
def songs_song_get(user, song_id): def songs_song_get(user, song_id):
'''查询歌曲信息''' '''查询歌曲信息'''
with Connect() as c: with Connect() as c:
try:
s = Song(c, song_id).select() s = Song(c, song_id).select()
return success_return(s.to_dict()) return success_return(s.to_dict())
except ArcError as e:
return error_return(e)
return error_return() @bp.route('/<string:song_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['name', 'charts'], must_change=True)
@api_try
def songs_song_put(data, user, song_id):
'''修改歌曲信息'''
with Connect() as c:
s = Song(c, song_id).select()
if 'name' in data:
s.name = str(data['name'])
if 'charts' in data:
for i in data['charts']:
if 'difficulty' in i and 'chart_const' in i:
s.charts[i['difficulty']].defnum = round(
i['chart_const'] * 10)
s.update()
return success_return(s.to_dict())
@bp.route('/<string:song_id>', methods=['DELETE'])
@role_required(request, ['change'])
@api_try
def songs_song_delete(user, song_id):
'''删除歌曲信息'''
with Connect() as c:
s = Song(c, song_id)
if not s.select_exists():
raise NoData(f'No such song: `{song_id}`')
s.delete()
return success_return()
@bp.route('', methods=['GET']) @bp.route('', methods=['GET'])
@role_required(request, ['select', 'select_song_info']) @role_required(request, ['select', 'select_song_info'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS) @request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def songs_get(data, user): def songs_get(data, user):
'''查询全歌曲信息''' '''查询全歌曲信息'''
A = ['song_id', 'name'] A = ['song_id', 'name']
B = ['song_id', 'name', 'rating_pst', B = ['song_id', 'name', 'rating_pst',
'rating_prs', 'rating_ftr', 'rating_byn'] 'rating_prs', 'rating_ftr', 'rating_byn']
with Connect() as c: with Connect() as c:
try: query = Query(A, A, B).from_dict(data)
query = Query(A, A, B).from_data(data)
x = Sql(c).select('chart', query=query) x = Sql(c).select('chart', query=query)
r = [] r = []
for i in x: for i in x:
@@ -43,6 +75,41 @@ def songs_get(data, user):
raise NoData(api_error_code=-2) raise NoData(api_error_code=-2)
return success_return([x.to_dict() for x in r]) return success_return([x.to_dict() for x in r])
except ArcError as e:
return error_return(e)
return error_return() @bp.route('', methods=['POST'])
@role_required(request, ['change'])
@request_json_handle(request, ['song_id', 'charts'], ['name'])
@api_try
def songs_post(data, user):
'''添加歌曲信息'''
with Connect() as c:
s = Song(c).from_dict(data)
if s.select_exists():
raise DataExist(f'Song `{s.song_id}` already exists')
s.insert()
return success_return(s.to_dict())
@bp.route('/<string:song_id>/<int:difficulty>/rank', methods=['GET'])
@role_required(request, ['select', 'select_song_rank', 'select_song_rank_top'])
@request_json_handle(request, optional_keys=['limit'])
@api_try
def songs_song_difficulty_rank_get(data, user, song_id, difficulty):
'''查询歌曲某个难度的成绩排行榜和游戏内接口相似只允许limit'''
if difficulty not in [0, 1, 2, 3]:
raise InputError('Difficulty must be 0, 1, 2 or 3')
limit = data.get('limit', 20)
if not isinstance(limit, int):
raise InputError('Limit must be int')
if user.role.only_has_powers(['select_song_rank_top'], ['select', 'select_song_rank']):
# 限制低权限只能查询前20名
if limit > 20 or limit < 0:
limit = 20
with Connect() as c:
rank_list = RankList(c)
rank_list.song.set_chart(song_id, difficulty)
rank_list.limit = limit
# 不检查歌曲是否存在,不存在返回的是空列表
rank_list.select_top()
return success_return(rank_list.to_dict_list())

View File

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

View File

@@ -1,43 +1,44 @@
from base64 import b64decode from base64 import b64decode
from core.api_user import APIUser from flask import Blueprint, current_app, request
from core.error import ArcError, PostError
from core.sql import Connect
from flask import Blueprint, request
from .api_auth import request_json_handle, role_required from core.api_user import APIUser
from .api_code import error_return, success_return 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 = Blueprint('token', __name__, url_prefix='/token')
@bp.route('', methods=['POST']) @bp.route('', methods=['POST'])
@request_json_handle(request, required_keys=['auth']) @request_json_handle(request, required_keys=['auth'])
@api_try
def token_post(data): def token_post(data):
''' '''
登录获取token\ 登录获取token
{'auth': base64('<user_id>:<password>')} {'auth': base64('<user_id>:<password>')}
''' '''
try: try:
auth_decode = bytes.decode(b64decode(data['auth'])) auth_decode = bytes.decode(b64decode(data['auth']))
except: except Exception as e:
return error_return(PostError(api_error_code=-100)) raise PostError(api_error_code=-100) from e
if not ':' in auth_decode: if ':' not in auth_decode:
return error_return(PostError(api_error_code=-100)) raise PostError(api_error_code=-100)
name, password = auth_decode.split(':', 1) name, password = auth_decode.split(':', 1)
with Connect() as c: with Connect() as c:
try:
user = APIUser(c) user = APIUser(c)
user.login(name, password, request.remote_addr) user.login(name, password, request.remote_addr)
return success_return({'token': user.token, 'user_id': user.user_id}) current_app.logger.info(f'API user `{user.user_id}` log in')
except ArcError as e: return success_return({'token': user.api_token, 'user_id': user.user_id})
return error_return(e)
return error_return()
@bp.route('', methods=['GET']) @bp.route('', methods=['GET'])
@role_required(request, ['select_me', 'select']) @role_required(request, ['select_me', 'select'])
@api_try
def token_get(user): def token_get(user):
'''判断登录有效性''' '''判断登录有效性'''
return success_return() return success_return()
@@ -45,13 +46,10 @@ def token_get(user):
@bp.route('', methods=['DELETE']) @bp.route('', methods=['DELETE'])
@role_required(request, ['change_me', 'select_me', 'select']) @role_required(request, ['change_me', 'select_me', 'select'])
@api_try
def token_delete(user): def token_delete(user):
'''登出''' '''登出'''
with Connect() as c: with Connect() as c:
try:
user.c = c user.c = c
user.logout() user.logout()
return success_return() return success_return()
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,10 +1,14 @@
from core.error import ArcError, InputError, NoAccess, NoData
from core.score import Potential, UserScoreList
from core.sql import Connect, Query, Sql
from core.user import UserInfo, UserRegister
from flask import Blueprint, request from flask import Blueprint, request
from .api_auth import request_json_handle, role_required 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 .api_code import error_return, success_return
from .constant import Constant from .constant import Constant
@@ -14,32 +18,29 @@ bp = Blueprint('users', __name__, url_prefix='/users')
@bp.route('', methods=['POST']) @bp.route('', methods=['POST'])
@role_required(request, ['change']) @role_required(request, ['change'])
@request_json_handle(request, ['name', 'password', 'email']) @request_json_handle(request, ['name', 'password', 'email'])
def users_post(data, _): @api_try
def users_post(data, user):
'''注册一个用户''' '''注册一个用户'''
with Connect() as c: with Connect() as c:
new_user = UserRegister(c) new_user = UserRegister(c)
try:
new_user.set_name(data['name']) new_user.set_name(data['name'])
new_user.set_password(data['password']) new_user.set_password(data['password'])
new_user.set_email(data['email']) new_user.set_email(data['email'])
new_user.register() new_user.register()
return success_return({'user_id': new_user.user_id, 'user_code': new_user.user_code}) return success_return({'user_id': new_user.user_id, 'user_code': new_user.user_code})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('', methods=['GET']) @bp.route('', methods=['GET'])
@role_required(request, ['select']) @role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS) @request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def users_get(data, user): def users_get(data, user):
'''查询全用户信息''' '''查询全用户信息'''
A = ['user_id', 'name', 'user_code'] A = ['user_id', 'name', 'user_code']
B = ['user_id', 'name', 'user_code', 'join_date', B = ['user_id', 'name', 'user_code', 'join_date',
'rating_ptt', 'time_played', 'ticket', 'world_rank_score'] 'rating_ptt', 'time_played', 'ticket', 'world_rank_score']
with Connect() as c: with Connect() as c:
try: query = Query(A, A, B).from_dict(data)
query = Query(A, A, B).from_data(data)
x = Sql(c).select('user', query=query) x = Sql(c).select('user', query=query)
r = [] r = []
for i in x: for i in x:
@@ -53,98 +54,171 @@ def users_get(data, user):
'name': x.name, 'name': x.name,
'join_date': x.join_date, 'join_date': x.join_date,
'user_code': x.user_code, 'user_code': x.user_code,
'rating_ptt': x.rating_ptt/100, 'rating_ptt': x.rating_ptt,
'character_id': x.character.character_id, 'character_id': x.character.character_id,
'is_char_uncapped': x.character.is_uncapped, 'is_char_uncapped': x.character.is_uncapped,
'is_char_uncapped_override': x.character.is_uncapped_override, 'is_char_uncapped_override': x.character.is_uncapped_override,
'is_hide_rating': x.is_hide_rating, 'is_hide_rating': x.is_hide_rating,
'ticket': x.ticket 'ticket': x.ticket
} for x in r]) } for x in r])
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/<int:user_id>', methods=['GET']) @bp.route('/<int:user_id>', methods=['GET'])
@role_required(request, ['select', 'select_me']) @role_required(request, ['select', 'select_me'])
@api_try
def users_user_get(user, user_id): def users_user_get(user, user_id):
'''查询用户信息''' '''查询用户信息'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
with Connect() as c: with Connect() as c:
try:
u = UserInfo(c, user_id) u = UserInfo(c, user_id)
return success_return(u.to_dict()) return success_return(u.to_dict())
except ArcError as e:
return error_return(e)
return error_return() @bp.route('/<int:user_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'], must_change=True)
@api_try
def users_user_put(data, user, user_id):
'''修改一个用户'''
with Connect() as c:
u = UserChanger(c, user_id)
r = {}
r['user_id'] = user_id
if 'name' in data:
u.set_name(data['name'])
r['name'] = u.name
if 'password' in data:
u.set_password(data['password'])
r['password'] = u.hash_pwd
if 'email' in data:
u.set_email(data['email'])
r['email'] = u.email
if 'user_code' in data:
u.set_user_code(data['user_code'])
r['user_code'] = u.user_code
if 'ticket' in data:
if not isinstance(data['ticket'], int):
raise InputError('Ticket must be int')
u.ticket = data['ticket']
r['ticket'] = u.ticket
u.update_columns(d=r)
return success_return(r)
@bp.route('/<int:user_id>/b30', methods=['GET']) @bp.route('/<int:user_id>/b30', methods=['GET'])
@role_required(request, ['select', 'select_me']) @role_required(request, ['select', 'select_me'])
@api_try
def users_user_b30_get(user, user_id): def users_user_b30_get(user, user_id):
'''查询用户b30''' '''查询用户b30'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
with Connect() as c: with Connect() as c:
try:
x = UserScoreList(c, UserInfo(c, user_id)) x = UserScoreList(c, UserInfo(c, user_id))
x.query.limit = 30 x.query.limit = 30
x.select_from_user() x.select_from_user()
if not x.scores:
raise NoData(
f'No best30 data of user `{user_id}`', api_error_code=-3)
x.select_song_name()
r = x.to_dict_list() r = x.to_dict_list()
rating_sum = sum([i.rating for i in x.scores]) rating_sum = sum(i.rating for i in x.scores)
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r}) return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/<int:user_id>/best', methods=['GET']) @bp.route('/<int:user_id>/best', methods=['GET'])
@role_required(request, ['select', 'select_me']) @role_required(request, ['select', 'select_me'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS) @request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def users_user_best_get(data, user, user_id): def users_user_best_get(data, user, user_id):
'''查询用户所有best成绩''' '''查询用户所有best成绩'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
with Connect() as c: with Connect() as c:
try:
x = UserScoreList(c, UserInfo(c, user_id)) x = UserScoreList(c, UserInfo(c, user_id))
x.query.from_data(data) x.query.from_dict(data)
x.select_from_user() x.select_from_user()
if not x.scores:
raise NoData(
f'No best score data of user `{user_id}`', api_error_code=-3)
r = x.to_dict_list() r = x.to_dict_list()
return success_return({'user_id': user_id, 'data': r}) return success_return({'user_id': user_id, 'data': r})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/<int:user_id>/r30', methods=['GET']) @bp.route('/<int:user_id>/r30', methods=['GET'])
@role_required(request, ['select', 'select_me']) @role_required(request, ['select', 'select_me'])
@api_try
def users_user_r30_get(user, user_id): def users_user_r30_get(user, user_id):
'''查询用户r30''' '''查询用户r30'''
if user_id <= 0: if user_id <= 0:
return error_return(InputError(api_error_code=-4)) return error_return(InputError(api_error_code=-110))
# 查别人需要select权限 # 查别人需要select权限
if user_id != user.user_id and user.user_id != 0 and not user.role.has_power('select'): if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403) return error_return(NoAccess('No permission', api_error_code=-1), 403)
with Connect() as c: with Connect() as c:
try:
p = Potential(c, UserInfo(c, user_id)) p = Potential(c, UserInfo(c, user_id))
return success_return({'user_id': user_id, 'r10_ptt': p.recent_10 / 10, 'data': p.recent_30_to_dict_list()}) return success_return({'user_id': user_id, 'r10_ptt': p.recent_10 / 10, 'data': p.recent_30_to_dict_list()})
except ArcError as e:
return error_return(e)
return error_return() @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]})

View File

@@ -1,6 +1,7 @@
class Config(): class Config():
''' '''
This is the setting file. You can change some parameters here. This is the example setting file.
The user's setting file's name is `config.py`.
''' '''
''' '''
@@ -9,7 +10,7 @@ class Config():
Host and port of your server Host and port of your server
''' '''
HOST = '0.0.0.0' HOST = '0.0.0.0'
PORT = '80' PORT = 80
''' '''
-------------------- --------------------
''' '''
@@ -30,29 +31,30 @@ class Config():
Allowed game versions Allowed game versions
If it is blank, all are allowed. If it is blank, all are allowed.
''' '''
ALLOW_APPVERSION = ['3.12.6', '3.12.6c', '4.0.255', '4.0.255c'] ALLOW_APPVERSION = ['3.12.6', '3.12.6c',
'4.0.256', '4.0.256c', '4.1.0', '4.1.0c']
''' '''
-------------------- --------------------
''' '''
''' '''
-------------------- --------------------
联机功能的端口号若为空则默认不开启联机功能 联机功能相关设置请确保与Link Play服务器端的设置一致
Port of your link play server Setting of your link play server
If it is blank, link play will be unavailable. Please ensure that the settings on the side of Link Play server are consistent.
''' '''
UDP_PORT = '10900' # 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
Link Play address LINKPLAY_AUTHENTICATION = 'my_link_play_server'
If left blank, it will be obtained automatically. # 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.
LINK_PLAY_HOST = '' # ***.com LINKPLAY_DISPLAY_HOST = ''
''' '''
-------------------- --------------------
''' '''
@@ -75,7 +77,7 @@ class Config():
愚人节模式开关 愚人节模式开关
Switch of April Fool's Day Switch of April Fool's Day
''' '''
IS_APRILFOOLS = False IS_APRILFOOLS = True
''' '''
-------------------- --------------------
''' '''
@@ -186,7 +188,8 @@ class Config():
是否记录详细的服务器日志 是否记录详细的服务器日志
If recording detailed server logs is enabled If recording detailed server logs is enabled
''' '''
ALLOW_LOG_INFO = False ALLOW_INFO_LOG = False
ALLOW_WARNING_LOG = False
''' '''
-------------------- --------------------
''' '''

View File

@@ -2,57 +2,62 @@ from hashlib import sha256
from os import urandom from os import urandom
from time import time from time import time
from .error import NoAccess, NoData, UserBan from .config_manager import Config
from .error import NoAccess, NoData, RateLimit, UserBan
from .limiter import ArcLimiter
from .user import UserOnline from .user import UserOnline
class Power: class Power:
def __init__(self, c=None): def __init__(self, c=None):
self.c = c self.c = c
self.power_id: int = None self.power_id: str = None
self.power_name: str = None
self.caption: str = None self.caption: str = None
@classmethod @classmethod
def from_dict(cls, d: dict, c=None) -> 'Power': def from_dict(cls, d: dict, c=None) -> 'Power':
p = cls(c) p = cls(c)
p.power_id = d['power_id'] p.power_id = d['power_id']
p.power_name = d['power_name']
p.caption = d['caption'] p.caption = d['caption']
return p return p
def select_from_name(self, power_name: str) -> 'Power':
pass
class Role: class Role:
def __init__(self, c=None): def __init__(self, c=None):
self.c = c self.c = c
self.role_id: int = None self.role_id: str = None
self.role_name: str = None
self.caption: str = None self.caption: str = None
self.powers: list = None self.powers: list = None
def has_power(self, power_name: str) -> bool: def has_power(self, power_id: str) -> bool:
'''判断role是否有power''' '''判断role是否有power'''
return any(power_id == i.power_id for i in self.powers)
def only_has_powers(self, power_ids: list, anti_power_ids: list = None) -> bool:
'''判断role是否全有power_ids里的power且没有anti_power_ids里的任何一个power'''
flags = [False] * len(power_ids)
if anti_power_ids is None:
anti_power_ids = []
for i in self.powers: for i in self.powers:
if i.power_name == power_name: if i.power_id in anti_power_ids:
return True
return False return False
for j, k in enumerate(power_ids):
if i.power_id == k:
flags[j] = True
return all(flags)
def select_from_id(self, role_id: int = None) -> 'Role': def select_from_id(self, role_id: int = None) -> 'Role':
'''用role_id查询role''' '''用role_id查询role'''
if role_id is not None: if role_id is not None:
self.role_id = role_id self.role_id = role_id
self.c.execute('''select role_name, caption from role where role_id = :a''', self.c.execute('''select caption from role where role_id = :a''',
{'a': self.role_id}) {'a': self.role_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The role `%s` does not exist.' % raise NoData(
self.role_id, api_error_code=-200) f'The role `{self.role_id}` does not exist.', api_error_code=-200)
self.role_name = x[0] self.caption = x[0]
self.caption = x[1]
return self return self
def select_powers(self) -> None: def select_powers(self) -> None:
@@ -63,10 +68,12 @@ class Role:
x = self.c.fetchall() x = self.c.fetchall()
for i in x: for i in x:
self.powers.append(Power.from_dict( self.powers.append(Power.from_dict(
{'power_id': i[0], 'power_name': i[1], 'caption': i[2]}, self.c)) {'power_id': i[0], 'caption': i[1]}, self.c))
class APIUser(UserOnline): class APIUser(UserOnline):
limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login')
def __init__(self, c=None, user_id=None) -> None: def __init__(self, c=None, user_id=None) -> None:
super().__init__(c, user_id) super().__init__(c, user_id)
self.api_token: str = None self.api_token: str = None
@@ -74,6 +81,13 @@ class APIUser(UserOnline):
self.ip: str = None self.ip: str = None
def set_role_system(self) -> None:
'''设置为最高权限用户API接口'''
self.user_id = 0
self.role = Role(self.c)
self.role.role_id = 'system'
self.role.select_powers()
def select_role(self) -> None: def select_role(self) -> None:
'''查询user的role''' '''查询user的role'''
self.c.execute('''select role_id from user_role where user_id = :a''', self.c.execute('''select role_id from user_role where user_id = :a''',
@@ -82,10 +96,9 @@ class APIUser(UserOnline):
self.role = Role(self.c) self.role = Role(self.c)
if x is None: if x is None:
# 默认role为user # 默认role为user
self.role.role_id = 1 self.role.role_id = 'user'
else: else:
self.role.role_id = int(x[0]) self.role.role_id = x[0]
self.role.select_from_id()
def select_role_and_powers(self) -> None: def select_role_and_powers(self) -> None:
'''查询user的role以及role的powers''' '''查询user的role以及role的powers'''
@@ -113,22 +126,27 @@ class APIUser(UserOnline):
self.password = password self.password = password
if ip is not None: if ip is not None:
self.ip = ip self.ip = ip
if not self.limiter.hit(name):
raise RateLimit(
f'Too many login attempts of username {name}', api_error_code=-205)
self.c.execute('''select user_id, password from user where name = :a''', { self.c.execute('''select user_id, password from user where name = :a''', {
'a': self.name}) 'a': self.name})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The user `%s` does not exist.' % raise NoData(
self.name, api_error_code=-201) f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
if x[1] == '': if x[1] == '':
raise UserBan('The user `%s` is banned.' % self.name) raise UserBan(f'The user `{x[0]}` is banned.')
if self.hash_pwd != x[1]: if self.hash_pwd != x[1]:
raise NoAccess('The password is incorrect.', api_error_code=-201) raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
api_error_code=-201, status=401)
self.user_id = x[0] self.user_id = x[0]
now = int(time() * 1000) now = int(time() * 1000)
self.token = sha256( self.api_token = sha256(
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest() (str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
self.logout() self.logout()
self.c.execute('''insert into api_login values(?,?,?,?)''', self.c.execute('''insert into api_login values(?,?,?,?)''',
(self.user_id, self.token, now, self.ip)) (self.user_id, self.api_token, now, self.ip))

View 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)

View File

@@ -1,7 +1,7 @@
from setting import Config from .config_manager import Config
from .error import ArcError, InputError, NoData, ItemNotEnough
from .constant import Constant from .constant import Constant
from .item import Item, ItemCore from .error import ArcError, InputError, ItemNotEnough, NoData
from .item import CollectionItemMixin, ItemCore
class Level: class Level:
@@ -9,9 +9,9 @@ class Level:
min_level = 1 min_level = 1
def __init__(self) -> None: def __init__(self) -> None:
self.max_level = None self.max_level: int = None
self.level = None self.level: int = None
self.exp = None self.exp: float = None
@property @property
def level_exp(self): def level_exp(self):
@@ -25,12 +25,13 @@ class Level:
if exp >= Constant.LEVEL_STEPS[self.max_level]: if exp >= Constant.LEVEL_STEPS[self.max_level]:
self.exp = Constant.LEVEL_STEPS[self.max_level] self.exp = Constant.LEVEL_STEPS[self.max_level]
self.level = self.max_level self.level = self.max_level
return None
a = [] a = []
b = [] b = []
for i in Constant.LEVEL_STEPS: for k, v in Constant.LEVEL_STEPS.items():
a.append(i) a.append(k)
b.append(Constant.LEVEL_STEPS[i]) b.append(v)
if exp < b[0]: # 向下溢出是异常状态不该被try捕获不然数据库无法回滚 if exp < b[0]: # 向下溢出是异常状态不该被try捕获不然数据库无法回滚
raise ValueError('EXP value error.') raise ValueError('EXP value error.')
@@ -45,23 +46,10 @@ class Level:
class Skill: class Skill:
def __init__(self) -> None: def __init__(self) -> None:
self.skill_id = None self.skill_id: str = None
self.skill_id_uncap = None self.skill_id_uncap: str = None
self.skill_unlock_level = None self.skill_unlock_level: int = None
self.skill_requires_uncap = None self.skill_requires_uncap: bool = None
class Core(Item):
item_type = 'core'
def __init__(self, core_type: str = '', amount: int = 0) -> None:
super().__init__()
self.item_id = core_type
self.amount = amount
self.is_available = True
def to_dict(self):
return {'core_type': self.item_id, 'amount': self.amount}
class CharacterValue: class CharacterValue:
@@ -69,16 +57,23 @@ class CharacterValue:
self.set_parameter(start, mid, end) self.set_parameter(start, mid, end)
@staticmethod @staticmethod
def _calc_char_value_20(level, stata, statb, lva=1, lvb=20): def _calc_char_value_20_math(level: int, value_1: float, value_20: float) -> float:
# 计算1~20级搭档数值的核心函数返回浮点数来自https://redive.estertion.win/arcaea/calc/ # by Lost-MSth
n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946, # 4/6859 = 0.00058317539
0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1] if level <= 10:
e = n[lva] - n[lvb] return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1
a = stata - statb return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
r = a / e
d = stata - n[lva] * r
return d + r * n[level] # @staticmethod
# def _calc_char_value_20(level: int, stata, statb, lva=1, lvb=20) -> float:
# # 计算1~20级搭档数值的核心函数返回浮点数来自https://redive.estertion.win/arcaea/calc/
# n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
# 0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
# e = n[lva] - n[lvb]
# a = stata - statb
# r = a / e
# d = stata - n[lva] * r
# return d + r * n[level]
@staticmethod @staticmethod
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30): def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
@@ -86,42 +81,51 @@ class CharacterValue:
return (level - lva) * (statb - stata) / (lvb - lva) + stata return (level - lva) * (statb - stata) / (lvb - lva) + stata
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0): def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
self.start = start self.start: float = start
self.mid = mid self.mid: float = mid
self.end = end self.end: float = end
def get_value(self, level: Level): def get_value(self, level: Level):
if level.min_level <= level.level <= level.mid_level: if level.min_level <= level.level <= level.mid_level:
return self._calc_char_value_20(level.level, self.start, self.mid) return self._calc_char_value_20_math(level.level, self.start, self.mid)
elif level.mid_level < level.level <= level.max_level: if level.mid_level < level.level <= level.max_level:
return self._calc_char_value_30(level.level, self.mid, self.end) return self._calc_char_value_30(level.level, self.mid, self.end)
else:
return 0 return 0
class Character: class Character(CollectionItemMixin):
database_table_name = None database_table_name = None
def __init__(self) -> None: collection_item_const = {
self.character_id = None 'name': 'character',
self.name = None 'table_name': 'char_item',
self.char_type = None 'table_primary_key': 'character_id',
self.is_uncapped = None 'id_name': 'character_id',
self.is_uncapped_override = None 'items_name': 'uncap_cores'
}
def __init__(self, c=None) -> None:
self.c = c
self.character_id: int = None
self.name: str = None
self.char_type: int = None
self.is_uncapped: bool = None
self.is_uncapped_override: bool = None
self.skill = Skill() self.skill = Skill()
self.level = Level() self.level = Level()
self.frag = CharacterValue() self.frag = CharacterValue()
self.prog = CharacterValue() self.prog = CharacterValue()
self.overdrive = CharacterValue() self.overdrive = CharacterValue()
self.uncap_cores = [] self.uncap_cores: list = []
self.voice = None self.voice: list = None
@property @property
def skill_id_displayed(self) -> str: def skill_id_displayed(self) -> str:
return None return None
def uncap_cores_to_dict(self): def uncap_cores_to_dict(self):
return [x.to_dict() for x in self.uncap_cores] return [x.to_dict(character_format=True) for x in self.uncap_cores]
@property @property
def is_uncapped_displayed(self) -> bool: def is_uncapped_displayed(self) -> bool:
@@ -133,10 +137,76 @@ class Character:
# 应该是只有对立这样 # 应该是只有对立这样
return self.character_id == 1 return self.character_id == 1
def to_dict(self, has_cores=False) -> dict:
# 用于API里游戏内的数据不太一样故不能super
r = {
'character_id': self.character_id,
'name': self.name,
'char_type': self.char_type,
'is_uncapped': self.is_uncapped,
'max_level': self.level.max_level,
'skill_id': self.skill.skill_id,
'skill_unlock_level': self.skill.skill_unlock_level,
'skill_requires_uncap': self.skill.skill_requires_uncap,
'skill_id_uncap': self.skill.skill_id_uncap,
'frag1': self.frag.start,
'frag20': self.frag.mid,
'frag30': self.frag.end,
'prog1': self.prog.start,
'prog20': self.prog.mid,
'prog30': self.prog.end,
'overdrive1': self.overdrive.start,
'overdrive20': self.overdrive.mid,
'overdrive30': self.overdrive.end,
}
if has_cores:
r['uncap_cores'] = self.uncap_cores_to_dict()
return r
def from_list(self, l: tuple) -> 'Character':
self.character_id = l[0]
self.name = l[1]
self.level.max_level = l[2]
self.frag.set_parameter(l[3], l[6], l[9])
self.prog.set_parameter(l[4], l[7], l[10])
self.overdrive.set_parameter(l[5], l[8], l[11])
self.skill.skill_id = l[12]
self.skill.skill_unlock_level = l[13]
self.skill.skill_requires_uncap = l[14] == 1
self.skill.skill_id_uncap = l[15]
self.char_type = l[16]
self.is_uncapped = l[17] == 1
return self
def select(self, character_id: int = None) -> 'Character':
if character_id is not None:
self.character_id = character_id
self.c.execute(
'select * from character where character_id = ?', (self.character_id,))
x = self.c.fetchone()
if not x:
raise NoData(
f'No such character: {self.character_id}', api_error_code=-130)
return self.from_list(x)
def update(self) -> None:
self.c.execute('''update character set name = ?, max_level = ?, frag1 = ?, frag20 = ?, frag30 = ?, prog1 = ?, prog20 = ?, prog30 = ?, overdrive1 = ?, overdrive20 = ?, overdrive30 = ?, skill_id = ?, skill_unlock_level = ?, skill_requires_uncap = ?, skill_id_uncap = ?, char_type = ?, is_uncapped = ? where character_id = ?''', (
self.name, self.level.max_level, self.frag.start, self.frag.mid, self.frag.end, self.prog.start, self.prog.mid, self.prog.end, self.overdrive.start, self.overdrive.mid, self.overdrive.end, self.skill.skill_id, self.skill.skill_unlock_level, self.skill.skill_requires_uncap, self.skill.skill_id_uncap, self.char_type, self.is_uncapped, self.character_id))
def select_character_core(self):
# 获取此角色所需核心
self.c.execute(
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
x = self.c.fetchall()
if x:
self.uncap_cores = []
for i in x:
self.uncap_cores.append(ItemCore(self.c, i[0], i[1]))
class UserCharacter(Character): class UserCharacter(Character):
''' '''
用户角色类\ 用户角色类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char' database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
@@ -152,33 +222,24 @@ class UserCharacter(Character):
'''对外显示的技能id''' '''对外显示的技能id'''
if self.is_uncapped_displayed and self.skill.skill_id_uncap: if self.is_uncapped_displayed and self.skill.skill_id_uncap:
return self.skill.skill_id_uncap return self.skill.skill_id_uncap
elif self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level: if self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
return self.skill.skill_id return self.skill.skill_id
else:
return None return None
def select_character_core(self):
# 获取此角色所需核心
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(Core(i[0], i[1]))
def select_character_uncap_condition(self, user=None): def select_character_uncap_condition(self, user=None):
# parameter: user - User类或子类的实例 # parameter: user - User类或子类的实例
# 获取此角色的觉醒信息 # 获取此角色的觉醒信息
if user: if user:
self.user = user self.user = user
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name, self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
{'a': self.user.user_id, 'b': self.character_id}) {'a': self.user.user_id, 'b': self.character_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('The character of the user does not exist.') 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 = x[0] == 1
self.is_uncapped_override = x[1] == 1 self.is_uncapped_override = x[1] == 1
@@ -187,7 +248,7 @@ class UserCharacter(Character):
# 获取所给用户此角色信息 # 获取所给用户此角色信息
if user: if user:
self.user = user self.user = user
self.c.execute('''select * from %s a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''' % self.database_table_name, self.c.execute(f'''select * from {self.database_table_name} a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
(self.user.user_id, self.character_id)) (self.user.user_id, self.character_id))
y = self.c.fetchone() y = self.c.fetchone()
@@ -209,7 +270,7 @@ class UserCharacter(Character):
self.skill.skill_unlock_level = y[19] self.skill.skill_unlock_level = y[19]
self.skill.skill_requires_uncap = y[20] == 1 self.skill.skill_requires_uncap = y[20] == 1
if self.character_id == 21 or self.character_id == 46: if self.character_id in (21, 46):
self.voice = [0, 1, 2, 3, 100, 1000, 1001] self.voice = [0, 1, 2, 3, 100, 1000, 1001]
self.select_character_core() self.select_character_core()
@@ -248,7 +309,7 @@ class UserCharacter(Character):
# 切换觉醒状态 # 切换觉醒状态
if user: if user:
self.user = user self.user = user
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name, self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
{'a': self.user.user_id, 'b': self.character_id}) {'a': self.user.user_id, 'b': self.character_id})
x = self.c.fetchone() x = self.c.fetchone()
@@ -258,7 +319,7 @@ class UserCharacter(Character):
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', { self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id}) 'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
self.c.execute('''update %s set is_uncapped_override = :a where user_id = :b and character_id = :c''' % self.database_table_name, { self.c.execute(f'''update {self.database_table_name} set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id}) 'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id})
self.is_uncapped_override = x[1] == 0 self.is_uncapped_override = x[1] == 0
@@ -288,11 +349,11 @@ class UserCharacter(Character):
self.c.execute( self.c.execute(
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id)) '''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
y = self.c.fetchone() y = self.c.fetchone()
if not y or i.amount > y[0]: if i.amount > 0 and (not y or i.amount > y[0]):
raise ItemNotEnough('The cores are not enough.') raise ItemNotEnough('The cores are not enough.')
for i in self.uncap_cores: for i in self.uncap_cores:
ItemCore(self.c, i, reverse=True).user_claim_item(self.user) i.user_claim_item(self.user, reverse=True)
self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''', self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
(self.user.user_id, self.character_id)) (self.user.user_id, self.character_id))
@@ -329,8 +390,9 @@ class UserCharacter(Character):
def upgrade_by_core(self, user=None, core=None): def upgrade_by_core(self, user=None, core=None):
''' '''
以太之滴升级注意这里core.amount应该是负数\ 以太之滴升级注意这里core.amount应该是负数
parameter: `user` - `User`类或子类的实例\
parameter: `user` - `User`类或子类的实例
`core` - `ItemCore`类或子类的实例 `core` - `ItemCore`类或子类的实例
''' '''
if user: if user:
@@ -350,7 +412,7 @@ class UserCharacter(Character):
class UserCharacterList: class UserCharacterList:
''' '''
用户拥有角色列表类\ 用户拥有角色列表类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char' database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
@@ -362,7 +424,7 @@ class UserCharacterList:
def select_user_characters(self): def select_user_characters(self):
self.c.execute( self.c.execute(
'''select character_id from %s where user_id=?''' % self.database_table_name, (self.user.user_id,)) f'''select character_id from {self.database_table_name} where user_id=?''', (self.user.user_id,))
x = self.c.fetchall() x = self.c.fetchall()
self.characters: list = [] self.characters: list = []
if x: if x:

View File

@@ -0,0 +1,98 @@
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 = '/pollen/22'
ALLOW_APPVERSION = [] # list[str]
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
LINKPLAY_HOST = '0.0.0.0'
LINKPLAY_UDP_PORT = 10900
LINKPLAY_TCP_PORT = 10901
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
LINKPLAY_DISPLAY_HOST = ''
SSL_CERT = ''
SSL_KEY = ''
IS_APRILFOOLS = True
WORLD_RANK_MAX = 200
AVAILABLE_MAP = [] # list[str]
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = '1145141919810'
API_TOKEN = ''
DOWNLOAD_LINK_PREFIX = ''
DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False
NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/'
DOWNLOAD_TIMES_LIMIT = 3000
DOWNLOAD_TIME_GAP_LIMIT = 1000
LOGIN_DEVICE_NUMBER_LIMIT = 1
ALLOW_LOGIN_SAME_DEVICE = False
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
ALLOW_SCORE_WITH_NO_SONG = True
ALLOW_INFO_LOG = False
ALLOW_WARNING_LOG = False
DEFAULT_MEMORIES = 0
UPDATE_WITH_NEW_CHARACTER_DATA = True
CHARACTER_FULL_UNLOCK = True
WORLD_SONG_FULL_UNLOCK = True
WORLD_SCENERY_FULL_UNLOCK = True
SAVE_FULL_UNLOCK = False
# ------------------------------------------
# You can change this to make another PTT mechanism.
BEST30_WEIGHT = 1 / 40
RECENT10_WEIGHT = 1 / 40
MAX_FRIEND_COUNT = 50
WORLD_MAP_FOLDER_PATH = './database/map/'
SONG_FILE_FOLDER_PATH = './database/songs/'
SONGLIST_FILE_PATH = './database/songs/songlist'
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/'
DATABASE_INIT_PATH = './database/init/'
SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db'
GAME_LOGIN_RATE_LIMIT = '30/5 minutes'
API_LOGIN_RATE_LIMIT = '10/5 minutes'
class ConfigManager:
@staticmethod
def load(config) -> None:
for k, v in config.__dict__.items():
if k.startswith('__') or k.endswith('__'):
continue
if hasattr(Config, k):
setattr(Config, k, v)

View File

@@ -1,4 +1,7 @@
from setting import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.2'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
class Constant: class Constant:
@@ -9,6 +12,8 @@ class Constant:
STAMINA_RECOVER_TICK = 1800000 STAMINA_RECOVER_TICK = 1800000
COURSE_STAMINA_COST = 4
CORE_EXP = 250 CORE_EXP = 250
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100, LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
@@ -18,25 +23,34 @@ class Constant:
LUNA_UNCAP_BONUS_PROGRESS = 7 LUNA_UNCAP_BONUS_PROGRESS = 7
AYU_UNCAP_BONUS_PROGRESS = 5 AYU_UNCAP_BONUS_PROGRESS = 5
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000 SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
MAX_FRIEND_COUNT = 50 MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
# You can change this to make another PTT mechanism. MY_RANK_MAX_LOCAL_POSITION = 5
BEST30_WEIGHT = 1 / 40 MY_RANK_MAX_GLOBAL_POSITION = 9999
RECENT10_WEIGHT = 1 / 40
WORLD_MAP_FOLDER_PATH = './database/map/' BEST30_WEIGHT = Config.BEST30_WEIGHT
SONG_FILE_FOLDER_PATH = './database/songs/' RECENT10_WEIGHT = Config.RECENT10_WEIGHT
SQLITE_DATABASE_PATH = './database/arcaea_database.db'
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
SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH
SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX
LINK_PLAY_UNLOCK_LENGTH = 512 # Units: bytes LINKPLAY_UNLOCK_LENGTH = 512 # Units: bytes
LINK_PLAY_TIMEOUT = 10 # Units: seconds LINKPLAY_TIMEOUT = 5 # Units: seconds
COURSE_STAMINA_COST = 4 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
# Well, I can't say a word when I see this. # Well, I can't say a word when I see this.
FINALE_SWITCH = [ FINALE_SWITCH = [
@@ -84,3 +98,10 @@ class Constant:
(0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE, (0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE,
0x6CB300), (0XA35687B, 0xE456CDEA) 0x6CB300), (0XA35687B, 0xE456CDEA)
] ]
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -1,8 +1,6 @@
from .error import NoData from .error import NoData
from .song import Chart
from .item import ItemFactory from .item import ItemFactory
from .song import Chart
class CourseChart(Chart): class CourseChart(Chart):
@@ -98,7 +96,7 @@ class Course:
'''select * from course where course_id = ?''', (self.course_id,)) '''select * from course where course_id = ?''', (self.course_id,))
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The course `%s` is not found.' % self.course_id) raise NoData(f'The course `{self.course_id}` is not found.')
return self.from_list(x) return self.from_list(x)
def select_course_chart(self) -> None: def select_course_chart(self) -> None:
@@ -153,7 +151,8 @@ class Course:
class UserCourse(Course): class UserCourse(Course):
''' '''
用户课题类\ 用户课题类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -202,7 +201,8 @@ class UserCourse(Course):
class UserCourseList: class UserCourseList:
''' '''
用户课题列表类\ 用户课题列表类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -213,7 +213,18 @@ class UserCourseList:
self.courses: list = [] self.courses: list = []
def to_dict_list(self) -> list: def to_dict_list(self) -> list:
return [x.to_dict() for x in self.courses] # 临时修复满足条件也无法解锁隐藏段位的问题
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: def select_all(self) -> None:
self.c.execute('''select * from course''') self.c.execute('''select * from course''')
@@ -228,8 +239,9 @@ class UserCourseList:
class CoursePlay(UserCourse): class CoursePlay(UserCourse):
''' '''
课题模式打歌类联动UserPlay\ 课题模式打歌类联动UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''

View File

@@ -1,9 +1,14 @@
import os import os
from functools import lru_cache from functools import lru_cache
from json import loads
from time import time from time import time
from flask import url_for
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .error import NoAccess from .error import NoAccess
from .limiter import ArcLimiter
from .user import User from .user import User
from .util import get_file_md5, md5 from .util import get_file_md5, md5
@@ -16,77 +21,131 @@ def get_song_file_md5(song_id: str, file_name: str) -> str:
return get_file_md5(path) return get_file_md5(path)
def initialize_songfile(): class SonglistParser:
'''初始化歌曲数据的md5信息''' '''songlist文件解析器'''
get_song_file_md5.cache_clear()
x = DownloadList() FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
x.url_flag = False 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']
x.add_songs() songs: dict = {} # {song_id: value, ...}
del x # value: bit 76543210
# 7: video_audio.ogg
# 6: video.mp4
# 5: 3.ogg
# 4: base.ogg
# 3: 3.aff
# 2: 2.aff
# 1: 1.aff
# 0: 0.aff
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
self.path = path
self.data: list = []
self.parse()
@staticmethod
def is_available_file(song_id: str, file_name: str) -> bool:
'''判断文件是否允许被下载'''
if song_id not in SonglistParser.songs:
# songlist没有则只限制文件名
return file_name in SonglistParser.FILE_NAMES
rule = SonglistParser.songs[song_id]
for i in range(8):
if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0:
return True
return False
def parse_one(self, song: dict) -> dict:
'''解析单个歌曲'''
if not 'id' in song:
return None
r = 0
if 'remote_dl' in song and song['remote_dl']:
r |= 16
for i in song.get('difficulties', []):
if i['ratingClass'] == 3 and i.get('audioOverride', False):
r |= 32
r |= 1 << i['ratingClass']
else:
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
r |= 8
if 'additional_files' in song:
if 'video.mp4' in song['additional_files']:
r |= 64
if 'video_audio.ogg' in song['additional_files']:
r |= 128
return {song['id']: r}
def parse(self) -> None:
'''解析songlist文件'''
if not os.path.isfile(self.path):
return
with open(self.path, 'r', encoding='utf-8') as f:
self.data = loads(f.read()).get('songs', [])
for x in self.data:
self.songs.update(self.parse_one(x))
class UserDownload: class UserDownload:
''' '''
用户下载类\ 用户下载类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
def __init__(self, c=None, user=None) -> None: limiter = ArcLimiter(
self.c = c str(Constant.DOWNLOAD_TIMES_LIMIT) + '/day', 'download')
def __init__(self, c_m=None, user=None) -> None:
self.c_m = c_m
self.user = user self.user = user
self.song_id: str = None self.song_id: str = None
self.file_name: str = None self.file_name: str = None
self.file_path: str = None
self.token: str = None self.token: str = None
self.token_time: int = None self.token_time: int = None
def clear_user_download(self) -> None:
self.c.execute(
'''delete from user_download where user_id = :a and time <= :b''', {'a': self.user.user_id, 'b': int(time()) - 24*3600})
@property @property
def is_limited(self) -> bool: def is_limited(self) -> bool:
'''是否达到用户最大下载量''' '''是否达到用户最大下载量'''
self.c.execute( if self.user is None:
'''select count(*) from user_download where user_id = :a''', {'a': self.user.user_id}) self.select_for_check()
y = self.c.fetchone()
return y is not None and y[0] > Constant.DOWNLOAD_TIMES_LIMIT return not self.limiter.test(str(self.user.user_id))
@property @property
def is_valid(self) -> bool: def is_valid(self) -> bool:
'''链接是否有效且未过期''' '''链接是否有效且未过期'''
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT and self.song_id+'/'+self.file_name == self.file_path if self.token_time is None:
self.select_for_check()
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT
def insert_user_download(self) -> None: def download_hit(self) -> bool:
'''记录下载信息''' '''下载次数+1返回成功与否bool值'''
self.c.execute('''insert into user_download values(:a,:b,:c)''', { return self.limiter.hit(str(self.user.user_id))
'a': self.user.user_id, 'b': self.token, 'c': int(time())})
def select_from_token(self, token: str = None) -> None: def select_for_check(self) -> None:
if token is not None: '''利用token、song_id、file_name查询其它信息'''
self.token = token self.c_m.execute('''select user_id, time from download_token where song_id=? and file_name=? and token = ? limit 1;''',
self.c.execute('''select * from download_token where token = :t limit 1''', (self.song_id, self.file_name, self.token))
{'t': self.token})
x = self.c.fetchone() x = self.c_m.fetchone()
if not x: if not x:
raise NoAccess('The token `%s` is not valid.' % self.token) raise NoAccess('The token `%s` is not valid.' %
self.token, status=403)
self.user = User() self.user = User()
self.user.user_id = x[0] self.user.user_id = x[0]
self.song_id = x[1] self.token_time = x[1]
self.file_name = x[2]
self.token_time = x[4]
def generate_token(self) -> None: def generate_token(self) -> None:
self.token_time = int(time()) self.token_time = int(time())
self.token = md5(str(self.user.user_id) + self.song_id + self.token = md5(str(self.user.user_id) + self.song_id +
self.file_name + str(self.token_time)) self.file_name + str(self.token_time) + str(os.urandom(8)))
def insert_download_token(self) -> None: def insert_download_token(self) -> None:
'''将数据插入数据库,让这个下载链接可用''' '''将数据插入数据库,让这个下载链接可用'''
self.c.execute('''insert into download_token values(:a,:b,:c,:d,:e)''', { self.c_m.execute('''insert or replace into download_token values(:a,:b,:c,:d,:e)''', {
'a': self.user.user_id, 'b': self.song_id, 'c': self.file_name, 'd': self.token, 'e': self.token_time}) 'a': self.user.user_id, 'b': self.song_id, 'c': self.file_name, 'd': self.token, 'e': self.token_time})
@property @property
@@ -94,15 +153,13 @@ class UserDownload:
'''生成下载链接''' '''生成下载链接'''
if self.token is None: if self.token is None:
self.generate_token() self.generate_token()
self.insert_download_token() # self.insert_download_token() # 循环插入速度慢改为executemany
if Constant.DOWNLOAD_LINK_PREFIX: if Constant.DOWNLOAD_LINK_PREFIX:
prefix = Constant.DOWNLOAD_LINK_PREFIX prefix = Constant.DOWNLOAD_LINK_PREFIX
if prefix[-1] != '/': if prefix[-1] != '/':
prefix += '/' prefix += '/'
return prefix + self.song_id + '/' + self.file_name + '?t=' + self.token return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
else: return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
from flask import url_for
return url_for('download', file_path=self.song_id + '/' + self.file_name, t=self.token, _external=True)
@property @property
def hash(self) -> str: def hash(self) -> str:
@@ -111,12 +168,12 @@ class UserDownload:
class DownloadList(UserDownload): class DownloadList(UserDownload):
''' '''
下载列表类\ 下载列表类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
def __init__(self, c=None, user=None) -> None: def __init__(self, c_m=None, user=None) -> None:
super().__init__(c, user) super().__init__(c_m, user)
self.song_ids: list = None self.song_ids: list = None
self.url_flag: bool = None self.url_flag: bool = None
@@ -124,21 +181,51 @@ class DownloadList(UserDownload):
self.downloads: list = [] self.downloads: list = []
self.urls: dict = {} self.urls: dict = {}
def clear_download_token_from_song(self, song_id: str) -> None: @classmethod
self.c.execute('''delete from download_token where user_id=:a and song_id=:b''', { def initialize_cache(cls) -> None:
'a': self.user.user_id, 'b': song_id}) '''初始化歌曲数据缓存包括md5、文件目录遍历、解析songlist'''
SonglistParser()
if Config.SONG_FILE_HASH_PRE_CALCULATE:
x = cls()
x.url_flag = False
x.add_songs()
del x
@staticmethod
def clear_all_cache() -> None:
'''清除所有歌曲文件有关缓存'''
get_song_file_md5.cache_clear()
DownloadList.get_one_song_file_names.cache_clear()
DownloadList.get_all_song_ids.cache_clear()
SonglistParser.songs = {}
def clear_download_token(self) -> None:
'''清除过期下载链接'''
self.c_m.execute('''delete from download_token where time<?''',
(int(time()) - Constant.DOWNLOAD_TIME_GAP_LIMIT,))
def insert_download_tokens(self) -> None:
'''插入所有下载链接'''
self.c_m.executemany('''insert or replace into download_token values(?,?,?,?,?)''', [(
self.user.user_id, x.song_id, x.file_name, x.token, x.token_time) for x in self.downloads])
@staticmethod
@lru_cache(maxsize=2048)
def get_one_song_file_names(song_id: str) -> list:
'''获取一个歌曲文件夹下的所有合法文件名有lru缓存'''
r = []
for i in os.scandir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)):
file_name = i.name
if i.is_file() and SonglistParser.is_available_file(song_id, file_name):
r.append(file_name)
return r
def add_one_song(self, song_id: str) -> None: def add_one_song(self, song_id: str) -> None:
if self.url_flag:
self.clear_download_token_from_song(song_id)
dir_list = os.listdir(os.path.join(
Constant.SONG_FILE_FOLDER_PATH, song_id))
re = {} re = {}
for i in dir_list: for i in self.get_one_song_file_names(song_id):
if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']: x = UserDownload(self.c_m, self.user)
x = UserDownload(self.c, self.user) self.downloads.append(x)
# self.downloads.append(x) # 这实际上没有用
x.song_id = song_id x.song_id = song_id
x.file_name = i x.file_name = i
if i == 'base.ogg': if i == 'base.ogg':
@@ -156,7 +243,7 @@ class DownloadList(UserDownload):
re['audio']['3'] = {"checksum": x.hash, "url": x.url} re['audio']['3'] = {"checksum": x.hash, "url": x.url}
else: else:
re['audio']['3'] = {"checksum": x.hash} re['audio']['3'] = {"checksum": x.hash}
elif i == 'video.mp4' or i == 'video_audio.ogg': elif i in ('video.mp4', 'video_audio.ogg'):
if 'additional_files' not in re: if 'additional_files' not in re:
re['additional_files'] = [] re['additional_files'] = []
@@ -177,13 +264,26 @@ class DownloadList(UserDownload):
self.urls.update({song_id: re}) self.urls.update({song_id: re})
@staticmethod
@lru_cache()
def get_all_song_ids() -> list:
'''获取全歌曲文件夹列表有lru缓存'''
return [i.name for i in os.scandir(Constant.SONG_FILE_FOLDER_PATH) if i.is_dir()]
def add_songs(self, song_ids: list = None) -> None: def add_songs(self, song_ids: list = None) -> None:
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲''' '''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
if song_ids is not None: if song_ids is not None:
self.song_ids = song_ids self.song_ids = song_ids
x = self.song_ids if self.song_ids else os.listdir( if not self.song_ids:
Constant.SONG_FILE_FOLDER_PATH) self.song_ids = self.get_all_song_ids()
for i in x: for i in self.song_ids:
self.add_one_song(i)
else:
for i in self.song_ids:
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)): if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
self.add_one_song(i) self.add_one_song(i)
if self.url_flag:
self.clear_download_token()
self.insert_download_tokens()

View File

@@ -1,90 +1,113 @@
class ArcError(Exception): class ArcError(Exception):
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None) -> None: def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
self.message = message self.message: str = message
self.error_code = error_code self.error_code: int = error_code
self.api_error_code = api_error_code self.api_error_code: int = api_error_code
self.extra_data = extra_data self.extra_data = extra_data
self.status: int = status
def __str__(self) -> str: def __str__(self) -> str:
return repr(self.message) return repr(self.message)
class InputError(ArcError): class InputError(ArcError):
# 输入类型错误 '''输入类型错误'''
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class DataExist(ArcError):
# 数据存在 '''数据存在'''
pass
def __init__(self, message=None, error_code=108, api_error_code=-4, extra_data=None, status=200) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class NoData(ArcError): class NoData(ArcError):
# 数据不存在 '''数据不存在'''
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class PostError(ArcError):
# 缺少输入 '''缺少输入'''
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class UserBan(ArcError):
# 用户封禁 '''用户封禁'''
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class ItemNotEnough(ArcError):
# 物品数量不足 '''物品数量不足'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class ItemUnavailable(ArcError):
# 物品不可用 '''物品不可用'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class RedeemUnavailable(ArcError):
# 兑换码不可用 '''兑换码不可用'''
def __init__(self, message=None, error_code=505, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class MapLocked(ArcError):
# 地图锁定 '''地图锁定'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class StaminaNotEnough(ArcError):
# 体力不足 '''体力不足'''
def __init__(self, message=None, error_code=107, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class TicketNotEnough(ArcError):
# 记忆源点不足 '''记忆源点不足'''
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class FriendError(ArcError):
# 好友系统出错 '''好友系统出错'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None) -> None:
super().__init__(message, error_code, api_error_code, extra_data) 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): class NoAccess(ArcError):
# 无权限 '''无权限'''
pass
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=403) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)
class Timeout(ArcError): class Timeout(ArcError):
# 超时 '''超时'''
pass pass
class RateLimit(ArcError):
'''频率达到限制'''
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=429) -> None:
super().__init__(message, error_code, api_error_code, extra_data, status)

326
latest version/core/init.py Normal file
View File

@@ -0,0 +1,326 @@
import os
import sys
from importlib import import_module
from json import load
from shutil import copy, copy2
from time import time
from traceback import format_exc
from core.config_manager import Config
from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
from core.course import Course
from core.download import DownloadList
from core.purchase import Purchase
from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
MemoryDatabase)
from core.user import UserRegister
from core.util import try_rename
class DatabaseInit:
def __init__(self, db_path: str = Config.SQLITE_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
self.db_path = db_path
self.init_folder_path = init_folder_path
self.c = None
self.init_data = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'tables.sql')
@property
def course_path(self) -> str:
return os.path.join(self.init_folder_path, 'courses.json')
@property
def pack_path(self) -> str:
return os.path.join(self.init_folder_path, 'packs.json')
@property
def single_path(self) -> str:
return os.path.join(self.init_folder_path, 'singles.json')
def table_init(self) -> None:
'''初始化数据库结构'''
with open(self.sql_path, 'r', encoding='utf-8') as f:
self.c.executescript(f.read())
self.c.execute('''insert into config values("version", :a);''', {
'a': ARCAEA_SERVER_VERSION})
def character_init(self) -> None:
'''初始化搭档信息'''
uncapped_characters = self.init_data.char_core.keys()
for i in range(0, len(self.init_data.char)):
skill_requires_uncap = 1 if i == 2 else 0
if i in uncapped_characters:
max_level = 30
uncapable = 1
else:
max_level = 20
uncapable = 0
sql = '''insert into character values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'''
self.c.execute(sql, (i, self.init_data.char[i], max_level, self.init_data.frag1[i], self.init_data.prog1[i], self.init_data.overdrive1[i], self.init_data.frag20[i], self.init_data.prog20[i], self.init_data.overdrive20[i],
self.init_data.frag30[i], self.init_data.prog30[i], self.init_data.overdrive30[i], self.init_data.skill_id[i], self.init_data.skill_unlock_level[i], skill_requires_uncap, self.init_data.skill_id_uncap[i], self.init_data.char_type[i], uncapable))
self.c.execute('''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)''', (
99, 'shirahime', 38, 33, 28, 66, 58, 50, 66, 58, 50, 'frags_preferred_song', 0, 0, '', 0))
for i in self.init_data.char_core:
self.c.executemany('''insert into char_item values(?,?,'core',?)''', [
(i, j['core_id'], j['amount']) for j in self.init_data.char_core[i]])
def insert_purchase_item(self, purchases: list):
'''处理singles和packs'''
for i in purchases:
x = Purchase(self.c).from_dict(i)
x.insert_all()
def item_init(self) -> None:
'''初始化物品信息'''
self.c.executemany('''insert into item values(?,"core",1)''',
[(i,) for i in self.init_data.cores])
self.c.executemany('''insert into item values(?,"world_song",1)''', [
(i,) for i in self.init_data.world_songs])
self.c.executemany('''insert into item values(?,"world_unlock",1)''', [
(i,) for i in self.init_data.world_unlocks])
self.c.executemany('''insert into item values(?,"course_banner",1)''', [
(i,) for i in self.init_data.course_banners])
self.c.execute('''insert into item values(?,?,?)''',
('fragment', 'fragment', 1))
self.c.execute('''insert into item values(?,?,?)''',
('memory', 'memory', 1))
self.c.execute('''insert into item values(?,?,?)''',
('anni5tix', 'anni5tix', 1))
with open(self.pack_path, 'rb') as f:
self.insert_purchase_item(load(f))
with open(self.single_path, 'rb') as f:
self.insert_purchase_item(load(f))
def course_init(self) -> None:
'''初始化课题信息'''
courses = []
with open(self.course_path, 'rb') as f:
courses = load(f)
for i in courses:
x = Course(self.c).from_dict(i)
x.insert_all()
def role_power_init(self) -> None:
'''初始化power和role'''
self.c.executemany('''insert into role values(?,?)''', [(
self.init_data.role[i], self.init_data.role_caption[i]) for i in range(len(self.init_data.role))])
self.c.executemany('''insert into power values(?,?)''', [(
self.init_data.power[i], self.init_data.power_caption[i]) for i in range(len(self.init_data.power))])
for i in self.init_data.role_power:
self.c.executemany('''insert into role_power values(?,?)''', [
(i, j) for j in self.init_data.role_power[i]])
def admin_init(self) -> None:
'''初始化测试账号'''
x = UserRegister(self.c)
x.user_code = '123456789'
x.user_id = 2000000
x.name = 'admin'
x.email = 'admin@admin.com'
now = int(time() * 1000)
x._insert_user_char()
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email})
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
'user_id': x.user_id})
self.c.execute(
'''insert into user_role values(?, "admin")''', (x.user_id,))
def init(self) -> None:
sys.path.append(os.path.join(sys.path[0], self.init_folder_path))
self.init_data = import_module('arc_data').InitData
with Connect(self.db_path) as c:
self.c = c
self.table_init()
self.character_init()
self.item_init()
self.course_init()
self.role_power_init()
self.admin_init()
class LogDatabaseInit:
def __init__(self, db_path: str = Config.SQLITE_LOG_DATABASE_PATH, init_folder_path: str = Config.DATABASE_INIT_PATH) -> None:
self.db_path = db_path
self.init_folder_path = init_folder_path
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_init(self) -> None:
'''初始化数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION})
def init(self) -> None:
with Connect(self.db_path) as c:
self.c = c
self.table_init()
class FileChecker:
'''文件检查及初始化类'''
def __init__(self, logger=None):
self.logger = logger
def check_file(self, file_path: str) -> bool:
f = os.path.isfile(file_path)
if not f:
self.logger.warning('File `%s` is missing.' % file_path)
return f
def check_folder(self, folder_path: str) -> bool:
f = os.path.isdir(folder_path)
if not f:
self.logger.warning('Folder `%s` is missing.' % folder_path)
return f
def check_update_database(self) -> bool:
if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH):
# 新建日志数据库
try:
self.logger.info(
f'Try to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
LogDatabaseInit().init()
self.logger.info(
f'Success to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False
else:
# 检查更新
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
try:
x = c.execute(
'''select value from cache where key="version"''').fetchone()
except:
x = None
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
self.logger.warning(
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
try:
self.logger.info(
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
self.update_log_database()
self.logger.info(
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库
try:
self.logger.info(
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
DatabaseInit().init()
self.logger.info(
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
except Exception as e:
self.logger.error(format_exc())
self.logger.warning(
'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
return False
else:
# 检查更新
with Connect() as c:
try:
c.execute('''select value from config where id="version"''')
x = c.fetchone()
except:
x = None
# 数据库自动更新,不强求
if not x or x[0] != ARCAEA_SERVER_VERSION:
self.logger.warning(
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
try:
self.logger.info(
'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH):
os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH)
backup_path = try_rename(Config.SQLITE_DATABASE_PATH, os.path.join(
Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(Config.SQLITE_DATABASE_PATH)[-1] + '.bak'))
try:
copy2(backup_path, Config.SQLITE_DATABASE_PATH)
except:
copy(backup_path, Config.SQLITE_DATABASE_PATH)
temp_path = os.path.join(
*os.path.split(Config.SQLITE_DATABASE_PATH)[:-1], 'old_arcaea_database.db')
if os.path.isfile(temp_path):
os.remove(temp_path)
try_rename(Config.SQLITE_DATABASE_PATH, temp_path)
DatabaseInit().init()
self.update_database(temp_path)
self.logger.info(
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
except Exception as e:
self.logger.error(format_exc())
self.logger.warning(
'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
return True
@staticmethod
def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None:
'''更新数据库,并删除旧文件'''
if os.path.isfile(old_path) and os.path.isfile(new_path):
DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path)
@staticmethod
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
'''直接更新日志数据库'''
if os.path.isfile(old_path):
LogDatabaseMigrator(old_path).update_database()
def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)
self.logger.info("Start to initialize song data...")
try:
DownloadList.initialize_cache()
if not Config.SONG_FILE_HASH_PRE_CALCULATE:
self.logger.info('Song file hash pre-calculate is disabled.')
self.logger.info('Complete!')
except Exception as e:
self.logger.error(format_exc())
self.logger.warning('Initialization error!')
f = False
return f
def check_before_run(self) -> bool:
'''运行前检查,返回布尔值'''
MemoryDatabase() # 初始化内存数据库
return self.check_song_file() & self.check_update_database()

View File

@@ -1,14 +1,18 @@
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData from .config_manager import Config
from setting import Config from .error import DataExist, InputError, ItemNotEnough, ItemUnavailable, NoData
class Item: class Item:
item_type = None item_type = None
def __init__(self) -> None: def __init__(self, c=None) -> None:
self.item_id = None self.item_id = None
self.__amount = None self.__amount = None
self.is_available = None self.is_available = None
self.c = c
def __eq__(self, other: 'Item') -> bool:
return self.item_id == other.item_id and self.item_type == other.item_type
@property @property
def amount(self): def amount(self):
@@ -18,12 +22,13 @@ class Item:
def amount(self, value: int): def amount(self, value: int):
self.__amount = int(value) self.__amount = int(value)
def to_dict(self, has_is_available: bool = False) -> dict: def to_dict(self, has_is_available: bool = False, has_amount: bool = True) -> dict:
r = { r = {
'id': self.item_id, 'id': self.item_id,
'amount': self.amount,
'type': self.item_type 'type': self.item_type
} }
if has_amount:
r['amount'] = self.amount
if has_is_available: if has_is_available:
r['is_available'] = self.is_available r['is_available'] = self.is_available
return r return r
@@ -32,6 +37,32 @@ class Item:
# parameter: user - User类或子类的实例 # parameter: user - User类或子类的实例
pass pass
def select_exists(self):
self.c.execute('''select exists(select * from item where item_id=? and type=?)''',
(self.item_id, self.item_type))
return bool(self.c.fetchone()[0])
def insert(self, ignore: bool = False):
sql = '''insert into item values(?,?,?)''' if not ignore else '''insert or ignore into item values(?,?,?)'''
self.c.execute(sql, (self.item_id, self.item_type, self.is_available))
def delete(self):
self.c.execute('''delete from item where item_id=? and type=?''',
(self.item_id, self.item_type))
def update(self):
self.c.execute('''update item set is_available=? where item_id=? and type=?''',
(self.is_available, self.item_id, self.item_type))
def select(self):
self.c.execute('''select is_available from item where item_id=? and type=?''',
(self.item_id, self.item_type))
x = self.c.fetchone()
if not x:
raise NoData(
f'No such item `{self.item_type}`: `{self.item_id}`', api_error_code=-121)
self.is_available = x[0]
class UserItem(Item): class UserItem(Item):
@@ -40,9 +71,10 @@ class UserItem(Item):
self.c = c self.c = c
self.user = None self.user = None
def select(self, user=None): def select_user_item(self, user=None):
''' '''
查询用户item\ 查询用户item
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
if user is not None: if user is not None:
@@ -51,7 +83,7 @@ class UserItem(Item):
(self.user.user_id, self.item_id, self.item_type)) (self.user.user_id, self.item_id, self.item_type))
x = self.c.fetchone() x = self.c.fetchone()
if x: if x:
self.amount = x[0] if x[0] else 1 self.amount = x[0] if x[0] is not None else 1
else: else:
self.amount = 0 self.amount = 0
@@ -71,7 +103,6 @@ class NormalItem(UserItem):
if x[0] == 0: if x[0] == 0:
self.is_available = False self.is_available = False
raise ItemUnavailable('The item is unavailable.') raise ItemUnavailable('The item is unavailable.')
else:
self.is_available = True self.is_available = True
else: else:
raise NoData('No item data.') raise NoData('No item data.')
@@ -111,16 +142,29 @@ class PositiveItem(UserItem):
class ItemCore(PositiveItem): class ItemCore(PositiveItem):
item_type = 'core' item_type = 'core'
def __init__(self, c, core=None, reverse=False) -> None: def __init__(self, c, core_type: str = '', amount: int = 0) -> None:
super().__init__(c) super().__init__(c)
self.is_available = True self.is_available = True
if core: self.item_id = core_type
self.item_id = core.item_id self.amount = amount
self.amount = - core.amount if reverse else core.amount
def __str__(self) -> str: def __str__(self) -> str:
return self.item_id + '_' + str(self.amount) return self.item_id + '_' + str(self.amount)
def to_dict(self, has_is_available: bool = False, has_amount: bool = True, character_format: bool = False) -> dict:
if character_format:
# 搭档的core是特殊格式的
return {'core_type': self.item_id, 'amount': self.amount}
return super().to_dict(has_is_available=has_is_available, has_amount=has_amount)
def user_claim_item(self, user, reverse: bool = False) -> None:
# 骚操作将amount变为负数后使用再变回来
if reverse:
self.amount = -self.amount
super().user_claim_item(user)
if reverse:
self.amount = -self.amount
class ItemCharacter(UserItem): class ItemCharacter(UserItem):
item_type = 'character' item_type = 'character'
@@ -226,16 +270,10 @@ class CourseBanner(NormalItem):
class Single(NormalItem): class Single(NormalItem):
item_type = 'single' item_type = 'single'
def __init__(self, c) -> None:
super().__init__(c)
class Pack(NormalItem): class Pack(NormalItem):
item_type = 'pack' item_type = 'pack'
def __init__(self, c) -> None:
super().__init__(c)
class ProgBoost(UserItem): class ProgBoost(UserItem):
item_type = 'prog_boost_300' item_type = 'prog_boost_300'
@@ -245,10 +283,11 @@ class ProgBoost(UserItem):
def user_claim_item(self, user): def user_claim_item(self, user):
''' '''
世界模式prog_boost\ 世界模式prog_boost
parameters: `user` - `UserOnline`类或子类的实例 parameters: `user` - `UserOnline`类或子类的实例
''' '''
user.update_user_one_column('prog_boost', 1) user.update_user_one_column('prog_boost', 300)
class Stamina6(UserItem): class Stamina6(UserItem):
@@ -259,7 +298,7 @@ class Stamina6(UserItem):
def user_claim_item(self, user): def user_claim_item(self, user):
''' '''
世界模式记忆源点或残片买体力+6\ 世界模式记忆源点或残片买体力+6
顺手清一下世界模式过载状态 顺手清一下世界模式过载状态
''' '''
user.select_user_about_stamina() user.select_user_about_stamina()
@@ -302,7 +341,8 @@ class ItemFactory:
elif item_type == 'course_banner': elif item_type == 'course_banner':
return CourseBanner(self.c) return CourseBanner(self.c)
else: else:
raise InputError('The item type `%s` is wrong.' % item_type) raise InputError(
f'The item type `{item_type}` is invalid.', api_error_code=-120)
@classmethod @classmethod
def from_dict(cls, d: dict, c=None): def from_dict(cls, d: dict, c=None):
@@ -354,8 +394,8 @@ class ItemFactory:
class UserItemList: class UserItemList:
''' '''
用户的item列表\ 用户的item列表
注意只能查在user_item里面的character不行\ 注意只能查在user_item里面的character不行
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -382,9 +422,69 @@ class UserItemList:
self.items: list = [] self.items: list = []
for i in x: for i in x:
if len(i) > 1: if len(i) > 1:
amount = i[1] if i[1] else 0 amount = i[1] if i[1] is not None else 1
else: else:
amount = 1 amount = 1
self.items.append(ItemFactory.from_dict( self.items.append(ItemFactory.from_dict(
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c)) {'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
return self return self
class CollectionItemMixin:
'''
批量修改一些集合中的items
'''
collection_item_const = {
'name': 'collection',
'table_name': 'collection_item',
'table_primary_key': 'collection_id',
'id_name': 'collection_id',
'items_name': 'items'
}
def add_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in collection_items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-123)
self.c.executemany(f'''insert into {self.collection_item_const["table_name"]} values (?, ?, ?, ?)''', [
(collection_id, i.item_id, i.item_type, i.amount) for i in items])
collection_items.extend(items)
def remove_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if i not in collection_items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
self.c.executemany(f'''delete from {self.collection_item_const["table_name"]} where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
(collection_id, i.item_id, i.item_type) for i in items])
for i in items:
collection_items.remove(i)
def update_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if i not in collection_items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
self.c.executemany(f'''update {self.collection_item_const["table_name"]} set amount=? where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
(i.amount, collection_id, i.item_id, i.item_type) for i in items])
for i in items:
collection_items[collection_items.index(i)].amount = i.amount

View 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)

View File

@@ -1,16 +1,19 @@
from base64 import b64encode import socket
from base64 import b64decode, b64encode
from core.error import ArcError, Timeout from core.error import ArcError, Timeout
from .constant import Constant from .constant import Constant
from .user import UserInfo from .user import UserInfo
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
def get_song_unlock(client_song_map: dict) -> bytes: def get_song_unlock(client_song_map: dict) -> bytes:
'''处理可用歌曲bit返回bytes''' '''处理可用歌曲bit返回bytes'''
user_song_unlock = [0] * Constant.LINK_PLAY_UNLOCK_LENGTH user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH
for i in range(0, Constant.LINK_PLAY_UNLOCK_LENGTH*2, 2): for i in range(0, Constant.LINKPLAY_UNLOCK_LENGTH*2, 2):
x = 0 x = 0
y = 0 y = 0
if str(i) in client_song_map: if str(i) in client_song_map:
@@ -82,9 +85,8 @@ class Room:
} }
class LocalMultiPlayer: class RemoteMultiPlayer:
def __init__(self, conn=None) -> None: def __init__(self) -> None:
self.conn = conn
self.user: 'Player' = None self.user: 'Player' = None
self.room: 'Room' = None self.room: 'Room' = None
@@ -93,29 +95,44 @@ class LocalMultiPlayer:
def to_dict(self) -> dict: def to_dict(self) -> dict:
return dict(self.room.to_dict(), **self.user.to_dict()) return dict(self.room.to_dict(), **self.user.to_dict())
def data_swap(self, data: tuple) -> tuple: @staticmethod
self.conn.send(data) def tcp(data: str) -> str:
if self.conn.poll(Constant.LINK_PLAY_TIMEOUT): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
self.data_recv = self.conn.recv() sock.connect((Constant.LINKPLAY_HOST,
if self.data_recv[0] != 0: Constant.LINKPLAY_TCP_PORT))
raise ArcError('Link Play error.', self.data_recv[0]) sock.sendall(bytes(data + "\n", "utf-8"))
else: try:
received = str(sock.recv(1024), "utf-8").strip()
except socket.timeout as e:
raise Timeout( raise Timeout(
'Timeout when waiting for data from local udp server.') 'Timeout when waiting for data from link play server.', status=400) from e
# print(received)
return received
def data_swap(self, data: tuple) -> tuple:
received = self.tcp(Constant.LINKPLAY_AUTHENTICATION +
'|' + '|'.join([str(x) for x in data]))
self.data_recv = received.split('|')
if self.data_recv[0] != '0':
code = int(self.data_recv[0])
raise ArcError(f'Link Play error code: {code}', code, status=400)
def create_room(self, user: 'Player' = None) -> None: def create_room(self, user: 'Player' = None) -> None:
'''创建房间''' '''创建房间'''
if user is not None: if user is not None:
self.user = user self.user = user
user.select_user_one_column('name') user.select_user_one_column('name')
self.data_swap((1, self.user.name, self.user.song_unlock)) self.data_swap((1, self.user.name, b64encode(
self.user.song_unlock).decode('utf-8')))
self.room = Room() self.room = Room()
self.room.room_code = self.data_recv[1] self.room.room_code = self.data_recv[1]
self.room.room_id = self.data_recv[2] self.room.room_id = int(self.data_recv[2])
self.room.song_unlock = self.user.song_unlock self.room.song_unlock = self.user.song_unlock
self.user.token = self.data_recv[3] self.user.token = int(self.data_recv[3])
self.user.key = self.data_recv[4] self.user.key = b64decode(self.data_recv[4])
self.user.player_id = self.data_recv[5] self.user.player_id = int(self.data_recv[5])
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None: def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
'''加入房间''' '''加入房间'''
@@ -126,13 +143,13 @@ class LocalMultiPlayer:
self.user.select_user_one_column('name') self.user.select_user_one_column('name')
self.data_swap( self.data_swap(
(2, self.user.name, self.user.song_unlock, room.room_code)) (2, self.user.name, b64encode(self.user.song_unlock).decode('utf-8'), room.room_code))
self.room.room_code = self.data_recv[1] self.room.room_code = self.data_recv[1]
self.room.room_id = self.data_recv[2] self.room.room_id = int(self.data_recv[2])
self.room.song_unlock = self.data_recv[6] self.room.song_unlock = b64decode(self.data_recv[6])
self.user.token = self.data_recv[3] self.user.token = int(self.data_recv[3])
self.user.key = self.data_recv[4] self.user.key = b64decode(self.data_recv[4])
self.user.player_id = self.data_recv[5] self.user.player_id = int(self.data_recv[5])
def update_room(self, user: 'Player' = None) -> None: def update_room(self, user: 'Player' = None) -> None:
'''更新房间''' '''更新房间'''
@@ -141,7 +158,7 @@ class LocalMultiPlayer:
self.data_swap((3, self.user.token)) self.data_swap((3, self.user.token))
self.room = Room() self.room = Room()
self.room.room_code = self.data_recv[1] self.room.room_code = self.data_recv[1]
self.room.room_id = self.data_recv[2] self.room.room_id = int(self.data_recv[2])
self.room.song_unlock = self.data_recv[5] self.room.song_unlock = b64decode(self.data_recv[5])
self.user.key = self.data_recv[3] self.user.key = b64decode(self.data_recv[3])
self.user.player_id = self.data_recv[4] self.user.player_id = int(self.data_recv[4])

View File

@@ -0,0 +1,247 @@
from .download import DownloadList
from .error import NoData
from .save import SaveData
from .score import Score
from .sql import Connect, Sql
from .user import User
class BaseOperation:
_name: str = None
def __init__(self, *args, **kwargs):
pass
def __call__(self, *args, **kwargs) -> None:
return self.run(*args, **kwargs)
def set_params(self, *args, **kwargs) -> None:
pass
def run(self, *args, **kwargs) -> None:
raise NotImplementedError
class RefreshAllScoreRating(BaseOperation):
'''
刷新所有成绩的评分
'''
_name = 'refresh_all_score_rating'
def run(self):
# 追求效率不用Song类尽量不用对象
# 但其实还是很慢
with Connect() as c:
c.execute(
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
x = c.fetchall()
songs = [i[0] for i in x]
c.execute(
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
for i in x:
for j in range(0, 4):
defnum = -10 # 没在库里的全部当做定数-10
if i[j+1] is not None and i[j+1] > 0:
defnum = float(i[j+1]) / 10
c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', {
'a': i[0], 'b': j})
y = c.fetchall()
values = []
where_values = []
for k in y:
ptt = Score.calculate_rating(defnum, k[1])
ptt = max(ptt, 0)
values.append((ptt,))
where_values.append((k[0], i[0], j))
if values:
Sql(c).update_many('best_score', ['rating'], values, [
'user_id', 'song_id', 'difficulty'], where_values)
class RefreshSongFileCache(BaseOperation):
'''
刷新歌曲文件缓存包括文件hash缓存重建、文件目录重遍历、songlist重解析
注意在设置里预先计算关闭的情况下文件hash不会计算
'''
_name = 'refresh_song_file_cache'
def run(self):
DownloadList.clear_all_cache()
DownloadList.initialize_cache()
class SaveUpdateScore(BaseOperation):
'''
云存档更新成绩,是覆盖式更新
提供user参数时只更新该用户的成绩否则更新所有用户的成绩
'''
_name = 'save_update_score'
def __init__(self, user=None):
self.user = user
def set_params(self, user_id: int = None, *args, **kwargs):
if user_id is not None:
self.user = User()
self.user.user_id = int(user_id)
def run(self, user=None):
'''
parameter:
`user` - `User`类或子类的实例
'''
if user is not None:
self.user = user
if self.user is not None and self.user.user_id is not None:
self._one_user_update()
else:
self._all_update()
def _one_user_update(self):
with Connect() as c:
save = SaveData(c)
save.select_scores(self.user)
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
for i in save.clearlamps_data}
song_id_1 = [i['song_id'] for i in save.scores_data]
song_id_2 = [i['song_id'] for i in save.clearlamps_data]
song_id = list(set(song_id_1 + song_id_2))
c.execute(
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id in ({','.join(['?']*len(song_id))})''', song_id)
x = c.fetchall()
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
for i in x} # chart const * 10
new_scores = []
for i in save.scores_data:
rating = 0
if i['song_id'] in song_chart_const:
rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
rating = max(rating, 0)
y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state:
clear_type = clear_state[y]
else:
clear_type = 0
new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
c.executemany(
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
def _all_update(self):
with Connect() as c:
c.execute(
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
for i in c.fetchall()} # chart const * 10
c.execute('''select user_id from user_save''')
for y in c.fetchall():
user = User()
user.user_id = y[0]
save = SaveData(c)
save.select_scores(user)
clear_state = {f'{i["song_id"]}{i["difficulty"]}': i['clear_type']
for i in save.clearlamps_data}
new_scores = []
for i in save.scores_data:
rating = 0
if i['song_id'] in song_chart_const:
rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
rating = max(rating, 0)
y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state:
clear_type = clear_state[y]
else:
clear_type = 0
new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'],
i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating))
c.executemany(
'''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores)
class UnlockUserItem(BaseOperation):
'''
全解锁/锁定用户物品
提供user参数时只更新该用户的否则更新所有用户的
'''
_name = 'unlock_user_item'
ALLOW_TYPES = ['single', 'pack', 'world_song',
'course_banner', 'world_unlock']
def __init__(self, user=None, method: str = 'unlock', item_types: list = ['single', 'pack']):
self.user = user
self.set_params(method=method, item_types=item_types)
def set_params(self, user_id: int = None, method: str = 'unlock', item_types: list = ['single', 'pack'], *args, **kwargs):
if user_id is not None:
self.user = User()
self.user.user_id = int(user_id)
if method in ['unlock', 'lock']:
self.method = method
if isinstance(item_types, list) and all(i in self.ALLOW_TYPES for i in item_types):
self.item_types = item_types
def run(self):
if self.user is not None and self.user.user_id is not None:
if self.method == 'unlock':
self._one_user_insert()
else:
self._one_user_delete()
else:
if self.method == 'unlock':
self._all_insert()
else:
self._all_delete()
def _one_user_insert(self):
with Connect() as c:
c.execute(
'''select exists(select * from user where user_id = ?)''', (self.user.user_id,))
if not c.fetchone()[0]:
raise NoData(
f'No such user: `{self.user.user_id}`', api_error_code=-110)
c.execute(
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
sql_list = [(self.user.user_id, i[0], i[1])
for i in c.fetchall()]
c.executemany(
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
def _all_insert(self):
with Connect() as c:
c.execute('''select user_id from user''')
x = c.fetchall()
c.execute(
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
y = c.fetchall()
sql_list = [(i[0], j[0], j[1])
for i in x for j in y]
c.executemany(
'''insert or ignore into user_item values (?, ?, ?, 1)''', sql_list)
def _one_user_delete(self):
with Connect() as c:
c.execute(
f'''delete from user_item where user_id = ? and type in ({','.join(['?'] * len(self.item_types))})''', (self.user.user_id, *self.item_types))
def _all_delete(self):
with Connect() as c:
c.execute(
f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)

View File

@@ -1,11 +1,18 @@
from time import time from time import time
from core.item import ItemFactory
from .error import ArcError, NoData from .error import ArcError, NoData
from .item import CollectionItemMixin, ItemFactory
class Present: class Present(CollectionItemMixin):
collection_item_const = {
'name': 'present',
'table_name': 'present_item',
'table_primary_key': 'present_id',
'id_name': 'present_id',
'items_name': 'items'
}
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.present_id: str = None self.present_id: str = None
@@ -18,15 +25,39 @@ class Present:
def is_expired(self) -> bool: def is_expired(self) -> bool:
return self.expire_ts < int(time() * 1000) return self.expire_ts < int(time() * 1000)
def to_dict(self) -> dict: def to_dict(self, has_items: bool = True) -> dict:
return { r = {
'present_id': self.present_id, 'present_id': self.present_id,
'expire_ts': self.expire_ts, 'expire_ts': self.expire_ts,
'description': self.description, 'description': self.description
'items': [x.to_dict() for x in self.items]
} }
if has_items:
r['items'] = [x.to_dict() for x in self.items]
return r
def select(self, present_id: str = None) -> None: def from_dict(self, d: dict) -> 'Present':
self.present_id = d['present_id']
self.expire_ts = int(d['expire_ts'])
self.description = d.get('description', '')
self.items = [ItemFactory.from_dict(
i, c=self.c) for i in d.get('items', [])]
return self
def from_list(self, l: list) -> 'Present':
self.present_id = l[0]
self.expire_ts = int(l[1]) if l[1] else 0
self.description = l[2] if l[2] else ''
return self
def select_exists(self) -> bool:
'''
查询present是否存在
'''
self.c.execute(
'''select exists(select * from present where present_id=?)''', (self.present_id,))
return bool(self.c.fetchone()[0])
def select(self, present_id: str = None) -> 'Present':
''' '''
用present_id查询信息 用present_id查询信息
''' '''
@@ -39,8 +70,8 @@ class Present:
if x is None: if x is None:
raise NoData('The present `%s` does not exist.' % self.present_id) raise NoData('The present `%s` does not exist.' % self.present_id)
self.expire_ts = x[1] if x[1] else 0 self.from_list(x)
self.description = x[2] if x[2] else '' return self
def select_items(self) -> None: def select_items(self) -> None:
''' '''
@@ -48,21 +79,49 @@ class Present:
''' '''
self.c.execute( self.c.execute(
'''select * from present_item where present_id=:a''', {'a': self.present_id}) '''select * from present_item where present_id=:a''', {'a': self.present_id})
x = self.c.fetchall()
if not x:
raise NoData('The present `%s` does not have any items.' %
self.present_id)
self.items = [ItemFactory.from_dict({ self.items = [ItemFactory.from_dict({
'item_id': i[1], 'item_id': i[1],
'type': i[2], 'type': i[2],
'amount': i[3] if i[3] else 1 'amount': i[3] if i[3] else 1
}, self.c) for i in x] }, self.c) for i in self.c.fetchall()]
def insert_items(self) -> None:
for i in self.items:
self.c.execute('''insert or ignore into item values(?,?,?)''',
(i.item_id, i.item_type, i.is_available))
self.c.execute('''insert or ignore into present_item values(?,?,?,?)''',
(self.present_id, i.item_id, i.item_type, i.amount))
def insert(self) -> None:
self.c.execute('''insert into present values(?,?,?)''',
(self.present_id, self.expire_ts, self.description))
def insert_all(self) -> None:
self.insert()
self.insert_items()
def delete(self) -> None:
self.c.execute(
'''delete from present where present_id=?''', (self.present_id,))
def delete_present_item(self) -> None:
self.c.execute(
'''delete from present_item where present_id=?''', (self.present_id,))
def delete_all(self) -> None:
self.delete_present_item()
self.delete()
def update(self) -> None:
self.c.execute('''update present set expire_ts=?, description=? where present_id=?''',
(self.expire_ts, self.description, self.present_id))
class UserPresent(Present): class UserPresent(Present):
''' '''
用户登录奖励类\ 用户登录奖励类
忽视了description的多语言\ 忽视了description的多语言
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''

View File

@@ -1,14 +1,21 @@
from time import time from time import time
from .error import NoData, TicketNotEnough from .error import InputError, NoData, TicketNotEnough
from .item import ItemFactory from .item import CollectionItemMixin, ItemFactory
class Purchase: class Purchase(CollectionItemMixin):
''' '''
购买类\ 购买类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
collection_item_const = {
'name': 'purchase',
'table_name': 'purchase_item',
'table_primary_key': 'purchase_name',
'id_name': 'purchase_name',
'items_name': 'items'
}
def __init__(self, c=None, user=None): def __init__(self, c=None, user=None):
self.c = c self.c = c
@@ -22,6 +29,8 @@ class Purchase:
self.items: list = [] self.items: list = []
# TODO: "discount_reason": "extend"
@property @property
def price_displayed(self) -> int: def price_displayed(self) -> int:
''' '''
@@ -32,27 +41,87 @@ class Purchase:
if self.discount_reason == 'anni5tix': if self.discount_reason == 'anni5tix':
x = ItemFactory(self.c).get_item('anni5tix') x = ItemFactory(self.c).get_item('anni5tix')
x.item_id = 'anni5tix' x.item_id = 'anni5tix'
x.select(self.user) x.select_user_item(self.user)
if x.amount >= 1: if x.amount >= 1:
return 0 return 0
return self.price return self.price
return self.orig_price return self.orig_price
def to_dict(self) -> dict: def to_dict(self, has_items: bool = True, show_real_price: bool = True) -> dict:
if show_real_price:
price = self.price_displayed price = self.price_displayed
r = { r = {
'name': self.purchase_name, 'name': self.purchase_name,
'price': price, 'price': price if show_real_price else self.price,
'orig_price': self.orig_price, 'orig_price': self.orig_price,
'items': [x.to_dict(has_is_available=True) for x in self.items]
} }
if has_items:
r['items'] = [x.to_dict(has_is_available=True) for x in self.items]
if self.discount_from > 0 and self.discount_to > 0: if self.discount_from > 0 and self.discount_to > 0:
r['discount_from'] = self.discount_from r['discount_from'] = self.discount_from
r['discount_to'] = self.discount_to r['discount_to'] = self.discount_to
if self.discount_reason == 'anni5tix' and price == 0: if not show_real_price or (self.discount_reason == 'anni5tix' and price == 0):
r['discount_reason'] = self.discount_reason r['discount_reason'] = self.discount_reason
return r return r
def from_dict(self, d: dict) -> 'Purchase':
self.purchase_name = d.get('name') or d.get('purchase_name')
if not self.purchase_name:
raise InputError('purchase_name is required')
self.orig_price = int(d['orig_price'])
self.price = d.get('price', self.orig_price)
self.discount_from = d.get('discount_from', -1)
self.discount_to = d.get('discount_to', -1)
self.discount_reason = d.get('discount_reason', '')
for i in d.get('items', []):
self.items.append(ItemFactory.from_dict(i))
return self
def from_list(self, l: list) -> 'Purchase':
self.purchase_name = l[0]
self.price = l[1]
self.orig_price = l[2]
self.discount_from = l[3] if l[3] else -1
self.discount_to = l[4] if l[4] else -1
self.discount_reason = l[5] if l[5] else ''
return self
def insert_all(self) -> None:
'''向数据库插入包括item表和purchase_item表'''
self.insert()
self.insert_items()
def insert(self) -> None:
'''向数据库插入不包括item表和purchase_item表'''
self.c.execute('''insert into purchase values(?,?,?,?,?,?)''',
(self.purchase_name, self.price, self.orig_price, self.discount_from, self.discount_to, self.discount_reason))
# def insert_only_purchase_item(self) -> None:
# '''向数据库插入purchase_item表'''
# for i in self.items:
# self.c.execute('''insert into purchase_item values(?,?,?,?)''',
# (self.purchase_name, i.item_id, i.item_type, i.amount))
def insert_items(self) -> None:
'''向数据库插入物品,注意已存在的物品不会变更'''
for i in self.items:
self.c.execute('''insert or ignore into item values(?,?,?)''',
(i.item_id, i.item_type, i.is_available))
self.c.execute('''insert or ignore into purchase_item values(?,?,?,?)''',
(self.purchase_name, i.item_id, i.item_type, i.amount))
def select_exists(self, purchase_name: str = None) -> bool:
'''
用purchase_name查询存在性
'''
if purchase_name:
self.purchase_name = purchase_name
self.c.execute(
'''select exists(select * from purchase where purchase_name=?)''', (self.purchase_name,))
return self.c.fetchone() == (1,)
def select(self, purchase_name: str = None) -> 'Purchase': def select(self, purchase_name: str = None) -> 'Purchase':
''' '''
用purchase_name查询信息 用purchase_name查询信息
@@ -61,11 +130,11 @@ class Purchase:
self.purchase_name = purchase_name self.purchase_name = purchase_name
self.c.execute( self.c.execute(
'''select * from purchase where purchase_name=:name''', {'name': purchase_name}) '''select * from purchase where purchase_name=:name''', {'name': self.purchase_name})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('The purchase `%s` does not exist.' % raise NoData(
purchase_name, 501) f'Purchase `{self.purchase_name}` does not exist.', 501)
self.price = x[1] self.price = x[1]
self.orig_price = x[2] self.orig_price = x[2]
@@ -80,9 +149,9 @@ class Purchase:
self.c.execute( self.c.execute(
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name}) '''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
x = self.c.fetchall() x = self.c.fetchall()
if not x: # if not x:
raise NoData('The items of the purchase `%s` does not exist.' % # raise NoData(
self.purchase_name, 501) # f'The items of the purchase `{self.purchase_name}` does not exist.', 501)
self.items = [] self.items = []
t = None t = None
@@ -130,10 +199,31 @@ class Purchase:
for i in self.items: for i in self.items:
i.user_claim_item(self.user) i.user_claim_item(self.user)
def delete_purchase_item(self) -> None:
'''删除purchase_item表'''
self.c.execute(
'''delete from purchase_item where purchase_name=?''', (self.purchase_name, ))
def delete(self) -> None:
'''删除purchase表'''
self.c.execute(
'''delete from purchase where purchase_name=?''', (self.purchase_name, ))
def delete_all(self) -> None:
'''删除purchase表和purchase_item表'''
self.delete_purchase_item()
self.delete()
def update(self) -> None:
'''更新purchase表'''
self.c.execute('''update purchase set price=:a, orig_price=:b, discount_from=:c, discount_to=:d, discount_reason=:e where purchase_name=:f''', {
'a': self.price, 'b': self.orig_price, 'c': self.discount_from, 'd': self.discount_to, 'e': self.discount_reason, 'f': self.purchase_name})
class PurchaseList: class PurchaseList:
''' '''
购买列表类\ 购买列表类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''

View File

@@ -1,13 +1,15 @@
from .constant import Constant from .constant import Constant
from .score import UserScore from .score import UserScore
from .song import Chart from .song import Chart
from .sql import Query, Sql
from .user import UserInfo from .user import UserInfo
class RankList: class RankList:
''' '''
排行榜类\ 排行榜类
默认limit=20limit<0认为是all\ 默认limit=20limit<0认为是all
property: `user` - `User`类或者子类的实例 property: `user` - `User`类或者子类的实例
''' '''
@@ -26,24 +28,29 @@ class RankList:
得到top分数表 得到top分数表
''' '''
if self.limit >= 0: if self.limit >= 0:
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', { self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit}) 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
else: else:
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', { self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', {
'song_id': self.song.song_id, 'difficulty': self.song.difficulty}) 'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
x = self.c.fetchall() x = self.c.fetchall()
if not x: if not x:
return None return None
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
user_info_dict = {i[0]: i[1:] for i in user_info_list}
rank = 0 rank = 0
self.list = [] self.list = []
for i in x: for i in x:
rank += 1 rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0])) y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
y.song = self.song y.song = self.song
y.select_score()
y.rank = rank y.rank = rank
y.user.from_list_about_character(user_info_dict[i[0]])
self.list.append(y) self.list.append(y)
def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None: def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None:
@@ -54,26 +61,60 @@ class RankList:
if user: if user:
self.user = user self.user = user
self.c.execute('''select user_id from best_score where user_id in (select :user_id union select user_id_other from friend where user_id_me = :user_id) and song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', { user_ids = [self.user.user_id] + [x[0] for x in self.user.friend_ids]
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
self.c.execute(f'''select * from best_score where user_id in ({','.join(['?'] * len(user_ids))}) and song_id = ? and difficulty = ? order by score DESC, time_played DESC limit ?''', user_ids + [
self.song.song_id, self.song.difficulty, self.limit])
x = self.c.fetchall() x = self.c.fetchall()
if not x: if not x:
return None return None
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
user_info_dict = {i[0]: i[1:] for i in user_info_list}
rank = 0 rank = 0
self.list = [] self.list = []
for i in x: for i in x:
rank += 1 rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0])) y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
y.song = self.song y.song = self.song
y.select_score()
y.rank = rank y.rank = rank
y.user.from_list_about_character(user_info_dict[i[0]])
self.list.append(y) self.list.append(y)
@staticmethod
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: def select_me(self, user=None) -> None:
''' '''
得到我的排名分数表\ 得到我的排名分数表
尚不清楚这个函数有没有问题
''' '''
if user: if user:
self.user = user self.user = user
@@ -85,73 +126,34 @@ class RankList:
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) )''', { 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]}) 'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'score': x[0], 'time_played': x[1]})
x = self.c.fetchone() my_rank = int(self.c.fetchone()[0]) + 1
myrank = int(x[0]) + 1
self.c.execute('''select count(*) from best_score where song_id=:a and difficulty=:b''', self.c.execute('''select count(*) from best_score where song_id=:a and difficulty=:b''',
{'a': self.song.song_id, 'b': self.song.difficulty}) {'a': self.song.song_id, 'b': self.song.difficulty})
amount = int(self.c.fetchone()[0])
if myrank <= 4: # 排名在前4 sql_limit, sql_offset, need_myself = self.get_my_rank_parameter(
self.select_top() my_rank, int(self.c.fetchone()[0]), self.limit)
elif myrank >= 5 and myrank <= 9999 - self.limit + 4 and amount >= 10000: # 万名内前面有4个人 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''', {
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', { 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': sql_limit, 'offset': sql_offset})
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit, 'offset': myrank - 5})
x = self.c.fetchall() x = self.c.fetchall()
if x: if x:
rank = myrank - 5 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 = [] self.list = []
for i in x: for i in x:
rank += 1 rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0])) y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
y.song = self.song y.song = self.song
y.select_score()
y.rank = rank y.rank = rank
y.user.from_list_about_character(user_info_dict[i[0]])
self.list.append(y) self.list.append(y)
elif myrank >= 10000: # 万名外 if need_myself:
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit - 1, 'offset': 9999-self.limit})
x = self.c.fetchall()
if x:
rank = 9999 - self.limit
for i in x:
rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0]))
y.song = self.song
y.select_score()
y.rank = rank
self.list.append(y)
y = UserScore(self.c, UserInfo(self.c, self.user.user_id)) y = UserScore(self.c, UserInfo(self.c, self.user.user_id))
y.song = self.song y.song = self.song
y.select_score()
y.rank = -1 y.rank = -1
self.list.append(y) self.list.append(y)
elif amount - myrank < self.limit - 5: # 后方人数不足
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit, 'offset': amount - self.limit})
x = self.c.fetchall()
if x:
rank = amount - self.limit
if rank < 0:
rank = 0
for i in x:
rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0]))
y.song = self.song
y.select_score()
y.rank = rank
self.list.append(y)
else:
self.c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit, 'offset': 9998-self.limit})
x = self.c.fetchall()
if x:
rank = 9998 - self.limit
for i in x:
rank += 1
y = UserScore(self.c, UserInfo(self.c, i[0]))
y.song = self.song
y.select_score()
y.rank = rank
self.list.append(y)

View File

@@ -1,8 +1,16 @@
from .error import NoData, RedeemUnavailable from .error import NoData, RedeemUnavailable
from .item import ItemFactory from .item import CollectionItemMixin, ItemFactory
class Redeem: class Redeem(CollectionItemMixin):
collection_item_const = {
'name': 'redeem',
'table_name': 'redeem_item',
'table_primary_key': 'code',
'id_name': 'code',
'items_name': 'items'
}
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.code: str = None self.code: str = None
@@ -11,34 +19,87 @@ class Redeem:
self.items: list = [] self.items: list = []
self.fragment: int = None self.fragment: int = None
def select(self, code: str = None) -> None: def to_dict(self, has_items: bool = True) -> dict:
r = {
'code': self.code,
'type': self.redeem_type
}
if has_items:
r['items'] = [x.to_dict() for x in self.items]
return r
def from_dict(self, d: dict) -> 'Redeem':
self.code = str(d['code'])
self.redeem_type = int(d.get('type') or d.get('redeem_type', 0))
self.items = [ItemFactory.from_dict(
i, c=self.c) for i in d.get('items', [])]
return self
def from_list(self, l: list) -> 'Redeem':
self.code = l[0]
self.redeem_type = l[1]
return self
def select_exists(self) -> bool:
self.c.execute(
'''select exists(select * from redeem where code=?)''', (self.code,))
return bool(self.c.fetchone()[0])
def select(self, code: str = None) -> 'Redeem':
if code: if code:
self.code = code self.code = code
self.c.execute('''select * from redeem where code=:a''', self.c.execute('''select * from redeem where code=:a''',
{'a': self.code}) {'a': self.code})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The redeem `%s` does not exist.' % self.code, 504) raise NoData(f'The redeem `{self.code}` does not exist.', 504)
self.redeem_type = x[1] self.redeem_type = x[1]
return self
def select_items(self) -> None: def select_items(self) -> None:
self.c.execute('''select * from redeem_item where code=:a''', self.c.execute('''select * from redeem_item where code=:a''',
{'a': self.code}) {'a': self.code})
x = self.c.fetchall()
if not x:
raise NoData(
'The redeem `%s` does not have any items.' % self.code)
self.items = [ItemFactory.from_dict({ self.items = [ItemFactory.from_dict({
'item_id': i[1], 'item_id': i[1],
'type': i[2], 'type': i[2],
'amount': i[3] if i[3] else 1 'amount': i[3] if i[3] else 1
}, self.c) for i in x] }, self.c) for i in self.c.fetchall()]
def insert(self) -> None:
self.c.execute('''insert into redeem values(?,?)''',
(self.code, self.redeem_type))
def insert_items(self) -> None:
for i in self.items:
i.insert(ignore=True)
self.c.execute('''insert into redeem_item values(?,?,?,?)''', (
self.code, i.item_id, i.item_type, i.amount))
def insert_all(self) -> None:
self.insert()
self.insert_items()
def delete(self) -> None:
self.c.execute('''delete from redeem where code=?''', (self.code,))
def delete_redeem_item(self) -> None:
self.c.execute(
'''delete from redeem_item where code=?''', (self.code,))
def delete_all(self) -> None:
self.delete_redeem_item()
self.delete()
def update(self) -> None:
self.c.execute('''update redeem set type=? where code=?''',
(self.redeem_type, self.code))
class UserRedeem(Redeem): class UserRedeem(Redeem):
''' '''
用户兑换码类\ 用户兑换码类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -77,7 +138,7 @@ class UserRedeem(Redeem):
if self.redeem_type == 0: if self.redeem_type == 0:
raise RedeemUnavailable( raise RedeemUnavailable(
'The redeem `%s` is unavailable.' % self.code) 'The redeem `%s` is unavailable.' % self.code)
elif self.redeem_type == 1: if self.redeem_type == 1:
raise RedeemUnavailable( raise RedeemUnavailable(
'The redeem `%s` is unavailable.' % self.code, 506) 'The redeem `%s` is unavailable.' % self.code, 506)

View File

@@ -1,11 +1,9 @@
import json import json
from time import time from time import time
from setting import Config from .config_manager import Config
from .constant import Constant
from core.constant import Constant from .error import InputError, NoData
from .error import InputError
from .util import md5 from .util import md5
@@ -13,6 +11,8 @@ class SaveData:
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.user = None
self.scores_data = [] self.scores_data = []
self.clearlamps_data = [] self.clearlamps_data = []
self.clearedsongs_data = [] self.clearedsongs_data = []
@@ -56,6 +56,20 @@ class SaveData:
} }
} }
def select_scores(self, user) -> None:
'''
parameter: `user` - `User`类或子类的实例
'''
self.user = user
self.c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''',
{'a': user.user_id})
x = self.c.fetchone()
if not x:
raise NoData(f'User `{user.user_id}` has no cloud save data')
self.scores_data: list = json.loads(x[0])[""]
self.clearlamps_data: list = json.loads(x[1])[""]
def select_all(self, user) -> None: def select_all(self, user) -> None:
''' '''
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
@@ -101,21 +115,21 @@ class SaveData:
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
self.createdAt = int(time() * 1000) self.createdAt = int(time() * 1000)
self.c.execute('''delete from user_save where user_id=:a''', { self.c.execute('''insert or replace into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j)''', {
'a': user.user_id})
self.c.execute('''insert into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j)''', {
'a': user.user_id, 'b': json.dumps({'': self.scores_data}), 'c': json.dumps({'': self.clearlamps_data}), 'd': json.dumps({'': self.clearedsongs_data}), 'e': json.dumps({'': self.unlocklist_data}), 'f': json.dumps({'val': self.installid_data}), 'g': json.dumps({'val': self.devicemodelname_data}), 'h': json.dumps({'': self.story_data}), 'i': self.createdAt, 'j': self.finalestate_data}) 'a': user.user_id, 'b': json.dumps({'': self.scores_data}), 'c': json.dumps({'': self.clearlamps_data}), 'd': json.dumps({'': self.clearedsongs_data}), 'e': json.dumps({'': self.unlocklist_data}), 'f': json.dumps({'val': self.installid_data}), 'g': json.dumps({'val': self.devicemodelname_data}), 'h': json.dumps({'': self.story_data}), 'i': self.createdAt, 'j': self.finalestate_data})
def set_value(self, key: str, value: str, checksum: str) -> None: def set_value(self, key: str, value: str, checksum: str) -> None:
''' '''
从Arcaea客户端给的奇怪字符串中获取存档数据并进行数据校验 从Arcaea客户端给的奇怪字符串中获取存档数据并进行数据校验
''' '''
if not value:
return None
if key not in self.__dict__: if key not in self.__dict__:
raise KeyError( raise KeyError(
'Property `%s` is not found in the instance of `SaveData` class.' % key) 'Property `%s` is not found in the instance of `SaveData` class.' % key)
if md5(value) == checksum: if md5(value) == checksum:
if key == 'installid_data' or key == 'devicemodelname_data' or key == 'finalestate_data': if key in ('installid_data', 'devicemodelname_data', 'finalestate_data'):
self.__dict__[key] = json.loads(value)['val'] self.__dict__[key] = json.loads(value)['val']
else: else:
self.__dict__[key] = json.loads(value)[''] self.__dict__[key] = json.loads(value)['']

View File

@@ -2,18 +2,22 @@ from base64 import b64encode
from os import urandom from os import urandom
from time import time from time import time
from .bgtask import BGTask, logdb_execute
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .course import CoursePlay from .course import CoursePlay
from .error import NoData, StaminaNotEnough from .error import NoData, StaminaNotEnough
from .item import ItemCore from .item import ItemCore
from .song import Chart from .song import Chart
from .sql import Query, Sql from .sql import Connect, Query, Sql
from .util import md5 from .util import get_today_timestamp, md5
from .world import WorldPlay from .world import WorldPlay
class Score: class Score:
def __init__(self) -> None: def __init__(self) -> None:
self.c = None
self.song: 'Chart' = Chart() self.song: 'Chart' = Chart()
self.score: int = None self.score: int = None
self.shiny_perfect_count: int = None self.shiny_perfect_count: int = None
@@ -45,17 +49,16 @@ class Score:
'''分数转换为评级''' '''分数转换为评级'''
if score >= 9900000: # EX+ if score >= 9900000: # EX+
return 6 return 6
elif 9800000 <= score < 9900000: # EX if score >= 9800000: # EX
return 5 return 5
elif 9500000 <= score < 9800000: # AA if score >= 9500000: # AA
return 4 return 4
elif 9200000 <= score < 9500000: # A if score >= 9200000: # A
return 3 return 3
elif 8900000 <= score < 9200000: # B if score >= 8900000: # B
return 2 return 2
elif 8600000 <= score < 8900000: # C if score >= 8600000: # C
return 1 return 1
else:
return 0 return 0
@property @property
@@ -67,21 +70,24 @@ class Score:
'''clear_type转换为成绩状态用数字大小标识便于比较''' '''clear_type转换为成绩状态用数字大小标识便于比较'''
if clear_type == 3: # PM if clear_type == 3: # PM
return 5 return 5
elif clear_type == 2: # FC if clear_type == 2: # FC
return 4 return 4
elif clear_type == 5: # Hard Clear if clear_type == 5: # Hard Clear
return 3 return 3
elif clear_type == 1: # Clear if clear_type == 1: # Clear
return 2 return 2
elif clear_type == 4: # Easy Clear if clear_type == 4: # Easy Clear
return 1 return 1
else: # Track Lost return 0 # Track Lost
return 0
@property @property
def song_state(self) -> int: def song_state(self) -> int:
return self.get_song_state(self.clear_type) return self.get_song_state(self.clear_type)
@property
def all_note_count(self) -> int:
return self.perfect_count + self.near_count + self.miss_count
@property @property
def is_valid(self) -> bool: def is_valid(self) -> bool:
'''分数有效性检查''' '''分数有效性检查'''
@@ -90,7 +96,7 @@ class Score:
if self.song.difficulty not in (0, 1, 2, 3): if self.song.difficulty not in (0, 1, 2, 3):
return False return False
all_note = self.perfect_count + self.near_count + self.miss_count all_note = self.all_note_count
if all_note == 0: if all_note == 0:
return False return False
@@ -102,8 +108,8 @@ class Score:
return True return True
@staticmethod @staticmethod
def calculate_rating(defnum: int, score: int) -> float: def calculate_rating(defnum: float, score: int) -> float:
'''计算rating谱面定数小于等于0视为Unrank这里的defnum = Chart const''' '''计算rating谱面定数小于等于0视为Unrank返回值会为-1这里的defnum = Chart const'''
if not defnum or defnum <= 0: if not defnum or defnum <= 0:
# 谱面没定数或者定数小于等于0被视作Unrank # 谱面没定数或者定数小于等于0被视作Unrank
return -1 return -1
@@ -112,8 +118,7 @@ class Score:
ptt = defnum + 2 ptt = defnum + 2
elif score < 9800000: elif score < 9800000:
ptt = defnum + (score-9500000) / 300000 ptt = defnum + (score-9500000) / 300000
if ptt < 0: ptt = max(ptt, 0)
ptt = 0
else: else:
ptt = defnum + 1 + (score-9800000) / 200000 ptt = defnum + 1 + (score-9800000) / 200000
@@ -128,7 +133,7 @@ class Score:
return self.rating return self.rating
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { r = {
"rating": self.rating, "rating": self.rating,
"modifier": self.modifier, "modifier": self.modifier,
"time_played": self.time_played, "time_played": self.time_played,
@@ -142,6 +147,9 @@ class Score:
"difficulty": self.song.difficulty, "difficulty": self.song.difficulty,
"song_id": self.song.song_id "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): class UserScore(Score):
@@ -152,9 +160,10 @@ class UserScore(Score):
super().__init__() super().__init__()
self.c = c self.c = c
self.user = user self.user = user
self.rank = None self.rank = None # 成绩排名给Ranklist用的
def select_score(self) -> None: def select_score(self) -> None:
'''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询'''
self.c.execute('''select * from best_score where user_id = :a and song_id = :b and difficulty = :c''', self.c.execute('''select * from best_score where user_id = :a and song_id = :b and difficulty = :c''',
{'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty}) {'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
x = self.c.fetchone() x = self.c.fetchone()
@@ -205,6 +214,7 @@ class UserPlay(UserScore):
self.stamina_multiply: int = None self.stamina_multiply: int = None
self.fragment_multiply: int = None self.fragment_multiply: int = None
self.prog_boost_multiply: int = None self.prog_boost_multiply: int = None
self.beyond_boost_gauge_usage: int = None
self.ptt: Potential = None # 临时用来计算用户ptt的 self.ptt: Potential = None # 临时用来计算用户ptt的
self.world_play: 'WorldPlay' = None self.world_play: 'WorldPlay' = None
@@ -212,7 +222,10 @@ class UserPlay(UserScore):
self.course_play_state: int = None self.course_play_state: int = None
self.course_play: 'CoursePlay' = None self.course_play: 'CoursePlay' = None
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
def to_dict(self) -> dict: def to_dict(self) -> dict:
# 不能super
if self.is_world_mode is None or self.course_play_state is None: if self.is_world_mode is None or self.course_play_state is None:
return {} return {}
if self.course_play_state == 4: if self.course_play_state == 4:
@@ -222,8 +235,9 @@ class UserPlay(UserScore):
else: else:
r = {} r = {}
r['user_rating'] = self.user.rating_ptt r['user_rating'] = self.user.rating_ptt
r['finale_challenge_higher'] = self.rating > self.ptt.value
r['global_rank'] = self.user.global_rank r['global_rank'] = self.user.global_rank
r['finale_play_value'] = 0 # emmmm r['finale_play_value'] = 9.065 * self.rating ** 0.5 # by Lost-MSth
return r return r
@property @property
@@ -243,10 +257,15 @@ class UserPlay(UserScore):
if songfile_hash and songfile_hash != self.song_hash: if songfile_hash and songfile_hash != self.song_hash:
return False return False
x = self.song_token + self.song_hash + self.song.song_id + str(self.song.difficulty) + str(self.score) + str(self.shiny_perfect_count) + str( x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
self.perfect_count) + str(self.near_count) + str(self.miss_count) + str(self.health) + str(self.modifier) + str(self.clear_type) if self.combo_interval_bonus is not None:
y = str(self.user.user_id) + self.song_hash if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
return False
x = x + str(self.combo_interval_bonus)
y = f'{self.user.user_id}{self.song_hash}'
checksum = md5(x+md5(y)) checksum = md5(x+md5(y))
if checksum != self.submission_hash: if checksum != self.submission_hash:
return False return False
@@ -254,6 +273,12 @@ class UserPlay(UserScore):
def get_play_state(self) -> None: def get_play_state(self) -> None:
'''检查token当然这里不管有没有是用来判断世界模式和课题模式的''' '''检查token当然这里不管有没有是用来判断世界模式和课题模式的'''
if self.song_token == '1145141919810':
# 硬编码检查,绕过数据库
self.is_world_mode = False
self.course_play_state = -1
return None
self.c.execute( self.c.execute(
'''select * from songplay_token where token=:a ''', {'a': self.song_token}) '''select * from songplay_token where token=:a ''', {'a': self.song_token})
x = self.c.fetchone() x = self.c.fetchone()
@@ -261,7 +286,8 @@ class UserPlay(UserScore):
self.is_world_mode = False self.is_world_mode = False
self.course_play_state = -1 self.course_play_state = -1
return None return None
self.song.set_chart(x[2], x[3]) # raise NoData('No token data.')
# self.song.set_chart(x[2], x[3])
if x[4]: if x[4]:
self.course_play = CoursePlay(self.c, self.user, self) self.course_play = CoursePlay(self.c, self.user, self)
self.course_play.course_id = x[4] self.course_play.course_id = x[4]
@@ -273,24 +299,29 @@ class UserPlay(UserScore):
self.stamina_multiply = int(x[8]) self.stamina_multiply = int(x[8])
self.fragment_multiply = int(x[9]) self.fragment_multiply = int(x[9])
self.prog_boost_multiply = int(x[10]) self.prog_boost_multiply = int(x[10])
self.beyond_boost_gauge_usage = int(x[11])
self.is_world_mode = True self.is_world_mode = True
self.course_play_state = -1 self.course_play_state = -1
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0) -> None: def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0) -> None:
self.song_token = b64encode(urandom(64)).decode() self.song_token = b64encode(urandom(64)).decode()
self.stamina_multiply = int(stamina_multiply) self.stamina_multiply = int(stamina_multiply)
self.fragment_multiply = int(fragment_multiply) self.fragment_multiply = int(fragment_multiply)
self.prog_boost_multiply = int(prog_boost_multiply) self.prog_boost_multiply = int(prog_boost_multiply)
if self.prog_boost_multiply != 0: self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
self.c.execute('''select prog_boost from user where user_id=:a''', { if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
'a': self.user.user_id}) 'a': self.user.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if x and x[0] == 1: if x:
self.prog_boost_multiply = 300 self.prog_boost_multiply = 300 if x[0] == 300 else 0
if x[1] < self.beyond_boost_gauge_usage or self.beyond_boost_gauge_usage not in (100, 200):
# 注意偷懒了没判断是否是beyond图
self.beyond_boost_gauge_usage = 0
self.clear_play_state() self.clear_play_state()
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f)''', { self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g)''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 't': self.song_token}) 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 't': self.song_token})
self.user.select_user_about_current_map() self.user.select_user_about_current_map()
self.user.current_map.select_map_info() self.user.current_map.select_map_info()
@@ -322,7 +353,7 @@ class UserPlay(UserScore):
self.course_play.score = 0 self.course_play.score = 0
self.course_play.clear_type = 3 # 设置为PM即最大值 self.course_play.clear_type = 3 # 设置为PM即最大值
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0)''', (self.song_token, self.user.user_id, self.song.song_id, self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0)''', (self.song_token, self.user.user_id, self.song.song_id,
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type)) self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
self.user.select_user_about_stamina() self.user.select_user_about_stamina()
if use_course_skip_purchase: if use_course_skip_purchase:
@@ -356,8 +387,8 @@ class UserPlay(UserScore):
'''更新此分数对应用户的recent30''' '''更新此分数对应用户的recent30'''
old_recent_10 = self.ptt.recent_10 old_recent_10 = self.ptt.recent_10
if self.is_protected: if self.is_protected:
old_r30 = [x for x in self.ptt.r30] old_r30 = self.ptt.r30.copy()
old_s30 = [x for x in self.ptt.s30] old_s30 = self.ptt.s30.copy()
# 寻找pop位置 # 寻找pop位置
songs = list(set(self.ptt.s30)) songs = list(set(self.ptt.s30))
@@ -371,12 +402,12 @@ class UserPlay(UserScore):
r30_id = 29 r30_id = 29
elif self.song.song_id_difficulty in songs and n == 10: elif self.song.song_id_difficulty in songs and n == 10:
i = 29 i = 29
while self.ptt.s30[i] == self.song.song_id_difficulty: while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0:
i -= 1 i -= 1
r30_id = i r30_id = i
elif self.song.song_id_difficulty not in songs and n == 9: elif self.song.song_id_difficulty not in songs and n == 9:
i = 29 i = 29
while self.ptt.s30.count(self.ptt.s30[-1]) == 1: while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0:
i -= 1 i -= 1
r30_id = i r30_id = i
else: else:
@@ -396,6 +427,23 @@ class UserPlay(UserScore):
self.ptt.insert_recent_30() self.ptt.insert_recent_30()
def record_score(self) -> None:
'''向log数据库记录分数请注意列名不同'''
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def record_rating_ptt(self, user_rating_ptt: float) -> None:
'''向log数据库记录用户ptt变化'''
today_timestamp = get_today_timestamp()
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
self.user.user_id, today_timestamp)).fetchone()
old_ptt = 0 if old_ptt is None else old_ptt[0]
if old_ptt != user_rating_ptt:
c2.execute('''insert or replace into user_rating values(?,?,?)''',
(self.user.user_id, today_timestamp, user_rating_ptt))
def upload_score(self) -> None: def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算''' '''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
self.get_play_state() self.get_play_state()
@@ -408,6 +456,9 @@ class UserPlay(UserScore):
self.time_played = int(time()) self.time_played = int(time())
# 记录分数
self.record_score()
# recent更新 # recent更新
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', { self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000}) 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})
@@ -436,7 +487,9 @@ class UserPlay(UserScore):
self.update_recent30() self.update_recent30()
# 总PTT更新 # 总PTT更新
self.user.rating_ptt = int(self.ptt.value * 100) user_rating_ptt = self.ptt.value
self.user.rating_ptt = int(user_rating_ptt * 100)
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', { self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
'a': self.user.rating_ptt, 'b': self.user.user_id}) 'a': self.user.rating_ptt, 'b': self.user.user_id})
@@ -452,7 +505,8 @@ class UserPlay(UserScore):
class Potential: class Potential:
''' '''
用户潜力值计算处理类\ 用户潜力值计算处理类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''
@@ -460,8 +514,8 @@ class Potential:
self.c = c self.c = c
self.user = user self.user = user
self.r30: list = None self.r30: 'list[float]' = None
self.s30: list = None self.s30: 'list[str]' = None
self.songs_selected: list = None self.songs_selected: list = None
self.b30: list = None self.b30: list = None
@@ -476,13 +530,16 @@ class Potential:
'''获取用户best30的总潜力值''' '''获取用户best30的总潜力值'''
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', { self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
'a': self.user.user_id}) 'a': self.user.user_id})
return sum([x[0] for x in self.c.fetchall()]) return sum(x[0] for x in self.c.fetchall())
def select_recent_30(self) -> None: def select_recent_30(self) -> None:
'''获取用户recent30数据''' '''获取用户recent30数据'''
self.c.execute( self.c.execute(
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id}) '''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x:
raise NoData(
f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3)
self.r30 = [] self.r30 = []
self.s30 = [] self.s30 = []
if not x: if not x:
@@ -517,11 +574,15 @@ class Potential:
def recent_30_to_dict_list(self) -> list: def recent_30_to_dict_list(self) -> list:
if self.r30 is None: if self.r30 is None:
self.select_recent_30() self.select_recent_30()
return [{ r = []
'song_id': self.s30[i][:-1], for x, y in zip(self.s30, self.r30):
'difficulty': int(self.s30[i][-1]), if x:
'rating': self.r30[i] r.append({
} for i in range(len(self.r30))] 'song_id': x[:-1],
'difficulty': int(x[-1]),
'rating': y
})
return r
def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None: def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None:
self.r30.pop(pop_index) self.r30.pop(pop_index)
@@ -544,7 +605,8 @@ class Potential:
class UserScoreList: class UserScoreList:
''' '''
用户分数查询类\ 用户分数查询类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -565,7 +627,16 @@ class UserScoreList:
self.query.query_append({'user_id': self.user.user_id}) self.query.query_append({'user_id': self.user.user_id})
self.query.sort += [{'column': 'rating', 'order': 'DESC'}] self.query.sort += [{'column': 'rating', 'order': 'DESC'}]
print(self.query.sort)
x = Sql(self.c).select('best_score', query=self.query) x = Sql(self.c).select('best_score', query=self.query)
self.scores = [UserScore(self.c, self.user).from_list(i) for i in x] 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 ''

View File

@@ -1,4 +1,5 @@
from .error import NoData from .error import NoData
from .config_manager import Config
class Chart: class Chart:
@@ -6,8 +7,11 @@ class Chart:
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None: def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
self.c = c self.c = c
self.song_id: str = None
self.difficulty: int = None
self.set_chart(song_id, difficulty) self.set_chart(song_id, difficulty)
self.defnum: int = None self.defnum: int = None
self.song_name: str = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@@ -32,14 +36,16 @@ class Chart:
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id}) '''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
if Config.ALLOW_SCORE_WITH_NO_SONG:
self.defnum = -10 self.defnum = -10
# raise NoData('The song `%s` does not exist.' % self.song_id) else:
raise NoData(f'The song `{self.song_id}` does not exist.', 120)
else: else:
self.defnum = x[self.difficulty] self.defnum = x[self.difficulty]
class Song: class Song:
def __init__(self, c=None, song_id=None) -> None: def __init__(self, c=None, song_id: str = None) -> None:
self.c = c self.c = c
self.song_id: str = song_id self.song_id: str = song_id
self.name: str = None self.name: str = None
@@ -64,10 +70,39 @@ class Song:
self.charts[3].defnum = x[5] self.charts[3].defnum = x[5]
return self return self
def from_dict(self, d: dict) -> 'Song':
self.song_id = d['song_id']
self.name = d.get('name', '')
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)]
for i in range(4):
self.charts[i].defnum = -10
for chart in d['charts']:
self.charts[chart['difficulty']].defnum = round(
chart['chart_const'] * 10)
return self
def delete(self) -> None:
self.c.execute(
'''delete from chart where song_id=?''', (self.song_id,))
def update(self) -> None:
'''全部更新'''
self.c.execute(
'''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.song_id))
def insert(self) -> None: def insert(self) -> None:
self.c.execute( self.c.execute(
'''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum)) '''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum))
def select_exists(self, song_id: str = None) -> bool:
if song_id is not None:
self.song_id = song_id
self.c.execute(
'''select exists(select * from chart where song_id=?)''', (self.song_id,))
return bool(self.c.fetchone()[0])
def select(self, song_id: str = None) -> 'Song': def select(self, song_id: str = None) -> 'Song':
if song_id is not None: if song_id is not None:
self.song_id = song_id self.song_id = song_id
@@ -76,6 +111,6 @@ class Song:
'a': self.song_id}) 'a': self.song_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The song `%s` does not exist.' % self.song_id) raise NoData(f'The song `{self.song_id}` does not exist.')
return self.from_list(x) return self.from_list(x)

View File

@@ -1,55 +1,72 @@
import os
import sqlite3 import sqlite3
import traceback import traceback
from atexit import register
from flask import current_app from .config_manager import Config
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
from .constant import Constant from .error import ArcError, InputError
from .error import InputError
class Connect: class Connect:
# 数据库连接类,上下文管理 # 数据库连接类,上下文管理
logger = None
def __init__(self, file_path=Constant.SQLITE_DATABASE_PATH): def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
""" """
数据库连接默认连接arcaea_database.db\ 数据库连接默认连接arcaea_database.db
接受:文件路径\ 接受:文件路径
返回sqlite3连接操作对象 返回sqlite3连接操作对象
""" """
self.file_path = file_path self.file_path = file_path
self.in_memory: bool = in_memory
if logger is not None:
self.logger = logger
def __enter__(self): self.conn: sqlite3.Connection = None
self.conn = sqlite3.connect(self.file_path) 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() self.c = self.conn.cursor()
return self.c return self.c
def __exit__(self, exc_type, exc_val, exc_tb) -> bool: def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
flag = True
if exc_type is not None: if exc_type is not None:
if self.conn: if issubclass(exc_type, ArcError):
flag = False
else:
self.conn.rollback() self.conn.rollback()
current_app.logger.error( self.logger.error(
traceback.format_exception(exc_type, exc_val, exc_tb)) traceback.format_exception(exc_type, exc_val, exc_tb))
if self.conn:
self.conn.commit() self.conn.commit()
self.conn.close() self.conn.close()
return True return flag
class Query: class Query:
'''查询参数类''' '''查询参数类'''
def __init__(self, query_able: list = None, quzzy_query_able: list = None, sort_able: list = None) -> None: def __init__(self, query_able: list = None, fuzzy_query_able: list = None, sort_able: list = None) -> None:
self.query_able: list = query_able self.query_able: list = query_able # None表示不限制
self.quzzy_query_able: list = quzzy_query_able self.fuzzy_query_able: list = fuzzy_query_able # None表示不限制
self.sort_able: list = sort_able self.sort_able: list = sort_able
self.__limit: int = -1 self.__limit: int = -1
self.__offset: int = 0 self.__offset: int = 0
self.__query: dict = {} # {'name': 'admin'}
# {'name': 'admin'} or {'name': ['admin', 'user']}
self.__query: dict = {}
self.__fuzzy_query: dict = {} # {'name': 'dmi'} self.__fuzzy_query: dict = {} # {'name': 'dmi'}
# [{'column': 'user_id', 'order': 'ASC'}, ...] # [{'column': 'user_id', 'order': 'ASC'}, ...]
self.__sort: list = [] self.__sort: list = []
@@ -104,7 +121,7 @@ class Query:
def fuzzy_query_append(self, fuzzy_query: dict) -> None: def fuzzy_query_append(self, fuzzy_query: dict) -> None:
if not isinstance(fuzzy_query, dict): if not isinstance(fuzzy_query, dict):
raise InputError(api_error_code=-101) raise InputError(api_error_code=-101)
if self.quzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.quzzy_query_able)): if self.fuzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.fuzzy_query_able)):
raise InputError(api_error_code=-102) raise InputError(api_error_code=-102)
if not self.__fuzzy_query: if not self.__fuzzy_query:
self.__fuzzy_query = fuzzy_query self.__fuzzy_query = fuzzy_query
@@ -132,18 +149,22 @@ class Query:
raise InputError(api_error_code=-104) raise InputError(api_error_code=-104)
self.__sort = sort self.__sort = sort
def set_value(self, limit=-1, offset=0, query={}, fuzzy_query={}, sort=[]) -> None: def set_value(self, limit=-1, offset=0, query=None, fuzzy_query=None, sort=None) -> None:
self.limit = limit self.limit = limit
self.offset = offset self.offset = offset
self.query = query self.query = query if query is not None else {}
self.fuzzy_query = fuzzy_query self.fuzzy_query = fuzzy_query if fuzzy_query is not None else {}
self.sort = sort self.sort = sort if sort is not None else []
def from_data(self, d: dict) -> 'Query': def from_dict(self, d: dict) -> 'Query':
self.set_value(d.get('limit', -1), d.get('offset', 0), self.set_value(d.get('limit', -1), d.get('offset', 0),
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', [])) d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
return self return self
def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = None, fuzzy_query: dict = None) -> 'Query':
self.set_value(limit, offset, query, fuzzy_query, sort)
return self
class Sql: class Sql:
''' '''
@@ -153,63 +174,277 @@ class Sql:
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
def select(self, table_name: str, target_column: 'list' = [], query: 'Query' = None) -> list: @staticmethod
'''单表内行查询单句sql语句返回fetchall数据''' def get_select_sql(table_name: str, target_column: list = None, query: 'Query' = None):
'''拼接单表内行查询单句sql语句返回语句和参数列表'''
sql = 'select '
sql_list = [] sql_list = []
if len(target_column) >= 2: if not target_column:
sql += target_column[0] sql = f'select * from {table_name}'
for i in range(1, len(target_column)):
sql += ',' + target_column[i]
sql += ' from ' + table_name
elif len(target_column) == 1:
sql += target_column[0] + ' from ' + table_name
else: else:
sql += '* from ' + table_name sql = f"select {', '.join(target_column)} from {table_name}"
if query is None: if query is None:
self.c.execute(sql) return sql, sql_list
return self.c.fetchall()
where_key = [] where_key = []
where_like_key = [] for k, v in query.query.items():
for i in query.query: if isinstance(v, list):
where_key.append(i) where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
sql_list.append(query.query[i]) sql_list.extend(v)
for i in query.fuzzy_query:
where_like_key.append(i)
sql_list.append('%' + query.fuzzy_query[i] + '%')
if where_key or where_like_key:
sql += ' where '
if where_key:
sql += where_key[0] + '=?'
if len(where_key) >= 2:
for i in range(1, len(where_key)):
sql += ' and ' + where_key[i] + '=?'
if where_like_key:
for i in where_like_key:
sql += ' and ' + i + ' like ?'
else: else:
sql += where_like_key[0] + ' like ?' where_key.append(f'{k}=?')
sql_list.append(v)
if len(where_like_key) >= 2: for k, v in query.fuzzy_query.items():
for i in range(1, len(where_key)): where_key.append(f'{k} like ?')
sql += ' and ' + where_key[i] + ' like ?' sql_list.append(f'%{v}%')
if where_key:
sql += ' where '
sql += ' and '.join(where_key)
if query.sort: if query.sort:
sql += ' order by ' + \ sql += ' order by ' + \
query.sort[0]['column'] + ' ' + query.sort[0]['order'] ', '.join([x['column'] + ' ' + x['order'] for x in query.sort])
for i in range(1, len(query.sort)):
sql += ', ' + query.sort[i]['column'] + \
' ' + query.sort[i]['order']
if query.limit >= 0: if query.limit >= 0:
sql += ' limit ? offset ?' sql += ' limit ? offset ?'
sql_list.append(query.limit) sql_list.append(query.limit)
sql_list.append(query.offset) sql_list.append(query.offset)
return sql, sql_list
@staticmethod
def get_insert_sql(table_name: str, key: list = None, value_len: int = None, insert_type: str = None) -> str:
'''拼接insert语句请注意只返回sql语句insert_type为replace或ignore'''
if key is None:
key = []
insert_type = 'replace' if insert_type in [
'replace', 'R', 'r', 'REPLACE'] else 'ignore'
return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')'
@staticmethod
def get_update_sql(table_name: str, d: dict = None, query: 'Query' = None):
if not d:
return None
sql_list = []
sql = f"update {table_name} set {','.join([f'{k}=?' for k in d.keys()])}"
sql_list.extend(d.values())
if query is None:
return sql, sql_list
where_key = []
for k, v in query.query.items():
if isinstance(v, list):
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
sql_list.extend(v)
else:
where_key.append(f'{k}=?')
sql_list.append(v)
for k, v in query.fuzzy_query.items():
where_key.append(f'{k} like ?')
sql_list.append(f'%{v}%')
if where_key:
sql += ' where '
sql += ' and '.join(where_key)
return sql, sql_list
@staticmethod
def get_update_many_sql(table_name: str, key: list = None, where_key: list = None) -> str:
'''拼接update语句这里不用Query类也不用字典请注意只返回sql语句'''
if not key or not where_key:
return None
return f"update {table_name} set {','.join([f'{k}=?' for k in key])} where {' and '.join([f'{k}=?' for k in where_key])}"
@staticmethod
def get_delete_sql(table_name: str, query: 'Query' = None):
'''拼接删除语句query中只有query和fuzzy_query会被处理'''
sql = f'delete from {table_name}'
if query is None:
return sql, []
sql_list = []
where_key = []
for k, v in query.query.items():
if isinstance(v, list):
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
sql_list.extend(v)
else:
where_key.append(f'{k}=?')
sql_list.append(v)
for k, v in query.fuzzy_query.items():
where_key.append(f'{k} like ?')
sql_list.append(f'%{v}%')
if where_key:
sql += ' where '
sql += ' and '.join(where_key)
return sql, sql_list
def select(self, table_name: str, target_column: list = None, query: 'Query' = None) -> list:
'''单表内行select单句sql语句返回fetchall数据'''
sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute(sql, sql_list) self.c.execute(sql, sql_list)
return self.c.fetchall() return self.c.fetchall()
def select_exists(self, table_name: str, target_column: list = None, query: 'Query' = None) -> bool:
'''单表内行select exists单句sql语句返回bool值'''
sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute('select exists(' + sql + ')', sql_list)
return self.c.fetchone() == (1,)
def insert(self, table_name: str, key: list, value: tuple, insert_type: str = None) -> None:
'''单行插入或覆盖插入key传[]则为全部列insert_type为replace或ignore'''
self.c.execute(self.get_insert_sql(
table_name, key, len(value), insert_type), value)
def insert_many(self, table_name: str, key: list, value_list: list, insert_type: str = None) -> None:
'''多行插入或覆盖插入key传[]则为全部列insert_type为replace或ignore'''
if not value_list:
return
self.c.executemany(self.get_insert_sql(
table_name, key, len(value_list[0]), insert_type), value_list)
def update(self, table_name: str, d: dict, query: 'Query' = None) -> None:
'''单表内行update单句sql语句'''
if not d:
return
sql, sql_list = self.get_update_sql(table_name, d, query)
self.c.execute(sql, sql_list)
def update_many(self, table_name: str, key: list, value_list: list, where_key: list, where_value_list: list) -> None:
'''单表内行update多句sql语句这里不用Query类也不用字典要求值list长度一致有点像insert_many'''
if not key or not value_list or not where_key or not where_value_list or not len(key) == len(value_list[0]) or not len(where_key) == len(where_value_list[0]) or not len(value_list) == len(where_value_list):
raise ValueError
self.c.executemany(self.get_update_many_sql(
table_name, key, where_key), [x + y for x, y in zip(value_list, where_value_list)])
def delete(self, table_name: str, query: 'Query' = None) -> None:
'''删除query中只有query和fuzzy_query会被处理'''
sql, sql_list = self.get_delete_sql(table_name, query)
self.c.execute(sql, sql_list)
def get_table_info(self, table_name: str):
'''得到表结构,返回主键列表和字段名列表'''
pk = []
name = []
self.c.execute(f'''pragma table_info ("{table_name}")''') # 这里无法参数化
x = self.c.fetchall()
if x:
for i in x:
name.append(i[1])
if i[5] != 0:
pk.append(i[1])
return pk, name
class DatabaseMigrator:
def __init__(self, c1_path: str, c2_path: str) -> None:
self.c1_path = c1_path
self.c2_path = c2_path
@staticmethod
def update_one_table(c1, c2, table_name: str) -> bool:
'''从c1向c2更新数据表c1中存在的信息不变即c2中的冲突信息会被覆盖'''
c1.execute(
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
c2.execute(
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
if not c1.fetchone() or not c2.fetchone():
return False
sql1 = Sql(c1)
sql2 = Sql(c2)
db1_pk, db1_name = sql1.get_table_info(table_name)
db2_pk, db2_name = sql2.get_table_info(table_name)
if db1_pk != db2_pk:
return False
public_column = list(filter(lambda x: x in db2_name, db1_name))
sql2.insert_many(table_name, public_column, sql1.select(
table_name, public_column), insert_type='replace')
return True
@staticmethod
def update_user_char_full(c) -> None:
'''用character表数据更新user_char_full'''
c.execute('''select character_id, max_level, is_uncapped from character''')
x = c.fetchall()
c.execute('''select user_id from user''')
y = c.fetchall()
c.execute('''delete from user_char_full''')
for i in x:
exp = 25000 if i[1] == 30 else 10000
c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [
(j[0], i[0], i[1], exp, i[2], 0) for j in y])
def update_database(self) -> None:
'''
将c1数据库不存在数据加入或覆盖到c2数据库上
对于c2更新一些表并用character数据更新user_char_full
'''
with Connect(self.c2_path) as c2:
with Connect(self.c1_path) as c1:
for i in Constant.DATABASE_MIGRATE_TABLES:
self.update_one_table(c1, c2, i)
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
self.update_one_table(c1, c2, 'character')
self.update_user_char_full(c2) # 更新user_char_full
class LogDatabaseMigrator:
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
self.c1_path = c1_path
# self.c2_path = c2_path
self.init_folder_path = Config.DATABASE_INIT_PATH
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_update(self) -> None:
'''直接更新数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
def update_database(self) -> None:
with Connect(self.c1_path) as c:
self.c = c
self.table_update()
class MemoryDatabase:
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)
def __init__(self):
self.c = self.conn.cursor()
self.c.execute('''PRAGMA journal_mode = OFF''')
self.c.execute('''PRAGMA synchronous = 0''')
self.c.execute('''create table if not exists download_token(user_id int,
song_id text,file_name text,token text,time int,primary key(user_id, song_id, file_name));''')
self.c.execute(
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
self.conn.commit()
@register
def atexit():
MemoryDatabase.conn.close()

View File

@@ -13,7 +13,7 @@ class GameInfo:
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK, "stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
"core_exp": Constant.CORE_EXP, "core_exp": Constant.CORE_EXP,
"curr_ts": int(time()*1000), "curr_ts": int(time()*1000),
"level_steps": [{'level': i, 'level_exp': Constant.LEVEL_STEPS[i]} for i in Constant.LEVEL_STEPS], "level_steps": [{'level': k, 'level_exp': v} for k, v in Constant.LEVEL_STEPS.items()],
"world_ranking_enabled": True, "world_ranking_enabled": True,
"is_byd_chapter_unlocked": True "is_byd_chapter_unlocked": True
} }

View File

@@ -2,16 +2,17 @@ import base64
import hashlib import hashlib
import time import time
from os import urandom from os import urandom
from random import randint
from setting import Config
from .character import UserCharacter, UserCharacterList from .character import UserCharacter, UserCharacterList
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess, from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
NoData, UserBan) NoData, RateLimit, UserBan)
from .item import UserItemList from .item import UserItemList
from .limiter import ArcLimiter
from .score import Score from .score import Score
from .sql import Connect from .sql import Query, Sql
from .world import Map, UserMap, UserStamina from .world import Map, UserMap, UserStamina
@@ -32,17 +33,17 @@ def code_get_id(c, user_code: str) -> int:
class User: class User:
def __init__(self) -> None: def __init__(self) -> None:
self.name = None self.name: str = None
self.email = None self.email: str = None
self.password = None self.password: str = None
self.user_id = None self.user_id: int = None
self.user_code = None self.user_code: str = None
self.join_date = None self.join_date = None
self.rating_ptt: int = None # 100 times self.rating_ptt: int = None # 100 times
self.ticket = None self.ticket: int = None
self.world_rank_score = None self.world_rank_score: int = None
self.ban_flag = None self.ban_flag = None
@property @property
@@ -63,7 +64,7 @@ class UserRegister(User):
if self.c.fetchone() == (0,): if self.c.fetchone() == (0,):
self.name = name self.name = name
else: else:
raise DataExist('Username exists.', 101, -203) raise DataExist('Username exists.', 101, -210)
else: else:
raise InputError('Username is invalid.') raise InputError('Username is invalid.')
@@ -76,19 +77,30 @@ class UserRegister(User):
def set_email(self, email: str): def set_email(self, email: str):
# 邮箱格式懒得多判断 # 邮箱格式懒得多判断
if 4 <= len(email) <= 32 and '@' in email and '.' in email: if 4 <= len(email) <= 64 and '@' in email and '.' in email:
self.c.execute( self.c.execute(
'''select exists(select * from user where email = :email)''', {'email': email}) '''select exists(select * from user where email = :email)''', {'email': email})
if self.c.fetchone() == (0,): if self.c.fetchone() == (0,):
self.email = email self.email = email
else: else:
raise DataExist('Email address exists.', 102, -204) raise DataExist('Email address exists.', 102, -211)
else: else:
raise InputError('Email address is invalid.') raise InputError('Email address is invalid.')
def set_user_code(self, user_code: str) -> None:
'''设置用户的user_code'''
if len(user_code) == 9 and user_code.isdigit():
self.c.execute(
'''select exists(select * from user where user_code = ?)''', (user_code, ))
if self.c.fetchone() == (0,):
self.user_code = user_code
else:
raise DataExist('User code exists.', 103, -212)
else:
raise InputError('User code is invalid.')
def _build_user_code(self): def _build_user_code(self):
# 生成9位的user_code用的自然是随机 # 生成9位的user_code用的自然是随机
from random import randint
random_times = 0 random_times = 0
while random_times <= 1000: while random_times <= 1000:
@@ -130,7 +142,9 @@ class UserRegister(User):
def register(self): def register(self):
now = int(time.time() * 1000) now = int(time.time() * 1000)
if self.user_code is None:
self._build_user_code() self._build_user_code()
if self.user_id is None:
self._build_user_id() self._build_user_id()
self._insert_user_char() self._insert_user_char()
@@ -144,6 +158,8 @@ class UserRegister(User):
class UserLogin(User): class UserLogin(User):
# 密码和token的加密方式为 SHA-256 # 密码和token的加密方式为 SHA-256
limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login')
def __init__(self, c) -> None: def __init__(self, c) -> None:
super().__init__() super().__init__()
self.c = c self.c = c
@@ -220,30 +236,35 @@ class UserLogin(User):
if ip: if ip:
self.set_ip(ip) self.set_ip(ip)
if not self.limiter.hit(name):
raise RateLimit(
f'Too many login attempts of username `{name}`', 123, -203)
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', { self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': self.name}) 'name': self.name})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('Username does not exist.', 104) raise NoData(f'Username `{self.name}` does not exist.', 104)
self.user_id = x[0]
self.now = int(time.time() * 1000) self.now = int(time.time() * 1000)
if x[2] is not None and x[2] != '': if x[2] is not None and x[2] != '':
# 自动封号检查 # 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1]) ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > self.now: if ban_timestamp > self.now:
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={ raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
'remaining_ts': ban_timestamp-self.now}) 'remaining_ts': ban_timestamp-self.now})
if x[1] == '': if x[1] == '':
# 账号封禁 # 账号封禁
raise UserBan('The account has been banned.', 106) raise UserBan(
f'The account `{self.user_id}` has been banned.', 106)
if x[1] != self.hash_pwd: if x[1] != self.hash_pwd:
raise NoAccess('Wrong password.', 104) raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
self.user_id = str(x[0])
self.token = base64.b64encode(hashlib.sha256( self.token = base64.b64encode(hashlib.sha256(
(self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode() (str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
self.c.execute( self.c.execute(
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id}) '''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
@@ -276,7 +297,7 @@ class UserAuth(User):
class UserInfo(User): class UserInfo(User):
def __init__(self, c, user_id=None) -> None: def __init__(self, c, user_id=None) -> None:
super().__init__() User.__init__(self)
self.c = c self.c = c
self.user_id = user_id self.user_id = user_id
self.character = None self.character = None
@@ -285,9 +306,12 @@ class UserInfo(User):
self.recent_score = Score() self.recent_score = Score()
self.favorite_character = None self.favorite_character = None
self.max_stamina_notification_enabled = False self.max_stamina_notification_enabled = False
self.prog_boost = 0 self.prog_boost: int = 0
self.beyond_boost_gauge: float = 0
self.next_fragstam_ts: int = None self.next_fragstam_ts: int = None
self.world_mode_locked_end_ts: int = None self.world_mode_locked_end_ts: int = None
self.current_map: 'Map' = None
self.stamina: 'UserStamina' = None
self.__cores: list = None self.__cores: list = None
self.__packs: list = None self.__packs: list = None
@@ -370,20 +394,22 @@ class UserInfo(User):
self.favorite_character.select_character_uncap_condition(self) self.favorite_character.select_character_uncap_condition(self)
return self.favorite_character return self.favorite_character
@property
def friend_ids(self) -> list:
self.c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
'user_id': self.user_id})
return self.c.fetchall()
@property @property
def friends(self) -> list: def friends(self) -> list:
# 得到用户的朋友列表 # 得到用户的朋友列表
if self.__friends is None: if self.__friends is None:
self.c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
'user_id': self.user_id})
x = self.c.fetchall()
s = [] s = []
if x != [] and x[0][0] is not None: for i in self.friend_ids:
for i in x:
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''', self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
{'x': i[0], 'y': self.user_id}) {'x': i[0], 'y': self.user_id})
is_mutual = True if self.c.fetchone() == (1,) else False is_mutual = self.c.fetchone() == (1,)
you = UserOnline(self.c, i[0]) you = UserOnline(self.c, i[0])
you.select_user() you.select_user()
@@ -450,6 +476,10 @@ class UserInfo(User):
favorite_character_id = self.favorite_character.character_id favorite_character_id = self.favorite_character.character_id
else: else:
favorite_character_id = -1 favorite_character_id = -1
if self.character.character_id not in character_list:
self.character.character_id = 0
return { return {
"is_aprilfools": Config.IS_APRILFOOLS, "is_aprilfools": Config.IS_APRILFOOLS,
"curr_available_maps": self.curr_available_maps_list, "curr_available_maps": self.curr_available_maps_list,
@@ -470,6 +500,7 @@ class UserInfo(User):
"is_skill_sealed": self.is_skill_sealed, "is_skill_sealed": self.is_skill_sealed,
"current_map": self.current_map.map_id, "current_map": self.current_map.map_id,
"prog_boost": self.prog_boost, "prog_boost": self.prog_boost,
"beyond_boost_gauge": self.beyond_boost_gauge,
"next_fragstam_ts": self.next_fragstam_ts, "next_fragstam_ts": self.next_fragstam_ts,
"max_stamina_ts": self.stamina.max_stamina_ts, "max_stamina_ts": self.stamina.max_stamina_ts,
"stamina": self.stamina.stamina, "stamina": self.stamina.stamina,
@@ -477,7 +508,7 @@ class UserInfo(User):
"world_songs": self.world_songs, "world_songs": self.world_songs,
"singles": self.singles, "singles": self.singles,
"packs": self.packs, "packs": self.packs,
"characters": self.characters_list, "characters": character_list,
"cores": self.cores, "cores": self.cores,
"recent_score": self.recent_score_list, "recent_score": self.recent_score_list,
"max_friend": Constant.MAX_FRIEND_COUNT, "max_friend": Constant.MAX_FRIEND_COUNT,
@@ -527,6 +558,7 @@ class UserInfo(User):
self.stamina = UserStamina(self.c, self) self.stamina = UserStamina(self.c, self)
self.stamina.set_value(x[32], x[33]) self.stamina.set_value(x[32], x[33])
self.world_mode_locked_end_ts = x[34] if x[34] else -1 self.world_mode_locked_end_ts = x[34] if x[34] else -1
self.beyond_boost_gauge = x[35] if x[35] else 0
return self return self
@@ -557,16 +589,8 @@ class UserInfo(User):
self.stamina = UserStamina(self.c, self) self.stamina = UserStamina(self.c, self)
self.stamina.set_value(x[0], x[1]) self.stamina.set_value(x[0], x[1])
def select_user_about_character(self) -> None: def from_list_about_character(self, x: list) -> None:
''' '''从数据库user表获取搭档信息'''
查询user表有关角色的信息
'''
self.c.execute('''select name, character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, favorite_character from user where user_id = :a''', {
'a': self.user_id})
x = self.c.fetchone()
if not x:
raise NoData('No user.', 108, -3)
self.name = x[0] self.name = x[0]
self.character = UserCharacter(self.c, x[1], self) self.character = UserCharacter(self.c, x[1], self)
self.is_skill_sealed = x[2] == 1 self.is_skill_sealed = x[2] == 1
@@ -575,12 +599,24 @@ class UserInfo(User):
self.favorite_character = None if x[5] == - \ self.favorite_character = None if x[5] == - \
1 else UserCharacter(self.c, x[5], self) 1 else UserCharacter(self.c, x[5], self)
def select_user_about_character(self) -> None:
'''
查询user表有关搭档的信息
'''
self.c.execute('''select name, character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, favorite_character from user where user_id = :a''', {
'a': self.user_id})
x = self.c.fetchone()
if not x:
raise NoData('No user.', 108, -3)
self.from_list_about_character(x)
def select_user_about_world_play(self) -> None: def select_user_about_world_play(self) -> None:
''' '''
查询user表有关世界模式打歌的信息 查询user表有关世界模式打歌的信息
''' '''
self.c.execute( self.c.execute(
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts from user where user_id=?''', (self.user_id,)) '''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge from user where user_id=?''', (self.user_id,))
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -593,6 +629,7 @@ class UserInfo(User):
self.character.is_uncapped_override = x[5] == 1 self.character.is_uncapped_override = x[5] == 1
self.current_map = UserMap(self.c, x[6], self) self.current_map = UserMap(self.c, x[6], self)
self.world_mode_locked_end_ts = x[7] if x[7] else -1 self.world_mode_locked_end_ts = x[7] if x[7] else -1
self.beyond_boost_gauge = x[8] if x[8] else 0
@property @property
def global_rank(self) -> int: def global_rank(self) -> int:
@@ -612,10 +649,10 @@ class UserInfo(User):
def update_global_rank(self) -> None: def update_global_rank(self) -> None:
'''用户世界排名计算,有新增成绩则要更新''' '''用户世界排名计算,有新增成绩则要更新'''
with Connect() as c2:
c2.execute('''select song_id, rating_ftr, rating_byn from chart''') self.c.execute('''select song_id, rating_ftr, rating_byn from chart''')
x = c2.fetchall() x = self.c.fetchall()
if x:
song_list_ftr = [self.user_id] song_list_ftr = [self.user_id]
song_list_byn = [self.user_id] song_list_byn = [self.user_id]
for i in x: for i in x:
@@ -624,25 +661,22 @@ class UserInfo(User):
if i[2] > 0: if i[2] > 0:
song_list_byn.append(i[0]) song_list_byn.append(i[0])
if len(song_list_ftr) >= 2:
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({0})'''.format(
','.join(['?']*(len(song_list_ftr)-1))), tuple(song_list_ftr))
x = self.c.fetchone()
if x[0] is not None:
score_sum = x[0]
else:
score_sum = 0 score_sum = 0
if len(song_list_ftr) >= 2:
if len(song_list_byn) >= 2: self.c.execute(
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({0})'''.format( f'''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({','.join(['?']*(len(song_list_ftr)-1))})''', tuple(song_list_ftr))
','.join(['?']*(len(song_list_byn)-1))), tuple(song_list_byn))
x = self.c.fetchone()
if x[0] is not None:
score_sum += x[0]
if len(song_list_byn) >= 2:
self.c.execute(
f'''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({','.join(['?']*(len(song_list_byn)-1))})''', tuple(song_list_byn))
x = self.c.fetchone() x = self.c.fetchone()
if x[0] is not None: if x[0] is not None:
score_sum += x[0] score_sum += x[0]
else:
score_sum += 0
self.c.execute('''update user set world_rank_score = :b where user_id = :a''', { self.c.execute('''update user set world_rank_score = :b where user_id = :a''', {
'a': self.user_id, 'b': score_sum}) 'a': self.user_id, 'b': score_sum})
@@ -651,13 +685,13 @@ class UserInfo(User):
def select_user_one_column(self, column_name: str, default_value=None) -> None: def select_user_one_column(self, column_name: str, default_value=None) -> None:
''' '''
查询user表的某个属性\ 查询user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
self.c.execute('''select %s from user where user_id = :a''' % self.c.execute(f'''select {column_name} from user where user_id = :a''', {
column_name, {'a': self.user_id}) 'a': self.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -666,15 +700,15 @@ class UserInfo(User):
def update_user_one_column(self, column_name: str, value=None) -> None: def update_user_one_column(self, column_name: str, value=None) -> None:
''' '''
更新user表的某个属性\ 更新user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
if value is not None: if value is not None:
self.__dict__[column_name] = value self.__dict__[column_name] = value
self.c.execute('''update user set %s = :a where user_id = :b''' % self.c.execute(f'''update user set {column_name} = :a where user_id = :b''', {
column_name, {'a': self.__dict__[column_name], 'b': self.user_id}) 'a': self.__dict__[column_name], 'b': self.user_id})
class UserOnline(UserInfo): class UserOnline(UserInfo):
@@ -719,3 +753,21 @@ class UserOnline(UserInfo):
self.favorite_character = UserCharacter(self.c, character_id, self) self.favorite_character = UserCharacter(self.c, character_id, self)
self.c.execute('''update user set favorite_character = :a where user_id = :b''', self.c.execute('''update user set favorite_character = :a where user_id = :b''',
{'a': self.favorite_character.character_id, 'b': self.user_id}) {'a': self.favorite_character.character_id, 'b': self.user_id})
class UserChanger(UserInfo, UserRegister):
def __init__(self, c, user_id=None) -> None:
super().__init__(c, user_id)
def update_columns(self, columns: list = None, d: dict = None) -> None:
if columns is not None:
d = {}
for column in columns:
if column == 'password' and self.password != '':
d[column] = self.hash_pwd
else:
d[column] = self.__dict__[column]
Sql(self.c).update('user', d, Query().from_args(
{'user_id': self.user_id}))

View File

@@ -1,8 +1,10 @@
import hashlib import hashlib
import os import os
from datetime import date
from time import mktime
def md5(code): def md5(code: str) -> str:
# md5加密算法 # md5加密算法
code = code.encode() code = code.encode()
md5s = hashlib.md5() md5s = hashlib.md5()
@@ -12,16 +14,33 @@ def md5(code):
return codes return codes
def get_file_md5(file_path): def get_file_md5(file_path: str) -> str:
# 计算文件MD5 '''计算文件MD5假设是文件'''
if not os.path.isfile(file_path):
return None
myhash = hashlib.md5() myhash = hashlib.md5()
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
while True: while True:
b = f.read(8096) b = f.read(8192)
if not b: if not b:
break break
myhash.update(b) myhash.update(b)
return myhash.hexdigest() return myhash.hexdigest()
def try_rename(path: str, new_path: str) -> str:
'''尝试重命名文件,并尝试避免命名冲突(在后面加自增数字),返回最终路径'''
final_path = new_path
if os.path.exists(new_path):
i = 1
while os.path.exists(new_path + str(i)):
i += 1
final_path = new_path + str(i)
os.rename(path, final_path)
return final_path
def get_today_timestamp():
'''相对于本机本地时间的今天0点的时间戳'''
return int(mktime(date.today().timetuple()))

View File

@@ -1,6 +1,6 @@
import json
import os import os
from functools import lru_cache from functools import lru_cache
from json import load
from random import random from random import random
from time import time from time import time
@@ -25,15 +25,15 @@ def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list:
def get_world_info(map_id: str) -> dict: def get_world_info(map_id: str) -> dict:
'''读取json文件内容返回字典''' '''读取json文件内容返回字典'''
world_info = {} world_info = {}
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, map_id+'.json'), 'r') as f: with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, f'{map_id}.json'), 'rb') as f:
world_info = json.load(f) world_info = load(f)
return world_info return world_info
def get_world_all(c, user) -> list: def get_world_all(c, user) -> list:
''' '''
读取所有地图信息,返回列表\ 读取所有地图信息,返回列表
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
worlds = get_world_name() worlds = get_world_name()
@@ -44,12 +44,13 @@ class Step:
'''台阶类''' '''台阶类'''
def __init__(self) -> None: def __init__(self) -> None:
self.postion: int = None self.position: int = None
self.capture: int = None self.capture: int = None
self.items: list = [] self.items: list = []
self.restrict_id: str = None self.restrict_id: str = None
self.restrict_ids: list = [] self.restrict_ids: list = []
self.restrict_type: str = None self.restrict_type: str = None
self.restrict_difficulty: int = None
self.step_type: list = None self.step_type: list = None
self.speed_limit_value: int = None self.speed_limit_value: int = None
self.plus_stamina_value: int = None self.plus_stamina_value: int = None
@@ -61,12 +62,14 @@ class Step:
} }
if self.items: if self.items:
r['items'] = [i.to_dict() for i in self.items] r['items'] = [i.to_dict() for i in self.items]
if self.restrict_type:
r['restrict_type'] = self.restrict_type
if self.restrict_id: if self.restrict_id:
r['restrict_id'] = self.restrict_id r['restrict_id'] = self.restrict_id
if self.restrict_ids: if self.restrict_ids:
r['restrict_ids'] = self.restrict_ids r['restrict_ids'] = self.restrict_ids
if self.restrict_type: if self.restrict_difficulty is not None:
r['restrict_type'] = self.restrict_type r['restrict_difficulty'] = self.restrict_difficulty
if self.step_type: if self.step_type:
r['step_type'] = self.step_type r['step_type'] = self.step_type
if self.speed_limit_value: if self.speed_limit_value:
@@ -82,6 +85,7 @@ class Step:
self.restrict_id = d.get('restrict_id') self.restrict_id = d.get('restrict_id')
self.restrict_ids = d.get('restrict_ids') self.restrict_ids = d.get('restrict_ids')
self.restrict_type = d.get('restrict_type') self.restrict_type = d.get('restrict_type')
self.restrict_difficulty = d.get('restrict_difficulty')
self.step_type = d.get('step_type') self.step_type = d.get('step_type')
self.speed_limit_value = d.get('speed_limit_value') self.speed_limit_value = d.get('speed_limit_value')
self.plus_stamina_value = d.get('plus_stamina_value') self.plus_stamina_value = d.get('plus_stamina_value')
@@ -102,7 +106,7 @@ class Map:
self.available_from: int = None self.available_from: int = None
self.available_to: int = None self.available_to: int = None
self.is_repeatable: bool = None self.is_repeatable: bool = None
self.require_id: str = None self.require_id: 'str | list[str]' = None
self.require_type: str = None self.require_type: str = None
self.require_value: int = None self.require_value: int = None
self.coordinate: str = None self.coordinate: str = None
@@ -111,6 +115,10 @@ class Map:
self.steps: list = [] self.steps: list = []
self.__rewards: list = None self.__rewards: list = None
self.require_localunlock_songid: str = None
self.require_localunlock_challengeid: str = None
self.chain_info: dict = None
@property @property
def rewards(self) -> list: def rewards(self) -> list:
if self.__rewards is None: if self.__rewards is None:
@@ -133,7 +141,7 @@ class Map:
def to_dict(self) -> dict: def to_dict(self) -> dict:
if self.chapter is None: if self.chapter is None:
self.select_map_info() self.select_map_info()
return { r = {
'map_id': self.map_id, 'map_id': self.map_id,
'is_legacy': self.is_legacy, 'is_legacy': self.is_legacy,
'is_beyond': self.is_beyond, 'is_beyond': self.is_beyond,
@@ -151,25 +159,35 @@ class Map:
'custom_bg': self.custom_bg, 'custom_bg': self.custom_bg,
'stamina_cost': self.stamina_cost, 'stamina_cost': self.stamina_cost,
'step_count': self.step_count, 'step_count': self.step_count,
'require_localunlock_songid': self.require_localunlock_songid,
'require_localunlock_challengeid': self.require_localunlock_challengeid,
'steps': [s.to_dict() for s in self.steps], 'steps': [s.to_dict() for s in self.steps],
} }
if self.chain_info is not None:
r['chain_info'] = self.chain_info
return r
def from_dict(self, raw_dict: dict) -> 'Map': def from_dict(self, raw_dict: dict) -> 'Map':
self.is_legacy = raw_dict.get('is_legacy') self.is_legacy = raw_dict.get('is_legacy')
self.is_beyond = raw_dict.get('is_beyond') self.is_beyond = raw_dict.get('is_beyond')
self.beyond_health = raw_dict.get('beyond_health') self.beyond_health = raw_dict.get('beyond_health')
self.character_affinity = raw_dict.get('character_affinity') self.character_affinity = raw_dict.get('character_affinity', [])
self.affinity_multiplier = raw_dict.get('affinity_multiplier') self.affinity_multiplier = raw_dict.get('affinity_multiplier', [])
self.chapter = raw_dict.get('chapter') self.chapter = raw_dict.get('chapter')
self.available_from = raw_dict.get('available_from') self.available_from = raw_dict.get('available_from', -1)
self.available_to = raw_dict.get('available_to') self.available_to = raw_dict.get('available_to', 9999999999999)
self.is_repeatable = raw_dict.get('is_repeatable') self.is_repeatable = raw_dict.get('is_repeatable')
self.require_id = raw_dict.get('require_id') self.require_id = raw_dict.get('require_id', '')
self.require_type = raw_dict.get('require_type') self.require_type = raw_dict.get('require_type', '')
self.require_value = raw_dict.get('require_value') self.require_value = raw_dict.get('require_value', 1)
self.coordinate = raw_dict.get('coordinate') self.coordinate = raw_dict.get('coordinate')
self.custom_bg = raw_dict.get('custom_bg') self.custom_bg = raw_dict.get('custom_bg', '')
self.stamina_cost = raw_dict.get('stamina_cost') self.stamina_cost = raw_dict.get('stamina_cost')
self.require_localunlock_songid = raw_dict.get(
'require_localunlock_songid', '')
self.require_localunlock_challengeid = raw_dict.get(
'require_localunlock_challengeid', '')
self.chain_info = raw_dict.get('chain_info')
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')] self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
return self return self
@@ -180,7 +198,7 @@ class Map:
class UserMap(Map): class UserMap(Map):
''' '''
用户地图类\ 用户地图类
parameters: `user` - `User`类或者子类的实例 parameters: `user` - `User`类或者子类的实例
''' '''
@@ -199,7 +217,7 @@ class UserMap(Map):
@property @property
def rewards_for_climbing(self) -> list: def rewards_for_climbing(self) -> list:
rewards = [] rewards = []
for i in range(self.prev_position, self.curr_position+1): for i in range(self.prev_position+1, self.curr_position+1):
step = self.steps[i] step = self.steps[i]
if step.items: if step.items:
rewards.append( rewards.append(
@@ -209,7 +227,7 @@ class UserMap(Map):
def rewards_for_climbing_to_dict(self) -> list: def rewards_for_climbing_to_dict(self) -> list:
rewards = [] rewards = []
for i in range(self.prev_position, self.curr_position+1): for i in range(self.prev_position+1, self.curr_position+1):
step = self.steps[i] step = self.steps[i]
if step.items: if step.items:
rewards.append( rewards.append(
@@ -290,7 +308,7 @@ class UserMap(Map):
if self.require_type in ['pack', 'single']: if self.require_type in ['pack', 'single']:
item = ItemFactory(self.c).get_item(self.require_type) item = ItemFactory(self.c).get_item(self.require_type)
item.item_id = self.require_id item.item_id = self.require_id
item.select(self.user) item.select_user_item(self.user)
if not item.amount: if not item.amount:
self.is_locked = True self.is_locked = True
@@ -395,7 +413,8 @@ class Stamina:
class UserStamina(Stamina): class UserStamina(Stamina):
''' '''
用户体力类\ 用户体力类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -421,8 +440,9 @@ class UserStamina(Stamina):
class WorldPlay: class WorldPlay:
''' '''
世界模式打歌类处理特殊角色技能联动UserMap和UserPlay\ 世界模式打歌类处理特殊角色技能联动UserMap和UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''
@@ -436,8 +456,9 @@ class WorldPlay:
self.step_value: float = None self.step_value: float = None
self.prog_tempest: float = None self.prog_tempest: float = None
self.overdrive_extra: float = None
self.character_bonus_progress: float = None self.character_bonus_progress: float = None
self.prog_skill_increase: float = None
self.over_skill_increase: float = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
arcmap: 'UserMap' = self.user.current_map arcmap: 'UserMap' = self.user.current_map
@@ -465,19 +486,22 @@ class WorldPlay:
}, },
"current_stamina": self.user.stamina.stamina, "current_stamina": self.user.stamina.stamina,
"max_stamina_ts": self.user.stamina.max_stamina_ts, "max_stamina_ts": self.user.stamina.max_stamina_ts,
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
'beyond_boost_gauge': self.user.beyond_boost_gauge
} }
if self.overdrive_extra is not None: if self.over_skill_increase is not None:
r['char_stats']['overdrive'] += self.overdrive_extra r['char_stats']['over_skill_increase'] = self.over_skill_increase
if self.prog_skill_increase is not None:
r['char_stats']['prog_skill_increase'] = self.prog_skill_increase
if self.prog_tempest is not None: if self.prog_tempest is not None:
r['char_stats']['prog'] += self.prog_tempest r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样
r['char_stats']['prog_tempest'] = self.prog_tempest r['char_stats']['prog_tempest'] = self.prog_tempest
if self.character_bonus_progress is not None: if self.character_bonus_progress is not None:
# 猜的,为了让客户端正确显示,当然结果是没问题的 # 猜的,为了让客户端正确显示,当然结果是没问题的
r['base_progress'] += self.character_bonus_progress # r['base_progress'] += self.character_bonus_progress # 肯定不是这样的
r['character_bonus_progress'] = self.character_bonus_progress r['character_bonus_progress'] = self.character_bonus_progress
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
@@ -492,16 +516,45 @@ class WorldPlay:
r['fragment_multiply'] = self.user_play.fragment_multiply r['fragment_multiply'] = self.user_play.fragment_multiply
if self.user_play.prog_boost_multiply != 0: if self.user_play.prog_boost_multiply != 0:
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
if self.user_play.beyond_boost_gauge_usage != 0:
r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage
return r return r
@property
def beyond_boost_gauge_addition(self) -> float:
# guessed by Lost-MSth
return 2.45 * self.user_play.rating ** 0.5 + 27
@property @property
def step_times(self) -> float: def step_times(self) -> float:
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * prog_boost_multiply / 100 * beyond_boost_times
@property @property
def exp_times(self) -> float: def exp_times(self) -> float:
return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * prog_boost_multiply / 100 * beyond_boost_times
def get_step(self) -> None: def get_step(self) -> None:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
@@ -510,6 +563,8 @@ class WorldPlay:
self.character_used.level) self.character_used.level)
if self.prog_tempest: if self.prog_tempest:
prog += self.prog_tempest prog += self.prog_tempest
if self.prog_skill_increase:
prog += self.prog_skill_increase
self.step_value = self.base_step_value * prog / 50 * self.step_times self.step_value = self.base_step_value * prog / 50 * self.step_times
else: else:
@@ -528,8 +583,8 @@ class WorldPlay:
overdrive = self.character_used.overdrive.get_value( overdrive = self.character_used.overdrive.get_value(
self.character_used.level) self.character_used.level)
if self.overdrive_extra: if self.over_skill_increase:
overdrive += self.overdrive_extra overdrive += self.over_skill_increase
self.step_value = self.base_step_value * overdrive / \ self.step_value = self.base_step_value * overdrive / \
50 * self.step_times * affinity_multiplier 50 * self.step_times * affinity_multiplier
@@ -542,6 +597,20 @@ class WorldPlay:
self.user_play.clear_play_state() self.user_play.clear_play_state()
self.user.select_user_about_world_play() self.user.select_user_about_world_play()
if self.user_play.beyond_gauge == 0:
# 更新byd大招蓄力条
self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition
self.user.beyond_boost_gauge = min(
self.user.beyond_boost_gauge, 200)
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge:
self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage
if abs(self.user.beyond_boost_gauge) <= 1e-5:
self.user.beyond_boost_gauge = 0
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
self.character_used = Character() self.character_used = Character()
self.user.character.select_character_info() self.user.character.select_character_info()
@@ -587,13 +656,19 @@ class WorldPlay:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed: if self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
self._special_tempest() self._special_tempest()
elif self.character_used.skill_id_displayed == 'ilith_awakened_skill':
self._ilith_awakened_skill()
elif self.character_used.skill_id_displayed == 'skill_mithra':
self._skill_mithra()
else: else:
if self.character_used.skill_id_displayed == 'skill_vita': if self.character_used.skill_id_displayed == 'skill_vita':
self._skill_vita() self._skill_vita()
if self.character_used.skill_id_displayed == 'skill_mika':
self._skill_mika()
def after_climb(self) -> None: def after_climb(self) -> None:
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap, factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis} 'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane}
if self.character_used.skill_id_displayed in factory_dict: if self.character_used.skill_id_displayed in factory_dict:
factory_dict[self.character_used.skill_id_displayed]() factory_dict[self.character_used.skill_id_displayed]()
@@ -613,12 +688,12 @@ class WorldPlay:
def _skill_vita(self) -> None: def _skill_vita(self) -> None:
''' '''
vita技能overdrive随回忆率提升提升量最多为10\ vita技能overdrive随回忆率提升提升量最多为10
此处采用线性函数 此处采用线性函数
''' '''
self.overdrive_extra = 0 self.over_skill_increase = 0
if 0 < self.user_play.health <= 100: if 0 < self.user_play.health <= 100:
self.overdrive_extra = self.user_play.health / 10 self.over_skill_increase = self.user_play.health / 10
def _eto_uncap(self) -> None: def _eto_uncap(self) -> None:
'''eto觉醒技能获得残片奖励时世界模式进度加7''' '''eto觉醒技能获得残片奖励时世界模式进度加7'''
@@ -666,3 +741,38 @@ class WorldPlay:
self.user.world_mode_locked_end_ts = int( self.user.world_mode_locked_end_ts = int(
time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME
self.user.update_user_one_column('world_mode_locked_end_ts') self.user.update_user_one_column('world_mode_locked_end_ts')
def _skill_amane(self) -> None:
'''
amane技能起始格为限速或随机成绩小于EX时世界模式进度减半
偷懒在after_climb里面需要重爬一次
'''
x: 'Step' = self.user.current_map.steps_for_climbing[0]
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
self.character_bonus_progress = -1 * self.step_value / 2 / self.step_times
self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value)
def _ilith_awakened_skill(self) -> None:
'''
ilith 觉醒技能,曲目通关时步数+6wiki 说是 prog 值+6
'''
if self.user_play.health > 0:
self.prog_skill_increase = 6
def _skill_mika(self) -> None:
'''
mika 技能,通关特定曲目能力值翻倍
'''
if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0:
self.over_skill_increase = self.character_used.overdrive.get_value(
self.character_used.level)
self.prog_skill_increase = self.character_used.prog.get_value(
self.character_used.level)
def _skill_mithra(self) -> None:
'''
mithra 技能,每 150 combo 增加世界模式进度+1直接理解成 prog 值增加
'''
if self.user_play.combo_interval_bonus:
self.prog_skill_increase = self.user_play.combo_interval_bonus

View File

@@ -1,600 +1,15 @@
import sqlite3 import os
import time import sys
import json from importlib import import_module
# 数据库初始化文件删掉arcaea_database.db文件后运行即可谨慎使用 # 数据库初始化文件删掉arcaea_database.db文件后运行即可谨慎使用
ARCAEA_SERVER_VERSION = 'v2.9.1'
def main(core_path: str = '../', db_path: str = './arcaea_database.db', init_folder_path: str = './init'):
def main(path='./'): sys.path.append(os.path.join(sys.path[0], core_path))
conn = sqlite3.connect(path+'arcaea_database.db') sys.path.append(os.path.join(sys.path[0], core_path, './core/'))
c = conn.cursor() init = import_module('init').DatabaseInit(db_path, init_folder_path)
c.execute('''create table if not exists config(id text primary key, init.init()
value text
);''')
c.execute('''insert into config values("version", :a);''',
{'a': ARCAEA_SERVER_VERSION})
c.execute('''create table if not exists user(user_id int primary key,
name text unique,
password text not null,
join_date char(20),
user_code char(10),
rating_ptt int,
character_id int,
is_skill_sealed int,
is_char_uncapped int,
is_char_uncapped_override int,
is_hide_rating int,
song_id text,
difficulty int,
score int,
shiny_perfect_count int,
perfect_count int,
near_count int,
miss_count int,
health int,
modifier int,
time_played int,
clear_type int,
rating real,
favorite_character int,
max_stamina_notification_enabled int,
current_map text,
ticket int,
prog_boost int,
email text,
world_rank_score int,
ban_flag text,
next_fragstam_ts int,
max_stamina_ts int,
stamina int,
world_mode_locked_end_ts int
);''')
c.execute('''create table if not exists login(access_token text,
user_id int,
login_time int,
login_ip text,
login_device text,
primary key(access_token, user_id)
);''')
c.execute('''create table if not exists friend(user_id_me int,
user_id_other int,
primary key (user_id_me, user_id_other)
);''')
c.execute('''create table if not exists best_score(user_id int,
song_id text,
difficulty int,
score int,
shiny_perfect_count int,
perfect_count int,
near_count int,
miss_count int,
health int,
modifier int,
time_played int,
best_clear_type int,
clear_type int,
rating real,
primary key(user_id, song_id, difficulty)
);''')
c.execute('''create table if not exists user_char(user_id int,
character_id int,
level int,
exp real,
is_uncapped int,
is_uncapped_override int,
primary key(user_id, character_id)
);''')
c.execute('''create table if not exists user_char_full(user_id int,
character_id int,
level int,
exp real,
is_uncapped int,
is_uncapped_override int,
primary key(user_id, character_id)
);''')
c.execute('''create table if not exists character(character_id int primary key,
name text,
max_level int,
frag1 real,
prog1 real,
overdrive1 real,
frag20 real,
prog20 real,
overdrive20 real,
frag30 real,
prog30 real,
overdrive30 real,
skill_id text,
skill_unlock_level int,
skill_requires_uncap int,
skill_id_uncap text,
char_type int,
is_uncapped int
);''')
c.execute('''create table if not exists char_item(character_id int,
item_id text,
type text,
amount int,
primary key(character_id, item_id, type)
);''')
c.execute('''create table if not exists recent30(user_id int primary key,
r0 real,
song_id0 text,
r1 real,
song_id1 text,
r2 real,
song_id2 text,
r3 real,
song_id3 text,
r4 real,
song_id4 text,
r5 real,
song_id5 text,
r6 real,
song_id6 text,
r7 real,
song_id7 text,
r8 real,
song_id8 text,
r9 real,
song_id9 text,
r10 real,
song_id10 text,
r11 real,
song_id11 text,
r12 real,
song_id12 text,
r13 real,
song_id13 text,
r14 real,
song_id14 text,
r15 real,
song_id15 text,
r16 real,
song_id16 text,
r17 real,
song_id17 text,
r18 real,
song_id18 text,
r19 real,
song_id19 text,
r20 real,
song_id20 text,
r21 real,
song_id21 text,
r22 real,
song_id22 text,
r23 real,
song_id23 text,
r24 real,
song_id24 text,
r25 real,
song_id25 text,
r26 real,
song_id26 text,
r27 real,
song_id27 text,
r28 real,
song_id28 text,
r29 real,
song_id29 text
);''')
c.execute('''create table if not exists user_world(user_id int,
map_id text,
curr_position int,
curr_capture real,
is_locked int,
primary key(user_id, map_id)
);''')
c.execute('''create table if not exists songplay_token(token text primary key,
user_id int,
song_id text,
difficulty int,
course_id text,
course_state int,
course_score int,
course_clear_type int,
stamina_multiply int,
fragment_multiply int,
prog_boost_multiply int
);''')
c.execute('''create table if not exists download_token(user_id int,
song_id text,
file_name text,
token text,
time int,
primary key(user_id, song_id, file_name)
);''')
c.execute('''create table if not exists user_download(user_id int,
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,
_id text,
primary key(item_id, type)
);''')
c.execute('''create table if not exists user_item(user_id int,
item_id text,
type text,
amount int,
primary key(user_id, item_id, type)
);''')
c.execute('''create table if not exists purchase(purchase_name text primary key,
price int,
orig_price int,
discount_from int,
discount_to int,
discount_reason text
);''')
c.execute('''create table if not exists purchase_item(purchase_name text,
item_id text,
type text,
amount int,
primary key(purchase_name, item_id, type)
);''')
c.execute('''create table if not exists user_save(user_id int primary key,
scores_data text,
clearlamps_data text,
clearedsongs_data text,
unlocklist_data text,
installid_data text,
devicemodelname_data text,
story_data text,
createdAt int,
finalestate_data text
);''')
c.execute('''create table if not exists present(present_id text primary key,
expire_ts int,
description text
);''')
c.execute('''create table if not exists user_present(user_id int,
present_id text,
primary key(user_id, present_id)
);''')
c.execute('''create table if not exists present_item(present_id text,
item_id text,
type text,
amount int,
primary key(present_id, item_id, type)
);''')
c.execute('''create table if not exists chart(song_id text primary key,
name text,
rating_pst int,
rating_prs int,
rating_ftr int,
rating_byn int
);''')
c.execute('''create table if not exists redeem(code text primary key,
type int
);''')
c.execute('''create table if not exists user_redeem(user_id int,
code text,
primary key(user_id, code)
);''')
c.execute('''create table if not exists redeem_item(code text,
item_id text,
type text,
amount int,
primary key(code, item_id, type)
);''')
c.execute('''create table if not exists role(role_id int primary key,
role_name text,
caption text
);''')
c.execute('''create table if not exists user_role(user_id int,
role_id int,
primary key(user_id, role_id)
);''')
c.execute('''create table if not exists power(power_id int primary key,
power_name text,
caption text
);''')
c.execute('''create table if not exists role_power(role_id int,
power_id int,
primary key(role_id, power_id)
);''')
c.execute('''create table if not exists api_login(user_id int,
token text,
login_time int,
login_ip text,
primary key(user_id, token)
);''')
c.execute('''create table if not exists course(course_id text primary key,
course_name text,
dan_name text,
style int,
gauge_requirement text,
flag_as_hidden_when_requirements_not_met int,
can_start int
);''')
c.execute('''create table if not exists user_course(user_id int,
course_id text,
high_score int,
best_clear_type int,
primary key(user_id, course_id)
);''')
c.execute('''create table if not exists course_chart(course_id text,
song_id text,
difficulty int,
flag_as_hidden int,
song_index int,
primary key(course_id, song_id, difficulty)
);''')
c.execute('''create table if not exists course_requirement(course_id text,
required_id text,
primary key(course_id, required_id)
);''')
c.execute('''create table if not exists course_item(course_id text,
item_id text,
type text,
amount int,
primary key(course_id, item_id, type)
);''')
# 搭档初始化
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)']
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']
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0]
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]
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]
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]
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]
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]
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]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200]
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]
char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
1: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_desolate', 'amount': 25}],
2: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_crimson', 'amount': 25}],
4: [{'core_id': 'core_ambivalent', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
13: [{'core_id': 'core_scarlet', 'amount': 30}],
21: [{'core_id': 'core_scarlet', 'amount': 30}],
26: [{'core_id': 'core_chunithm', 'amount': 15}],
27: [{'core_id': 'core_chunithm', 'amount': 15}],
28: [{'core_id': 'core_chunithm', 'amount': 15}],
29: [{'core_id': 'core_chunithm', 'amount': 15}],
36: [{'core_id': 'core_chunithm', 'amount': 15}],
42: [{'core_id': 'core_chunithm', 'amount': 15}],
43: [{'core_id': 'core_chunithm', 'amount': 15}],
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}]
}
for i in range(0, 56):
skill_requires_uncap = 1 if i == 2 else 0
if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43, 11, 12, 19, 5]:
sql = '''insert into character values(?,?,30,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1)'''
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
else:
sql = '''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)'''
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
c.execute('''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)''', (99,
'shirahime', 38, 33, 28, 66, 58, 50, 66, 58, 50, 'frags_preferred_song', 0, 0, '', 0))
for i in char_core:
for j in char_core[i]:
c.execute('''insert into char_item values(?,?,'core',?)''',
(i, j['core_id'], j['amount']))
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase']
for i in cores:
c.execute('''insert into item values(?,"core",1,'')''', (i,))
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow']
for i in world_songs:
c.execute('''insert into item values(?,"world_song",1,'')''', (i,))
world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
for i in world_unlocks:
c.execute('''insert into item values(?,"world_unlock",1,'')''', (i,))
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
for i in course_banners:
c.execute('''insert into item values(?,"course_banner",1,'')''', (i,))
c.execute('''insert into item values(?,?,?,?)''',
('fragment', 'fragment', 1, ''))
c.execute('''insert into item values(?,?,?,?)''',
('memory', 'memory', 1, ''))
c.execute('''insert into item values(?,?,?,?)''',
('anni5tix', 'anni5tix', 1, ''))
def insert_items(c, items):
# 物品数据导入
for i in items:
if 'discount_from' not in i:
discount_from = -1
else:
discount_from = i['discount_from']
if 'discount_to' not in i:
discount_to = -1
else:
discount_to = i['discount_to']
if 'discount_reason' not in i:
discount_reason = ''
else:
discount_reason = i['discount_reason']
c.execute('''insert into purchase values(?,?,?,?,?,?)''',
(i['name'], i['price'], i['orig_price'], discount_from, discount_to, discount_reason))
for j in i['items']:
if "_id" not in j:
_id = ''
else:
_id = j['_id']
c.execute(
'''select exists(select * from item where item_id=?)''', (j['id'],))
if c.fetchone() == (0,):
c.execute('''insert into item values(?,?,?,?)''',
(j['id'], j['type'], j['is_available'], _id))
if 'amount' in j:
amount = j['amount']
else:
amount = 1
c.execute('''insert into purchase_item values(?,?,?,?)''',
(i['name'], j['id'], j['type'], amount))
# item初始化
try:
f = open(path+'singles.json', 'r')
singles = json.load(f)
f.close()
insert_items(c, singles)
except:
pass
try:
f = open(path+'packs.json', 'r')
packs = json.load(f)
f.close()
insert_items(c, packs)
c.execute('''select exists(select * from item where item_id='epilogue')''')
if c.fetchone() == (0,):
c.execute('''insert into item values('epilogue','pack',1,'')''')
except:
pass
# course初始化
try:
f = open(path+'courses.json', 'r', encoding="utf-8")
courses = json.load(f)
f.close()
except:
courses = []
if courses:
if path == './':
# 有什么好办法吗
import sys
sys.path.append('..')
from core.course import Course
for i in courses:
x = Course(c).from_dict(i)
x.insert_all()
# api权限与权限组初始化
role = ['admin', 'user', 'selecter']
role_caption = ['管理员', '用户', '查询接口']
power = ['select', 'select_me', 'change', 'change_me', 'grant',
'grant_inf', 'select_song_rank', 'select_song_info']
power_caption = ['总体查询权限', '自我查询权限', '总体修改权限',
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限']
role_power = {'0': [0, 1, 3, 5, 6, 7],
'1': [1, 3, 6, 7],
'2': [0]
}
for i in range(0, len(role)):
c.execute('''insert into role values(:a,:b,:c)''', {
'a': i, 'b': role[i], 'c': role_caption[i]})
for i in range(0, len(power)):
c.execute('''insert into power values(:a,:b,:c)''', {
'a': i, 'b': power[i], 'c': power_caption[i]})
for i in role_power:
for j in role_power[i]:
c.execute('''insert into role_power values(:a,:b)''',
{'a': int(i), 'b': j})
conn.commit()
conn.close()
def arc_register(name: str):
def build_user_code(c):
return '123456789'
def build_user_id(c):
return 2000000
def insert_user_char(c, user_id):
# 为用户添加可用角色
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
(user_id, 0, 30, 25000, 1, 0))
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
(user_id, 1, 30, 25000, 1, 0))
c.execute(
'''select character_id, max_level, is_uncapped from character''')
x = c.fetchall()
if x:
for i in x:
exp = 25000 if i[1] == 30 else 10000
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
(user_id, i[0], i[1], exp, i[2], 0))
conn = sqlite3.connect(path+'arcaea_database.db')
c = conn.cursor()
hash_pwd = '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb'
c.execute(
'''select exists(select * from user where name = :name)''', {'name': name})
if c.fetchone() == (0,):
user_code = build_user_code(c)
user_id = build_user_id(c)
now = int(time.time() * 1000)
c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket)
values(:user_id, :name, :password, :join_date, :user_code, 1250, 1, 0, 1, 0, 0, -1, 0, '', 114514)
''', {'user_code': user_code, 'user_id': user_id, 'join_date': now, 'name': name, 'password': hash_pwd})
c.execute('''insert into recent30(user_id) values(:user_id)''', {
'user_id': user_id})
c.execute(
'''insert into best_score values(2000000,'vexaria',3,10000000,100,0,0,0,100,0,1599667200,3,3,10.8)''')
insert_user_char(c, user_id)
conn.commit()
conn.close()
return None
else:
conn.commit()
conn.close()
return None
arc_register('admin')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -0,0 +1,89 @@
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)']
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_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 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]
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]
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]
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]
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]
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 60]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 60]
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]
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}]
}
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']
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']
world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]
course_banners = ['course_banner_' + str(i) for i in range(1, 12)]
role = ['system', 'admin', 'user', 'selecter']
role_caption = ['系统', '管理员', '用户', '查询接口']
power = ['system', 'select', 'select_me', 'change', 'change_me', 'grant',
'grant_inf', 'select_song_rank', 'select_song_info', 'select_song_rank_top']
power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限',
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限', '歌曲排行榜有限查询权限']
role_power = {'system': power,
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank_top', 'select_song_info'],
'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'],
'selector': ['select']
}

View 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;

View File

@@ -327,11 +327,10 @@
"is_available": true "is_available": true
} }
], ],
"price": 0, "price": 700,
"orig_price": 700, "orig_price": 700,
"discount_from": 1646784000000, "discount_from": 1646784000000,
"discount_to": 1647388799000, "discount_to": 1647388799000
"discount_reason": "extend"
}, },
{ {
"name": "alice", "name": "alice",
@@ -595,6 +594,85 @@
"id": "finale", "id": "finale",
"is_available": true "is_available": true
}, },
{
"type": "pack",
"id": "epilogue",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "ongeki_append_1",
"items": [
{
"type": "pack",
"id": "ongeki_append_1",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "extend_2",
"items": [
{
"type": "pack",
"id": "extend_2",
"is_available": true
},
{
"type": "core",
"amount": 7,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 700,
"price": 700,
"discount_from": 1646784000000,
"discount_to": 1647388799000
},
{
"name": "maimai_append_1",
"items": [
{
"type": "pack",
"id": "maimai_append_1",
"is_available": true
},
{
"type": "core",
"amount": 4,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 400,
"price": 400
},
{
"name": "chunithm_append_2",
"items": [
{
"type": "pack",
"id": "chunithm_append_2",
"is_available": true
},
{ {
"type": "core", "type": "core",
"amount": 5, "amount": 5,

View File

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

View File

@@ -0,0 +1,314 @@
create table if not exists config(id text primary key, value text);
create table if not exists user(user_id int primary key,
name text unique,
password text,
join_date char(20),
user_code char(10),
rating_ptt int,
character_id int,
is_skill_sealed int,
is_char_uncapped int,
is_char_uncapped_override int,
is_hide_rating int,
song_id text,
difficulty int,
score int,
shiny_perfect_count int,
perfect_count int,
near_count int,
miss_count int,
health int,
modifier int,
time_played int,
clear_type int,
rating real,
favorite_character int,
max_stamina_notification_enabled int,
current_map text,
ticket int,
prog_boost int,
email text,
world_rank_score int,
ban_flag text,
next_fragstam_ts int,
max_stamina_ts int,
stamina int,
world_mode_locked_end_ts int,
beyond_boost_gauge real default 0
);
create table if not exists login(access_token text,
user_id int,
login_time int,
login_ip text,
login_device text,
primary key(access_token, user_id)
);
create table if not exists friend(user_id_me int,
user_id_other int,
primary key (user_id_me, user_id_other)
);
create table if not exists best_score(user_id int,
song_id text,
difficulty int,
score int,
shiny_perfect_count int,
perfect_count int,
near_count int,
miss_count int,
health int,
modifier int,
time_played int,
best_clear_type int,
clear_type int,
rating real,
primary key(user_id, song_id, difficulty)
);
create table if not exists user_char(user_id int,
character_id int,
level int,
exp real,
is_uncapped int,
is_uncapped_override int,
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,
primary key(user_id, character_id)
);
create table if not exists character(character_id int primary key,
name text,
max_level int,
frag1 real,
prog1 real,
overdrive1 real,
frag20 real,
prog20 real,
overdrive20 real,
frag30 real,
prog30 real,
overdrive30 real,
skill_id text,
skill_unlock_level int,
skill_requires_uncap int,
skill_id_uncap text,
char_type int,
is_uncapped int
);
create table if not exists char_item(character_id int,
item_id text,
type text,
amount int,
primary key(character_id, item_id, type)
);
create table if not exists recent30(user_id int primary key,
r0 real,
song_id0 text,
r1 real,
song_id1 text,
r2 real,
song_id2 text,
r3 real,
song_id3 text,
r4 real,
song_id4 text,
r5 real,
song_id5 text,
r6 real,
song_id6 text,
r7 real,
song_id7 text,
r8 real,
song_id8 text,
r9 real,
song_id9 text,
r10 real,
song_id10 text,
r11 real,
song_id11 text,
r12 real,
song_id12 text,
r13 real,
song_id13 text,
r14 real,
song_id14 text,
r15 real,
song_id15 text,
r16 real,
song_id16 text,
r17 real,
song_id17 text,
r18 real,
song_id18 text,
r19 real,
song_id19 text,
r20 real,
song_id20 text,
r21 real,
song_id21 text,
r22 real,
song_id22 text,
r23 real,
song_id23 text,
r24 real,
song_id24 text,
r25 real,
song_id25 text,
r26 real,
song_id26 text,
r27 real,
song_id27 text,
r28 real,
song_id28 text,
r29 real,
song_id29 text
);
create table if not exists user_world(user_id int,
map_id text,
curr_position int,
curr_capture real,
is_locked int,
primary key(user_id, map_id)
);
create table if not exists songplay_token(token text primary key,
user_id int,
song_id text,
difficulty int,
course_id text,
course_state int,
course_score int,
course_clear_type int,
stamina_multiply int,
fragment_multiply int,
prog_boost_multiply int,
beyond_boost_gauge_usage int
);
create table if not exists item(item_id text,
type text,
is_available int,
primary key(item_id, type)
);
create table if not exists user_item(user_id int,
item_id text,
type text,
amount int,
primary key(user_id, item_id, type)
);
create table if not exists purchase(purchase_name text primary key,
price int,
orig_price int,
discount_from int,
discount_to int,
discount_reason text
);
create table if not exists purchase_item(purchase_name text,
item_id text,
type text,
amount int,
primary key(purchase_name, item_id, type)
);
create table if not exists user_save(user_id int primary key,
scores_data text,
clearlamps_data text,
clearedsongs_data text,
unlocklist_data text,
installid_data text,
devicemodelname_data text,
story_data text,
createdAt int,
finalestate_data text
);
create table if not exists present(present_id text primary key,
expire_ts int,
description text
);
create table if not exists user_present(user_id int,
present_id text,
primary key(user_id, present_id)
);
create table if not exists present_item(present_id text,
item_id text,
type text,
amount int,
primary key(present_id, item_id, type)
);
create table if not exists chart(song_id text primary key,
name text,
rating_pst int,
rating_prs int,
rating_ftr int,
rating_byn int
);
create table if not exists redeem(code text primary key,
type int
);
create table if not exists user_redeem(user_id int,
code text,
primary key(user_id, code)
);
create table if not exists redeem_item(code text,
item_id text,
type text,
amount int,
primary key(code, item_id, type)
);
create table if not exists role(role_id text primary key,
caption text
);
create table if not exists user_role(user_id int,
role_id text,
primary key(user_id, role_id)
);
create table if not exists power(power_id text primary key,
caption text
);
create table if not exists role_power(role_id text,
power_id text,
primary key(role_id, power_id)
);
create table if not exists api_login(user_id int,
token text,
login_time int,
login_ip text,
primary key(user_id, token)
);
create table if not exists course(course_id text primary key,
course_name text,
dan_name text,
style int,
gauge_requirement text,
flag_as_hidden_when_requirements_not_met int,
can_start int
);
create table if not exists user_course(user_id int,
course_id text,
high_score int,
best_clear_type int,
primary key(user_id, course_id)
);
create table if not exists course_chart(course_id text,
song_id text,
difficulty int,
flag_as_hidden int,
song_index int,
primary key(course_id, song_index)
);
create table if not exists course_requirement(course_id text,
required_id text,
primary key(course_id, required_id)
);
create table if not exists course_item(course_id text,
item_id text,
type text,
amount int,
primary key(course_id, item_id, type)
);
create index if not exists best_score_1 on best_score (song_id, difficulty);
PRAGMA journal_mode = WAL;
PRAGMA default_cache_size = 8000;

View File

@@ -0,0 +1,12 @@
{
"songs": [
{
"id": "dement",
"difficulties": [
{
"ratingClass": 3
}
]
}
]
}

View File

@@ -0,0 +1 @@
from .main import link_play

View File

@@ -1,11 +1,11 @@
import os from os import urandom
from cryptography.hazmat.primitives.ciphers import ( from cryptography.hazmat.primitives.ciphers import (
Cipher, algorithms, modes Cipher, algorithms, modes
) )
def encrypt(key, plaintext, associated_data): def encrypt(key, plaintext, associated_data):
iv = os.urandom(12) iv = urandom(12)
encryptor = Cipher( encryptor = Cipher(
algorithms.AES(key), algorithms.AES(key),
modes.GCM(iv, min_tag_length=12), modes.GCM(iv, min_tag_length=12),

View File

@@ -0,0 +1,29 @@
class Config:
'''
Link Play server configuration
'''
'''
服务器地址、端口号、校验码
Server address, port and verification code
'''
HOST = '0.0.0.0'
UDP_PORT = 10900
TCP_PORT = 10901
AUTHENTICATION = 'my_link_play_server'
'''
--------------------------------------------------
'''
DEBUG = False
TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000
COUNTDOWM_TIME = 3999
PLAYER_PRE_TIMEOUT = 3000000
PLAYER_TIMEOUT = 20000000
LINK_PLAY_UNLOCK_LENGTH = 512

View File

@@ -0,0 +1,83 @@
import binascii
import logging
import socketserver
import threading
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
try:
token = client_msg[:8]
iv = client_msg[8:20]
tag = client_msg[20:32]
ciphertext = client_msg[32:]
if bi(token) not in Store.link_play_data:
return None
user = Store.link_play_data[bi(token)]
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[:12] +
ciphertext, self.client_address)
class TCP_handler(socketserver.StreamRequestHandler):
def handle(self):
self.data = self.rfile.readline().strip()
message = self.data.decode('utf-8')
if Config.DEBUG:
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
data = message.split('|')
if data[0] != Config.AUTHENTICATION:
self.wfile.write(b'No authentication')
logging.warning(f'TCP-{self.client_address[0]}-No authentication')
return None
r = TCPRouter(data[1:]).handle()
if Config.DEBUG:
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
self.wfile.write(r.encode('utf-8'))
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]

View File

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

View File

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

View File

@@ -1,36 +1,29 @@
import time import time
from .udp_class import Room, bi from .udp_class import Room, bi
from .udp_config import Config from .config import Config
from .udp_sender import CommandSender from .udp_sender import CommandSender
class CommandParser: class CommandParser:
route = [None, 'command_01', 'command_02', 'command_03', 'command_04', 'command_05',
'command_06', 'command_07', 'command_08', 'command_09', 'command_0a', 'command_0b']
def __init__(self, room: Room, player_index: int = 0) -> None: def __init__(self, room: Room, player_index: int = 0) -> None:
self.room = room self.room = room
self.player_index = player_index self.player_index = player_index
self.s = CommandSender(self.room)
self.command: bytes = None
def get_commands(self, command): def get_commands(self, command: bytes):
self.command = command self.command = command
l = {b'\x06\x16\x01': self.command_01, r = getattr(self, self.route[self.command[2]])()
b'\x06\x16\x02': self.command_02,
b'\x06\x16\x03': self.command_03,
b'\x06\x16\x04': self.command_04,
b'\x06\x16\x05': self.command_05,
b'\x06\x16\x06': self.command_06,
b'\x06\x16\x07': self.command_07,
b'\x06\x16\x08': self.command_08,
b'\x06\x16\x09': self.command_09,
b'\x06\x16\x0a': self.command_0a,
b'\x06\x16\x0b': self.command_0b
}
r = l[command[:3]]()
re = [] re = []
flag_13 = False flag_13 = False
for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length): for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length):
if self.room.command_queue[i][:3] == b'\x06\x16\x13': if self.room.command_queue[i][2] == 0x13:
if flag_13: if flag_13:
break break
flag_13 = True flag_13 = True
@@ -52,16 +45,11 @@ class CommandParser:
if i.player_id == player_id and i.online == 1: if i.player_id == player_id and i.online == 1:
self.room.host_id = player_id self.room.host_id = player_id
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24] self.room.command_queue.append(self.s.command_10())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_10())
return None
def command_02(self): def command_02(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
song_idx = bi(self.command[24:26]) song_idx = bi(self.command[24:26])
flag = 2 flag = 2
@@ -69,17 +57,14 @@ class CommandParser:
flag = 0 flag = 0
self.room.state = 3 self.room.state = 3
self.room.song_idx = song_idx self.room.song_idx = song_idx
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_11())
self.room.command_queue.append(x.command_11()) self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_13())
return [x.command_0d(flag)] return [self.s.command_0d(flag)]
def command_03(self): def command_03(self):
# 尝试进入结算 # 尝试进入结算
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player = self.room.players[self.player_index] player = self.room.players[self.player_index]
player.score = bi(self.command[24:28]) player.score = bi(self.command[24:28])
player.cleartype = self.command[28] player.cleartype = self.command[28]
@@ -89,20 +74,15 @@ class CommandParser:
player.last_timestamp -= Config.COMMAND_INTERVAL player.last_timestamp -= Config.COMMAND_INTERVAL
self.room.last_song_idx = self.room.song_idx self.room.last_song_idx = self.room.song_idx
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_12(self.player_index))
self.room.command_queue.append(x.command_12(self.player_index))
if self.room.is_finish(): if self.room.is_finish():
self.room.make_finish() self.room.make_finish()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return None
def command_04(self): def command_04(self):
# 踢人 # 踢人
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player_id = bi(self.command[24:32]) player_id = bi(self.command[24:32])
flag = 2 flag = 2
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id: if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
@@ -110,51 +90,38 @@ class CommandParser:
if self.room.players[i].player_id == player_id: if self.room.players[i].player_id == player_id:
flag = 1 flag = 1
self.room.delete_player(i) self.room.delete_player(i)
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_12(i))
self.room.command_queue.append(x.command_12(i))
self.room.update_song_unlock() self.room.update_song_unlock()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_14())
break break
return [x.command_0d(flag)] return [self.s.command_0d(flag)]
def command_05(self): def command_05(self):
pass pass
def command_06(self): def command_06(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return None
def command_07(self): def command_07(self):
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
self.room.players[self.player_index].song_unlock = self.command[24:536] self.room.players[self.player_index].song_unlock = self.command[24:536]
self.room.update_song_unlock() self.room.update_song_unlock()
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_14())
return None
def command_08(self): def command_08(self):
self.room.round_switch = bi(self.command[24:25]) self.room.round_switch = bi(self.command[24:25])
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24] self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_13())
return None
def command_09(self): def command_09(self):
re = [] re = []
x = CommandSender(self.room) self.s.random_code = self.command[16:24]
x.random_code = self.command[16:24]
player = self.room.players[self.player_index] player = self.room.players[self.player_index]
if bi(self.command[12:16]) == 0: if bi(self.command[12:16]) == 0:
@@ -162,29 +129,17 @@ class CommandParser:
self.room.state = 1 self.room.state = 1
self.room.update_song_unlock() self.room.update_song_unlock()
player.start_command_num = self.room.command_queue_length player.start_command_num = self.room.command_queue_length
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_15())
self.room.command_queue.append(x.command_15())
else: else:
if x.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL: if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
re.append(x.command_0c()) re.append(self.s.command_0c())
player.last_timestamp = x.timestamp player.last_timestamp = self.s.timestamp
flag_13 = False
# 离线判断 # 离线判断
for i in range(4): flag_13, player_index_list = self.room.check_player_online(
if i != self.player_index: self.s.timestamp)
t = self.room.players[i] for i in player_index_list:
if t.player_id != 0: self.room.command_queue.append(self.s.command_12(i))
if t.last_timestamp != 0:
if t.online == 1 and x.timestamp - t.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
t.online = 0
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(i))
elif t.online == 0 and x.timestamp - t.last_timestamp >= Config.PLAYER_TIMEOUT:
self.room.delete_player(i)
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(i))
flag_13 = True
flag_11 = False flag_11 = False
flag_12 = False flag_12 = False
@@ -230,8 +185,11 @@ class CommandParser:
self.room.countdown = Config.COUNTDOWM_TIME self.room.countdown = Config.COUNTDOWM_TIME
self.room.timestamp = round(time.time() * 1000) self.room.timestamp = round(time.time() * 1000)
self.room.state = 4 self.room.state = 4
if self.room.round_switch == 1:
# 将换房主时间提前到此刻
self.room.make_round()
if self.room.state == 4 or self.room.state == 5 or self.room.state == 6: if self.room.state in (4, 5, 6):
timestamp = round(time.time() * 1000) timestamp = round(time.time() * 1000)
self.room.countdown -= timestamp - self.room.timestamp self.room.countdown -= timestamp - self.room.timestamp
self.room.timestamp = timestamp self.room.timestamp = timestamp
@@ -260,10 +218,9 @@ class CommandParser:
self.room.countdown = 0xffffffff self.room.countdown = 0xffffffff
flag_13 = True flag_13 = True
if self.room.countdown <= 0: self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0
self.room.countdown = 0
if self.room.state == 7 or self.room.state == 8: if self.room.state in (7, 8):
if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0: if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0:
player.last_timer = player.timer player.last_timer = player.timer
player.last_score = player.score player.last_score = player.score
@@ -273,14 +230,12 @@ class CommandParser:
if player.timer != 0 or self.room.state != 8: if player.timer != 0 or self.room.state != 8:
for i in self.room.players: for i in self.room.players:
i.extra_command_queue.append( i.extra_command_queue.append(
x.command_0e(self.player_index)) self.s.command_0e(self.player_index))
if self.room.is_ready(8, 1): if self.room.is_ready(8, 1):
flag_13 = True flag_13 = True
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
if self.room.round_switch == 1:
self.room.make_round()
for i in self.room.players: for i in self.room.players:
i.timer = 0 i.timer = 0
@@ -292,14 +247,12 @@ class CommandParser:
flag_13 = True flag_13 = True
if flag_11: if flag_11:
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_11())
self.room.command_queue.append(x.command_11())
if flag_12: if flag_12:
self.room.command_queue_length += 1 self.room.command_queue.append(
self.room.command_queue.append(x.command_12(self.player_index)) self.s.command_12(self.player_index))
if flag_13: if flag_13:
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(x.command_13())
return re return re
@@ -307,28 +260,19 @@ class CommandParser:
# 退出房间 # 退出房间
self.room.delete_player(self.player_index) self.room.delete_player(self.player_index)
x = CommandSender(self.room) self.room.command_queue.append(self.s.command_12(self.player_index))
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_12(self.player_index))
if self.room.state == 3: if self.room.state in (2, 3):
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
# self.room.command_queue_length += 1 # self.room.command_queue.append(self.s.command_11())
# self.room.command_queue.append(x.command_11()) self.room.command_queue.append(self.s.command_13())
self.room.command_queue_length += 1 self.room.command_queue.append(self.s.command_14())
self.room.command_queue.append(x.command_13())
self.room.command_queue_length += 1
self.room.command_queue.append(x.command_14())
return None
def command_0b(self): def command_0b(self):
# 推荐歌曲 # 推荐歌曲
song_idx = bi(self.command[16:18]) song_idx = bi(self.command[16:18])
x = CommandSender(self.room)
for i in range(4): for i in range(4):
if self.player_index != i and self.room.players[i].online == 1: if self.player_index != i and self.room.players[i].online == 1:
self.room.players[i].extra_command_queue.append( self.room.players[i].extra_command_queue.append(
x.command_0f(self.player_index, song_idx)) self.s.command_0f(self.player_index, song_idx))
return None

View File

@@ -0,0 +1,64 @@
from time import time
from .udp_class import Room, b
class CommandSender:
PROTOCOL_NAME = b'\x06\x16'
PROTOCOL_VERSION = b'\x09'
def __init__(self, room: Room = None) -> None:
self.room = room
self.timestamp = round(time() * 1000000)
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
@staticmethod
def command_encode(t: tuple):
r = b''.join(t)
x = 16 - len(r) % 16
return r + b(x) * x
def command_prefix(self, command: bytes):
length = self.room.command_queue_length
if command >= b'\x10':
length += 1
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
def command_0c(self):
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):
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
def command_0e(self, player_index: int):
# 分数广播
player = self.room.players[player_index]
return self.command_encode((*self.command_prefix(b'\x0e'), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b'\x01', b(player.last_score, 4), b(player.last_timer, 4), b(player.online)))
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), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b(player.online)))
def command_13(self):
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
def command_14(self):
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, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))

View File

@@ -1,29 +1,53 @@
# encoding: utf-8 # encoding: utf-8
import os import os
from importlib import import_module
from core.config_manager import Config, ConfigManager
if os.path.exists('config.py') or os.path.exists('config'):
# 导入用户自定义配置
ConfigManager.load(import_module('config').Config)
if Config.DEPLOY_MODE == 'gevent':
# 异步
from gevent import monkey
monkey.patch_all()
import sys import sys
from logging.config import dictConfig from logging.config import dictConfig
from multiprocessing import Process, set_start_method from multiprocessing import Process, set_start_method
from traceback import format_exc
from flask import Flask, request, send_from_directory from flask import Flask, make_response, request, send_from_directory
import api import api
import server import server
import server.init
import web.index import web.index
import web.login import web.login
from core.constant import Constant from core.constant import Constant
from core.download import UserDownload, initialize_songfile from core.download import UserDownload
from core.error import ArcError from core.error import ArcError, NoAccess, RateLimit
from core.init import FileChecker
from core.sql import Connect from core.sql import Connect
from server.func import error_return from server.func import error_return
from setting import Config
app = Flask(__name__) app = Flask(__name__)
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)
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径 os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY) app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
app.config['SESSION_TYPE'] = 'filesystem' app.config['SESSION_TYPE'] = 'filesystem'
app.register_blueprint(web.login.bp) app.register_blueprint(web.login.bp)
@@ -48,22 +72,61 @@ def favicon():
@app.route('/download/<path:file_path>', methods=['GET']) # 下载 @app.route('/download/<path:file_path>', methods=['GET']) # 下载
def download(file_path): def download(file_path):
with Connect() as c: with Connect(in_memory=True) as c:
try: try:
x = UserDownload(c) x = UserDownload(c)
x.file_path = file_path x.token = request.args.get('t')
x.select_from_token(request.args.get('t')) x.song_id, x.file_name = file_path.split('/', 1)
x.select_for_check()
if x.is_limited: if x.is_limited:
raise ArcError('You have reached the download limit.', 903) raise RateLimit('You have reached the download limit.', 903)
if x.is_valid: if not x.is_valid:
x.insert_user_download() raise NoAccess('Expired token.')
return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True) 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: except ArcError as e:
if Config.ALLOW_WARNING_LOG:
app.logger.warning(format_exc())
return error_return(e) return error_return(e)
return error_return() return error_return()
if Config.DEPLOY_MODE == 'waitress':
# 给waitress加个日志
@app.after_request
def after_request(response):
app.logger.info(
f'{request.remote_addr} - - {request.method} {request.path} {response.status_code}')
return response
# @app.before_request
# def before_request():
# print(request.path)
# print(request.headers)
# print(request.data)
def tcp_server_run(): def tcp_server_run():
if 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
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: if Config.SSL_CERT and Config.SSL_KEY:
app.run(Config.HOST, Config.PORT, ssl_context=( app.run(Config.HOST, Config.PORT, ssl_context=(
Config.SSL_CERT, Config.SSL_KEY)) Config.SSL_CERT, Config.SSL_KEY))
@@ -71,6 +134,18 @@ def tcp_server_run():
app.run(Config.HOST, Config.PORT) app.run(Config.HOST, Config.PORT)
def generate_log_file_dict(level: str, filename: str) -> dict:
return {
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": level,
"formatter": "default",
"filename": filename
}
def main(): def main():
log_dict = { log_dict = {
'version': 1, 'version': 1,
@@ -84,15 +159,7 @@ def main():
'stream': 'ext://flask.logging.wsgi_errors_stream', 'stream': 'ext://flask.logging.wsgi_errors_stream',
'formatter': 'default' 'formatter': 'default'
}, },
"error_file": { "error_file": generate_log_file_dict('ERROR', './log/error.log')
"class": "logging.handlers.RotatingFileHandler",
"maxBytes": 1024 * 1024,
"backupCount": 1,
"encoding": "utf-8",
"level": "ERROR",
"formatter": "default",
"filename": "./log/error.log"
}
}, },
'formatters': { 'formatters': {
'default': { 'default': {
@@ -100,39 +167,32 @@ def main():
} }
} }
} }
if Config.ALLOW_LOG_INFO: if Config.ALLOW_INFO_LOG:
log_dict['root']['handlers'] = ['wsgi', 'info_file', 'error_file'] log_dict['root']['handlers'].append('info_file')
log_dict['handlers']['info_file'] = { log_dict['handlers']['info_file'] = generate_log_file_dict(
"class": "logging.handlers.RotatingFileHandler", 'INFO', './log/info.log')
"maxBytes": 1024 * 1024, if Config.ALLOW_WARNING_LOG:
"backupCount": 1, log_dict['root']['handlers'].append('warning_file')
"encoding": "utf-8", log_dict['handlers']['warning_file'] = generate_log_file_dict(
"level": "INFO", 'WARNING', './log/warning.log')
"formatter": "default",
"filename": "./log/info.log"
}
dictConfig(log_dict) dictConfig(log_dict)
if not server.init.check_before_run(app): Connect.logger = app.logger
app.logger.error('Something wrong. The server will not run.') if not FileChecker(app.logger).check_before_run():
app.logger.error('Some errors occurred. The server will not run.')
input('Press ENTER key to exit.') input('Press ENTER key to exit.')
sys.exit() sys.exit()
app.logger.info("Start to initialize song data...") if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
try: from linkplay_server import link_play
initialize_songfile()
app.logger.info('Complete!')
except:
app.logger.warning('Initialization error!')
if Config.UDP_PORT and Config.UDP_PORT != '':
from server.multiplayer import conn2
from udpserver.udp_main import link_play
process = [Process(target=link_play, args=( process = [Process(target=link_play, args=(
conn2, Config.HOST, int(Config.UDP_PORT)))] Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
[p.start() for p in process] [p.start() for p in process]
app.logger.info("UDP server is running...") 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() tcp_server_run()
[p.join() for p in process] [p.join() for p in process]
else: else:

View File

@@ -1,2 +1,3 @@
flask>=2.0 flask>=2.0.2
cryptography cryptography>=3.0.0
limits>=2.7.0

View File

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

View File

@@ -0,0 +1,9 @@
import os
import sys
import linkplay_server
os.chdir(sys.path[0])
if __name__ == '__main__':
linkplay_server.link_play()

View File

@@ -1,5 +1,6 @@
from flask import Blueprint from flask import Blueprint
from setting import Config
from core.config_manager import Config
from . import (auth, course, friend, multiplayer, others, present, purchase, from . import (auth, course, friend, multiplayer, others, present, purchase,
score, user, world) score, user, world)

View File

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

View File

@@ -1,28 +1,27 @@
import base64 import base64
import functools from functools import wraps
from flask import Blueprint, current_app, g, jsonify, request
from core.error import ArcError, NoAccess from core.error import ArcError, NoAccess
from core.sql import Connect from core.sql import Connect
from core.user import UserAuth, UserLogin from core.user import UserAuth, UserLogin
from flask import Blueprint, jsonify, request
from setting import Config
from .func import error_return from .func import arc_try, error_return, header_check
bp = Blueprint('auth', __name__, url_prefix='/auth') bp = Blueprint('auth', __name__, url_prefix='/auth')
@bp.route('/login', methods=['POST']) # 登录接口 @bp.route('/login', methods=['POST']) # 登录接口
@arc_try
def login(): def login():
if 'AppVersion' in request.headers: # 版本检查
if Config.ALLOW_APPVERSION:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return error_return(NoAccess('Wrong app version.', 1203))
headers = request.headers headers = request.headers
e = header_check(request)
if e is not None:
raise e
request.form['grant_type'] request.form['grant_type']
with Connect() as c: with Connect() as c:
try:
id_pwd = headers['Authorization'] id_pwd = headers['Authorization']
id_pwd = base64.b64decode(id_pwd[6:]).decode() id_pwd = base64.b64decode(id_pwd[6:]).decode()
name, password = id_pwd.split(':', 1) name, password = id_pwd.split(':', 1)
@@ -33,36 +32,42 @@ def login():
user = UserLogin(c) user = UserLogin(c)
user.login(name, password, device_id, request.remote_addr) user.login(name, password, device_id, request.remote_addr)
current_app.logger.info(f'User `{user.user_id}` log in')
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token}) return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
except ArcError as e:
return error_return(e)
return error_return()
def auth_required(request): @bp.route('/verify', methods=['POST']) # 邮箱验证进度查询
@arc_try
def email_verify():
raise ArcError('Email verification unavailable.', 151, status=404)
def auth_required(req):
# arcaea登录验证写成了修饰器 # arcaea登录验证写成了修饰器
def decorator(view): def decorator(view):
@functools.wraps(view) @wraps(view)
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):
headers = request.headers headers = req.headers
if 'AppVersion' in headers: # 版本检查 e = header_check(req)
if Config.ALLOW_APPVERSION: if e is not None:
if headers['AppVersion'] not in Config.ALLOW_APPVERSION: current_app.logger.warning(
return error_return(NoAccess('Wrong app version.', 1203)) f' - {e.error_code}|{e.api_error_code}: {e}')
return error_return(e)
with Connect() as c: with Connect() as c:
try: try:
user = UserAuth(c) user = UserAuth(c)
user.token = headers['Authorization'][7:] token = headers.get('Authorization')
return view(user.token_get_id(), *args, **kwargs) if not token:
raise NoAccess('No token.', -4)
user.token = token[7:]
user_id = user.token_get_id()
g.user = user
except ArcError as e: except ArcError as e:
return error_return(e) return error_return(e)
return view(user_id, *args, **kwargs)
return error_return()
return wrapped_view return wrapped_view
return decorator return decorator

View File

@@ -1,26 +1,26 @@
from flask import Blueprint, request
from core.constant import Constant from core.constant import Constant
from core.course import UserCourseList from core.course import UserCourseList
from core.error import ArcError
from core.item import ItemCore from core.item import ItemCore
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('course', __name__, url_prefix='/course') bp = Blueprint('course', __name__, url_prefix='/course')
@bp.route('/me', methods=['GET']) @bp.route('/me', methods=['GET'])
@auth_required(request) @auth_required(request)
@arc_try
def course_me(user_id): def course_me(user_id):
with Connect() as c: with Connect() as c:
try:
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
core = ItemCore(c) core = ItemCore(c)
core.item_id = 'core_course_skip_purchase' core.item_id = 'core_course_skip_purchase'
core.select(user) core.select_user_item(user)
x = UserCourseList(c, user) x = UserCourseList(c, user)
x.select_all() x.select_all()
return success_return({ return success_return({
@@ -28,6 +28,3 @@ def course_me(user_id):
"stamina_cost": Constant.COURSE_STAMINA_COST, "stamina_cost": Constant.COURSE_STAMINA_COST,
"course_skip_purchase_ticket": core.amount "course_skip_purchase_ticket": core.amount
}) })
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,18 +1,19 @@
from flask import Blueprint, request from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.error import ArcError
from core.user import UserOnline, code_get_id from core.user import UserOnline, code_get_id
from .func import error_return, success_return
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return
bp = Blueprint('friend', __name__, url_prefix='/friend') bp = Blueprint('friend', __name__, url_prefix='/friend')
@bp.route('/me/add', methods=['POST']) # 加好友 @bp.route('/me/add', methods=['POST']) # 加好友
@auth_required(request) @auth_required(request)
@arc_try
def add_friend(user_id): def add_friend(user_id):
with Connect() as c: with Connect() as c:
try:
friend_code = request.form['friend_code'] friend_code = request.form['friend_code']
friend_id = code_get_id(c, friend_code) friend_id = code_get_id(c, friend_code)
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
@@ -24,16 +25,13 @@ def add_friend(user_id):
"createdAt": "2020-09-06T10:05:18.471Z", "createdAt": "2020-09-06T10:05:18.471Z",
"friends": user.friends "friends": user.friends
}) })
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/delete', methods=['POST']) # 删好友 @bp.route('/me/delete', methods=['POST']) # 删好友
@auth_required(request) @auth_required(request)
@arc_try
def delete_friend(user_id): def delete_friend(user_id):
with Connect() as c: with Connect() as c:
try:
friend_id = int(request.form['friend_id']) friend_id = int(request.form['friend_id'])
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
user.delete_friend(friend_id) user.delete_friend(friend_id)
@@ -44,6 +42,3 @@ def delete_friend(user_id):
"createdAt": "2020-09-06T10:05:18.471Z", "createdAt": "2020-09-06T10:05:18.471Z",
"friends": user.friends "friends": user.friends
}) })
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,7 +1,19 @@
from flask import jsonify from functools import wraps
from core.error import ArcError from traceback import format_exc
default_error = ArcError('Unknown Error') from flask import current_app, g, jsonify
from core.config_manager import Config
from core.error import ArcError, NoAccess
has_arc_hash = False
try:
from core.arc_crypto import ArcHashChecker # type: ignore
has_arc_hash = True
except ModuleNotFoundError:
pass
default_error = ArcError('Unknown Error', status=500)
def error_return(e: ArcError = default_error): # 错误返回 def error_return(e: ArcError = default_error): # 错误返回
@@ -28,6 +40,8 @@ def error_return(e: ArcError = default_error): # 错误返回
# 124 你今天不能再使用这个IP地址创建新的账号 # 124 你今天不能再使用这个IP地址创建新的账号
# 150 非常抱歉您已被限制使用此功能 # 150 非常抱歉您已被限制使用此功能
# 151 目前无法使用此功能 # 151 目前无法使用此功能
# 160 账户未邮箱认证,请检查邮箱
# 161 账户认证过期,请重新注册
# 401 用户不存在 # 401 用户不存在
# 403 无法连接至服务器 # 403 无法连接至服务器
# 501 502 -6 此物品目前无法获取 # 501 502 -6 此物品目前无法获取
@@ -39,6 +53,7 @@ def error_return(e: ArcError = default_error): # 错误返回
# 604 你不能加自己为好友 # 604 你不能加自己为好友
# 903 下载量超过了限制请24小时后重试 # 903 下载量超过了限制请24小时后重试
# 905 请在再次使用此功能前等待24小时 # 905 请在再次使用此功能前等待24小时
# 910 重新请求验证邮件前需等待x分钟 extra: retry_at
# 1001 设备数量达到上限 # 1001 设备数量达到上限
# 1002 此设备已使用过此功能 # 1002 此设备已使用过此功能
# 1201 房间已满 # 1201 房间已满
@@ -56,7 +71,7 @@ def error_return(e: ArcError = default_error): # 错误返回
if e.extra_data: if e.extra_data:
r['extra'] = e.extra_data r['extra'] = e.extra_data
return jsonify(r) return jsonify(r), e.status
def success_return(value=None): def success_return(value=None):
@@ -64,3 +79,36 @@ def success_return(value=None):
if value is not None: if value is not None:
r['value'] = value r['value'] = value
return jsonify(r) 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 NoAccess('Invalid app version', 1203)
if has_arc_hash and not ArcHashChecker(request).check():
return NoAccess('Invalid request')
return None

View File

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

View File

@@ -1,49 +1,44 @@
from multiprocessing import Pipe
from core.error import ArcError
from core.linkplay import LocalMultiPlayer, Player, Room
from core.sql import Connect
from flask import Blueprint, request from flask import Blueprint, request
from setting import Config
from core.config_manager import Config
from core.error import ArcError
from core.linkplay import Player, RemoteMultiPlayer, Room
from core.sql import Connect
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('multiplayer', __name__, url_prefix='/multiplayer') bp = Blueprint('multiplayer', __name__, url_prefix='/multiplayer')
conn1, conn2 = Pipe()
@bp.route('/me/room/create', methods=['POST']) # 创建房间 @bp.route('/me/room/create', methods=['POST']) # 创建房间
@auth_required(request) @auth_required(request)
@arc_try
def room_create(user_id): def room_create(user_id):
if not Config.UDP_PORT or Config.UDP_PORT == '': if not Config.LINKPLAY_HOST:
return error_return(ArcError('The local udp server is down.', 151)), 404 raise ArcError('The link play server is unavailable.', 151, status=404)
with Connect() as c: with Connect() as c:
try: x = RemoteMultiPlayer()
x = LocalMultiPlayer(conn1)
user = Player(c, user_id) user = Player(c, user_id)
user.get_song_unlock(request.json['clientSongMap']) user.get_song_unlock(request.json['clientSongMap'])
x.create_room(user) x.create_room(user)
r = x.to_dict() r = x.to_dict()
r['endPoint'] = request.host.split( r['endPoint'] = request.host.split(
':')[0] if Config.LINK_PLAY_HOST == '' else Config.LINK_PLAY_HOST ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
r['port'] = int(Config.UDP_PORT) r['port'] = int(Config.LINKPLAY_UDP_PORT)
return success_return(r) return success_return(r)
except ArcError as e:
return error_return(e), 400
return error_return()
@bp.route('/me/room/join/<room_code>', methods=['POST']) # 加入房间 @bp.route('/me/room/join/<room_code>', methods=['POST']) # 加入房间
@auth_required(request) @auth_required(request)
@arc_try
def room_join(user_id, room_code): def room_join(user_id, room_code):
if not Config.UDP_PORT or Config.UDP_PORT == '': if not Config.LINKPLAY_HOST:
return error_return(ArcError('The local udp server is down.', 151)), 404 raise ArcError('The link play server is unavailable.', 151, status=404)
with Connect() as c: with Connect() as c:
try: x = RemoteMultiPlayer()
x = LocalMultiPlayer(conn1)
user = Player(c, user_id) user = Player(c, user_id)
user.get_song_unlock(request.json['clientSongMap']) user.get_song_unlock(request.json['clientSongMap'])
room = Room() room = Room()
@@ -51,31 +46,25 @@ def room_join(user_id, room_code):
x.join_room(room, user) x.join_room(room, user)
r = x.to_dict() r = x.to_dict()
r['endPoint'] = request.host.split( r['endPoint'] = request.host.split(
':')[0] if Config.LINK_PLAY_HOST == '' else Config.LINK_PLAY_HOST ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
r['port'] = int(Config.UDP_PORT) r['port'] = int(Config.LINKPLAY_UDP_PORT)
return success_return(r) return success_return(r)
except ArcError as e:
return error_return(e), 400
return error_return()
@bp.route('/me/update', methods=['POST']) # 更新房间 @bp.route('/me/update', methods=['POST']) # 更新房间
@auth_required(request) @auth_required(request)
@arc_try
def multiplayer_update(user_id): def multiplayer_update(user_id):
if not Config.UDP_PORT or Config.UDP_PORT == '': if not Config.LINKPLAY_HOST:
return error_return(ArcError('The local udp server is down.', 151)), 404 raise ArcError('The link play server is unavailable.', 151, status=404)
with Connect() as c: with Connect() as c:
try: x = RemoteMultiPlayer()
x = LocalMultiPlayer(conn1)
user = Player(c, user_id) user = Player(c, user_id)
user.token = int(request.json['token']) user.token = int(request.json['token'])
x.update_room(user) x.update_room(user)
r = x.to_dict() r = x.to_dict()
r['endPoint'] = request.host.split( r['endPoint'] = request.host.split(
':')[0] if Config.LINK_PLAY_HOST == '' else Config.LINK_PLAY_HOST ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
r['port'] = int(Config.UDP_PORT) r['port'] = int(Config.LINKPLAY_UDP_PORT)
return success_return(r) return success_return(r)
except ArcError as e:
return error_return(e), 400
return error_return()

View File

@@ -1,18 +1,19 @@
import json import json
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from core.download import DownloadList
from core.error import ArcError
from core.sql import Connect
from core.system import GameInfo
from core.user import UserOnline
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from werkzeug.datastructures import ImmutableMultiDict from werkzeug.datastructures import ImmutableMultiDict
from core.download import DownloadList
from core.error import RateLimit
from core.sql import Connect
from core.system import GameInfo
from core.user import UserOnline
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, error_return, success_return
from .present import present_info from .present import present_info
from .purchase import bundle_pack, bundle_bundle from .purchase import bundle_bundle, bundle_pack
from .score import song_score_friend from .score import song_score_friend
from .user import user_me from .user import user_me
from .world import world_all from .world import world_all
@@ -27,21 +28,17 @@ def game_info():
@bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载 @bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载
@auth_required(request) @auth_required(request)
@arc_try
def download_song(user_id): def download_song(user_id):
with Connect() as c: with Connect(in_memory=True) as c:
try: x = DownloadList(c, UserOnline(None, user_id))
x = DownloadList(c, UserOnline(c, user_id))
x.song_ids = request.args.getlist('sid') x.song_ids = request.args.getlist('sid')
x.url_flag = json.loads(request.args.get('url', 'true')) x.url_flag = json.loads(request.args.get('url', 'true'))
x.clear_user_download() if x.url_flag and x.is_limited:
if x.is_limited and x.url_flag: raise RateLimit('You have reached the download limit.', 903)
raise ArcError('You have reached the download limit.', 903)
x.add_songs() x.add_songs()
return success_return(x.urls) return success_return(x.urls)
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/finale/progress', methods=['GET']) @bp.route('/finale/progress', methods=['GET'])
@@ -57,6 +54,11 @@ def finale_start():
return success_return({}) return success_return({})
@bp.route('/finale/finale_end', methods=['POST'])
def finale_end():
return success_return({})
map_dict = {'/user/me': user_me, map_dict = {'/user/me': user_me,
'/purchase/bundle/pack': bundle_pack, '/purchase/bundle/pack': bundle_pack,
'/serve/download/me/song': download_song, '/serve/download/me/song': download_song,
@@ -85,16 +87,19 @@ def aggregate():
{key: value[0] for key, value in parse_qs(urlparse(endpoint).query).items()}) {key: value[0] for key, value in parse_qs(urlparse(endpoint).query).items()})
resp_t = map_dict[urlparse(endpoint).path]() 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"): if hasattr(resp_t, "response"):
resp_t = resp_t.response[0].decode().rstrip('\n') resp_t = resp_t.response[0].decode().rstrip('\n')
resp = json.loads(resp_t) resp = json.loads(resp_t)
if hasattr(resp, 'get') and resp.get('success') is False: if hasattr(resp, 'get') and resp.get('success') is False:
finally_response = {'success': False, 'error_code': 7, 'extra': { finally_response = {'success': False, 'error_code': resp.get(
"id": i['id'], 'error_code': resp.get('error_code')}} 'error_code'), 'id': i['id']}
if "extra" in resp: if "extra" in resp:
finally_response['extra']['extra'] = resp['extra'] finally_response['extra'] = resp['extra']
#request = request_ #request = request_
return jsonify(finally_response) return jsonify(finally_response)

View File

@@ -1,38 +1,32 @@
from core.error import ArcError from flask import Blueprint, request
from core.present import UserPresent, UserPresentList from core.present import UserPresent, UserPresentList
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('present', __name__, url_prefix='/present') bp = Blueprint('present', __name__, url_prefix='/present')
@bp.route('/me', methods=['GET']) # 用户奖励信息 @bp.route('/me', methods=['GET']) # 用户奖励信息
@auth_required(request) @auth_required(request)
@arc_try
def present_info(user_id): def present_info(user_id):
with Connect() as c: with Connect() as c:
try:
x = UserPresentList(c, UserOnline(c, user_id)) x = UserPresentList(c, UserOnline(c, user_id))
x.select_user_presents() x.select_user_presents()
return success_return(x.to_dict_list()) return success_return(x.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/claim/<present_id>', methods=['POST']) # 礼物确认 @bp.route('/me/claim/<present_id>', methods=['POST']) # 礼物确认
@auth_required(request) @auth_required(request)
@arc_try
def claim_present(user_id, present_id): def claim_present(user_id, present_id):
with Connect() as c: with Connect() as c:
try:
x = UserPresent(c, UserOnline(c, user_id)) x = UserPresent(c, UserOnline(c, user_id))
x.claim_user_present(present_id) x.claim_user_present(present_id)
return success_return() return success_return()
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,43 +1,38 @@
from time import time from time import time
from core.error import ArcError, ItemUnavailable from flask import Blueprint, request
from core.error import InputError, ItemUnavailable, PostError
from core.item import ItemFactory, Stamina6 from core.item import ItemFactory, Stamina6
from core.purchase import Purchase, PurchaseList from core.purchase import Purchase, PurchaseList
from core.redeem import UserRedeem from core.redeem import UserRedeem
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('purchase', __name__, url_prefix='/purchase') bp = Blueprint('purchase', __name__, url_prefix='/purchase')
@bp.route('/bundle/pack', methods=['GET']) # 曲包信息 @bp.route('/bundle/pack', methods=['GET']) # 曲包信息
@auth_required(request) @auth_required(request)
@arc_try
def bundle_pack(user_id): def bundle_pack(user_id):
with Connect() as c: with Connect() as c:
try:
x = PurchaseList(c, UserOnline(c, user_id) x = PurchaseList(c, UserOnline(c, user_id)
).select_from_type('pack') ).select_from_type('pack')
return success_return(x.to_dict_list()) return success_return(x.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/bundle/single', methods=['GET']) # 单曲购买信息获取 @bp.route('/bundle/single', methods=['GET']) # 单曲购买信息获取
@auth_required(request) @auth_required(request)
@arc_try
def get_single(user_id): def get_single(user_id):
with Connect() as c: with Connect() as c:
try:
x = PurchaseList(c, UserOnline(c, user_id) x = PurchaseList(c, UserOnline(c, user_id)
).select_from_type('single') ).select_from_type('single')
return success_return(x.to_dict_list()) return success_return(x.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/bundle/bundle', methods=['GET']) # 捆绑包 @bp.route('/bundle/bundle', methods=['GET']) # 捆绑包
@@ -72,9 +67,9 @@ def bundle_bundle():
@bp.route('/me/pack', methods=['POST']) # 曲包和单曲购买 @bp.route('/me/pack', methods=['POST']) # 曲包和单曲购买
@auth_required(request) @auth_required(request)
@arc_try
def buy_pack_or_single(user_id): def buy_pack_or_single(user_id):
with Connect() as c: with Connect() as c:
try:
if 'pack_id' in request.form: if 'pack_id' in request.form:
purchase_name = request.form['pack_id'] purchase_name = request.form['pack_id']
elif 'single_id' in request.form: elif 'single_id' in request.form:
@@ -92,18 +87,15 @@ def buy_pack_or_single(user_id):
'singles': x.user.singles, 'singles': x.user.singles,
'characters': x.user.characters_list 'characters': x.user.characters_list
}) })
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/item', methods=['POST']) # 特殊购买world模式boost和stamina @bp.route('/me/item', methods=['POST']) # 特殊购买world模式boost和stamina
@auth_required(request) @auth_required(request)
@arc_try
def buy_special(user_id): def buy_special(user_id):
with Connect() as c: with Connect() as c:
try:
if 'item_id' not in request.form: if 'item_id' not in request.form:
return error_return() raise PostError('`item_id` is required')
item_id = request.form['item_id'] item_id = request.form['item_id']
x = Purchase(c, UserOnline(c, user_id)) x = Purchase(c, UserOnline(c, user_id))
@@ -121,18 +113,15 @@ def buy_special(user_id):
r['max_stamina_ts'] = x.user.stamina.max_stamina_ts r['max_stamina_ts'] = x.user.stamina.max_stamina_ts
r['world_mode_locked_end_ts'] = -1 r['world_mode_locked_end_ts'] = -1
return success_return(r) return success_return(r)
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/stamina/<buy_stamina_type>', methods=['POST']) # 购买体力 @bp.route('/me/stamina/<buy_stamina_type>', methods=['POST']) # 购买体力
@auth_required(request) @auth_required(request)
@arc_try
def purchase_stamina(user_id, buy_stamina_type): def purchase_stamina(user_id, buy_stamina_type):
with Connect() as c: with Connect() as c:
try:
if buy_stamina_type != 'fragment': if buy_stamina_type != 'fragment':
return error_return() raise InputError('Invalid type of buying stamina')
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
user.select_user_one_column('next_fragstam_ts', -1) user.select_user_one_column('next_fragstam_ts', -1)
@@ -151,20 +140,14 @@ def purchase_stamina(user_id, buy_stamina_type):
"next_fragstam_ts": user.next_fragstam_ts, "next_fragstam_ts": user.next_fragstam_ts,
'world_mode_locked_end_ts': -1 'world_mode_locked_end_ts': -1
}) })
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/redeem', methods=['POST']) # 兑换码 @bp.route('/me/redeem', methods=['POST']) # 兑换码
@auth_required(request) @auth_required(request)
@arc_try
def redeem(user_id): def redeem(user_id):
with Connect() as c: with Connect() as c:
try:
x = UserRedeem(c, UserOnline(c, user_id)) x = UserRedeem(c, UserOnline(c, user_id))
x.claim_user_redeem(request.form['code']) x.claim_user_redeem(request.form['code'])
return success_return({"coupon": "fragment" + str(x.fragment) if x.fragment > 0 else ""}) return success_return({"coupon": "fragment" + str(x.fragment) if x.fragment > 0 else ""})
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,15 +1,16 @@
from time import time from time import time
from core.course import CoursePlay
from core.error import ArcError, InputError from flask import Blueprint, request
from core.course import CoursePlay
from core.error import InputError
from core.rank import RankList from core.rank import RankList
from core.score import UserPlay from core.score import UserPlay
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('score', __name__, url_prefix='/score') bp = Blueprint('score', __name__, url_prefix='/score')
@@ -21,37 +22,32 @@ def score_token():
@bp.route('/token/world', methods=['GET']) # 世界模式成绩上传所需的token @bp.route('/token/world', methods=['GET']) # 世界模式成绩上传所需的token
@auth_required(request) @auth_required(request)
@arc_try
def score_token_world(user_id): def score_token_world(user_id):
stamina_multiply = int( stamina_multiply = int(request.args.get('stamina_multiply', 1))
request.args['stamina_multiply']) if 'stamina_multiply' in request.args else 1 fragment_multiply = int(request.args.get('fragment_multiply', 100))
fragment_multiply = int( prog_boost_multiply = int(request.args.get('prog_boost_multiply', 0))
request.args['fragment_multiply']) if 'fragment_multiply' in request.args else 100 beyond_boost_gauge_use = int(request.args.get('beyond_boost_gauge_use', 0))
prog_boost_multiply = int(
request.args['prog_boost_multiply']) if 'prog_boost_multiply' in request.args else 0
with Connect() as c: with Connect() as c:
try:
x = UserPlay(c, UserOnline(c, user_id)) x = UserPlay(c, UserOnline(c, user_id))
x.song.set_chart(request.args['song_id'], int( x.song.set_chart(request.args['song_id'], int(
request.args['difficulty'])) request.args['difficulty']))
x.set_play_state_for_world(stamina_multiply, x.set_play_state_for_world(
fragment_multiply, prog_boost_multiply) stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use)
return success_return({ return success_return({
"stamina": x.user.stamina.stamina, "stamina": x.user.stamina.stamina,
"max_stamina_ts": x.user.stamina.max_stamina_ts, "max_stamina_ts": x.user.stamina.max_stamina_ts,
"token": x.song_token "token": x.song_token
} }
) )
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token @bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
@auth_required(request) @auth_required(request)
@arc_try
def score_token_course(user_id): def score_token_course(user_id):
with Connect() as c: with Connect() as c:
try:
use_course_skip_purchase = request.args.get( use_course_skip_purchase = request.args.get(
'use_course_skip_purchase', 'false') == 'true' 'use_course_skip_purchase', 'false') == 'true'
@@ -83,16 +79,13 @@ def score_token_course(user_id):
"token": user_play.song_token, "token": user_play.song_token,
'status': status 'status': status
}) })
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/song', methods=['POST']) # 成绩上传 @bp.route('/song', methods=['POST']) # 成绩上传
@auth_required(request) @auth_required(request)
@arc_try
def song_score_post(user_id): def song_score_post(user_id):
with Connect() as c: with Connect() as c:
try:
x = UserPlay(c, UserOnline(c, user_id)) x = UserPlay(c, UserOnline(c, user_id))
x.song_token = request.form['song_token'] x.song_token = request.form['song_token']
x.song_hash = request.form['song_hash'] x.song_hash = request.form['song_hash']
@@ -102,55 +95,45 @@ def song_score_post(user_id):
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type']) request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
x.beyond_gauge = int(request.form['beyond_gauge']) x.beyond_gauge = int(request.form['beyond_gauge'])
x.submission_hash = request.form['submission_hash'] x.submission_hash = request.form['submission_hash']
if 'combo_interval_bonus' in request.form:
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
if not x.is_valid: if not x.is_valid:
raise InputError('Invalid score.', 107) raise InputError('Invalid score.', 107)
x.upload_score() x.upload_score()
return success_return(x.to_dict()) return success_return(x.to_dict())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/song', methods=['GET']) # TOP20 @bp.route('/song', methods=['GET']) # TOP20
@auth_required(request) @auth_required(request)
@arc_try
def song_score_top(user_id): def song_score_top(user_id):
with Connect() as c: with Connect() as c:
try:
rank_list = RankList(c) rank_list = RankList(c)
rank_list.song.set_chart(request.args.get( rank_list.song.set_chart(request.args.get(
'song_id'), request.args.get('difficulty')) 'song_id'), request.args.get('difficulty'))
rank_list.select_top() rank_list.select_top()
return success_return(rank_list.to_dict_list()) return success_return(rank_list.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/song/me', methods=['GET']) # 我的排名默认最多20 @bp.route('/song/me', methods=['GET']) # 我的排名默认最多20
@auth_required(request) @auth_required(request)
@arc_try
def song_score_me(user_id): def song_score_me(user_id):
with Connect() as c: with Connect() as c:
try:
rank_list = RankList(c) rank_list = RankList(c)
rank_list.song.set_chart(request.args.get( rank_list.song.set_chart(request.args.get(
'song_id'), request.args.get('difficulty')) 'song_id'), request.args.get('difficulty'))
rank_list.select_me(UserOnline(c, user_id)) rank_list.select_me(UserOnline(c, user_id))
return success_return(rank_list.to_dict_list()) return success_return(rank_list.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/song/friend', methods=['GET']) # 好友排名默认最多50 @bp.route('/song/friend', methods=['GET']) # 好友排名默认最多50
@auth_required(request) @auth_required(request)
@arc_try
def song_score_friend(user_id): def song_score_friend(user_id):
with Connect() as c: with Connect() as c:
try:
rank_list = RankList(c) rank_list = RankList(c)
rank_list.song.set_chart(request.args.get( rank_list.song.set_chart(request.args.get(
'song_id'), request.args.get('difficulty')) 'song_id'), request.args.get('difficulty'))
rank_list.select_friend(UserOnline(c, user_id)) rank_list.select_friend(UserOnline(c, user_id))
return success_return(rank_list.to_dict_list()) return success_return(rank_list.to_dict_list())
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,27 +1,26 @@
from flask import Blueprint, current_app, request
from core.character import UserCharacter from core.character import UserCharacter
from core.error import ArcError, NoAccess from core.error import ArcError
from core.item import ItemCore from core.item import ItemCore
from core.save import SaveData from core.save import SaveData
from core.sql import Connect from core.sql import Connect
from core.user import User, UserLogin, UserOnline, UserRegister from core.user import User, UserLogin, UserOnline, UserRegister
from flask import Blueprint, request
from setting import Config
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, header_check, success_return
bp = Blueprint('user', __name__, url_prefix='/user') bp = Blueprint('user', __name__, url_prefix='/user')
@bp.route('', methods=['POST']) # 注册接口 @bp.route('', methods=['POST']) # 注册接口
@arc_try
def register(): def register():
if 'AppVersion' in request.headers: # 版本检查 error = header_check(request)
if Config.ALLOW_APPVERSION: if error is not None:
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION: raise error
return error_return(NoAccess('Wrong app version.', 1203))
with Connect() as c: with Connect() as c:
try:
new_user = UserRegister(c) new_user = UserRegister(c)
new_user.set_name(request.form['name']) new_user.set_name(request.form['name'])
new_user.set_password(request.form['password']) new_user.set_password(request.form['password'])
@@ -37,77 +36,63 @@ def register():
user = UserLogin(c) user = UserLogin(c)
user.login(new_user.name, new_user.password, user.login(new_user.name, new_user.password,
device_id, request.remote_addr) device_id, request.remote_addr)
current_app.logger.info(f'New user `{user.user_id}` registered')
return success_return({'user_id': user.user_id, 'access_token': user.token}) return success_return({'user_id': user.user_id, 'access_token': user.token})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me', methods=['GET']) # 用户信息 @bp.route('/me', methods=['GET']) # 用户信息
@auth_required(request) @auth_required(request)
@arc_try
def user_me(user_id): def user_me(user_id):
with Connect() as c: with Connect() as c:
try:
return success_return(UserOnline(c, user_id).to_dict()) return success_return(UserOnline(c, user_id).to_dict())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/character', methods=['POST']) # 角色切换 @bp.route('/me/character', methods=['POST']) # 角色切换
@auth_required(request) @auth_required(request)
@arc_try
def character_change(user_id): def character_change(user_id):
with Connect() as c: with Connect() as c:
try:
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
user.change_character( user.change_character(
int(request.form['character']), request.form['skill_sealed'] == 'true') int(request.form['character']), request.form['skill_sealed'] == 'true')
return success_return({'user_id': user.user_id, 'character': user.character.character_id}) return success_return({'user_id': user.user_id, 'character': user.character.character_id})
except ArcError as e:
return error_return(e)
return error_return()
# 角色觉醒切换 # 角色觉醒切换
@bp.route('/me/character/<int:character_id>/toggle_uncap', methods=['POST']) @bp.route('/me/character/<int:character_id>/toggle_uncap', methods=['POST'])
@auth_required(request) @auth_required(request)
@arc_try
def toggle_uncap(user_id, character_id): def toggle_uncap(user_id, character_id):
with Connect() as c: with Connect() as c:
try:
user = User() user = User()
user.user_id = user_id user.user_id = user_id
character = UserCharacter(c, character_id) character = UserCharacter(c, character_id)
character.change_uncap_override(user) character.change_uncap_override(user)
character.select_character_info(user) character.select_character_info(user)
return success_return({'user_id': user.user_id, 'character': [character.to_dict()]}) return success_return({'user_id': user.user_id, 'character': [character.to_dict()]})
except ArcError as e:
return error_return(e)
return error_return()
# 角色觉醒 # 角色觉醒
@bp.route('/me/character/<int:character_id>/uncap', methods=['POST']) @bp.route('/me/character/<int:character_id>/uncap', methods=['POST'])
@auth_required(request) @auth_required(request)
@arc_try
def character_first_uncap(user_id, character_id): def character_first_uncap(user_id, character_id):
with Connect() as c: with Connect() as c:
try:
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
character = UserCharacter(c, character_id) character = UserCharacter(c, character_id)
character.select_character_info(user) character.select_character_info(user)
character.character_uncap(user) character.character_uncap(user)
return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores}) return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
except ArcError as e:
return error_return(e)
return error_return()
# 角色使用以太之滴 # 角色使用以太之滴
@bp.route('/me/character/<int:character_id>/exp', methods=['POST']) @bp.route('/me/character/<int:character_id>/exp', methods=['POST'])
@auth_required(request) @auth_required(request)
@arc_try
def character_exp(user_id, character_id): def character_exp(user_id, character_id):
with Connect() as c: with Connect() as c:
try:
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
character = UserCharacter(c, character_id) character = UserCharacter(c, character_id)
character.select_character_info(user) character.select_character_info(user)
@@ -115,32 +100,26 @@ def character_exp(user_id, character_id):
core.amount = - int(request.form['amount']) core.amount = - int(request.form['amount'])
core.item_id = 'core_generic' core.item_id = 'core_generic'
character.upgrade_by_core(user, core) character.upgrade_by_core(user, core)
return success_return({'user_id': user.user_id, 'character': [character.to_dict], 'cores': user.cores}) return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/save', methods=['GET']) # 从云端同步 @bp.route('/me/save', methods=['GET']) # 从云端同步
@auth_required(request) @auth_required(request)
@arc_try
def cloud_get(user_id): def cloud_get(user_id):
with Connect() as c: with Connect() as c:
try:
user = User() user = User()
user.user_id = user_id user.user_id = user_id
save = SaveData(c) save = SaveData(c)
save.select_all(user) save.select_all(user)
return success_return(save.to_dict()) return success_return(save.to_dict())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/save', methods=['POST']) # 向云端同步 @bp.route('/me/save', methods=['POST']) # 向云端同步
@auth_required(request) @auth_required(request)
@arc_try
def cloud_post(user_id): def cloud_post(user_id):
with Connect() as c: with Connect() as c:
try:
user = User() user = User()
user.user_id = user_id user.user_id = user_id
save = SaveData(c) save = SaveData(c)
@@ -159,35 +138,36 @@ def cloud_post(user_id):
save.set_value( save.set_value(
'story_data', request.form['story_data'], request.form['story_checksum']) 'story_data', request.form['story_data'], request.form['story_checksum'])
save.set_value( save.set_value(
'finalestate_data', request.form['finalestate_data'], request.form['finalestate_checksum']) 'finalestate_data', request.form.get('finalestate_data'), request.form.get('finalestate_checksum'))
save.update_all(user) save.update_all(user)
return success_return({'user_id': user.user_id}) return success_return({'user_id': user.user_id})
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/setting/<set_arg>', methods=['POST']) # 三个设置 @bp.route('/me/setting/<set_arg>', methods=['POST']) # 三个设置
@auth_required(request) @auth_required(request)
@arc_try
def sys_set(user_id, set_arg): def sys_set(user_id, set_arg):
with Connect() as c: with Connect() as c:
try:
value = request.form['value'] value = request.form['value']
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
if 'favorite_character' == set_arg: if 'favorite_character' == set_arg:
user.change_favorite_character(int(value)) user.change_favorite_character(int(value))
else: else:
value = 'true' == value value = 'true' == value
if 'is_hide_rating' == set_arg or 'max_stamina_notification_enabled' == set_arg: if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'):
user.update_user_one_column(set_arg, value) user.update_user_one_column(set_arg, value)
return success_return(user.to_dict()) return success_return(user.to_dict())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/me/request_delete', methods=['POST']) # 删除账号 @bp.route('/me/request_delete', methods=['POST']) # 删除账号
@auth_required(request) @auth_required(request)
@arc_try
def user_delete(user_id): def user_delete(user_id):
return error_return(ArcError('Cannot delete the account.', 151)), 404 raise ArcError('Cannot delete the account.', 151, status=404)
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
@arc_try
def email_resend_verify():
raise ArcError('Email verification unavailable.', 151, status=404)

View File

@@ -1,20 +1,20 @@
from core.error import ArcError from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from core.world import UserMap, get_world_all from core.world import UserMap, get_world_all
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import error_return, success_return from .func import arc_try, success_return
bp = Blueprint('world', __name__, url_prefix='/world') bp = Blueprint('world', __name__, url_prefix='/world')
@bp.route('/map/me', methods=['GET']) # 获得世界模式信息,所有地图 @bp.route('/map/me', methods=['GET']) # 获得世界模式信息,所有地图
@auth_required(request) @auth_required(request)
@arc_try
def world_all(user_id): def world_all(user_id):
with Connect() as c: with Connect() as c:
try:
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
user.select_user_about_current_map() user.select_user_about_current_map()
return success_return({ return success_return({
@@ -22,29 +22,23 @@ def world_all(user_id):
"user_id": user_id, "user_id": user_id,
"maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in get_world_all(c, user)] "maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in get_world_all(c, user)]
}) })
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/map/me', methods=['POST']) # 进入地图 @bp.route('/map/me', methods=['POST']) # 进入地图
@auth_required(request) @auth_required(request)
@arc_try
def world_in(user_id): def world_in(user_id):
with Connect() as c: with Connect() as c:
try:
arcmap = UserMap(c, request.form['map_id'], UserOnline(c, user_id)) arcmap = UserMap(c, request.form['map_id'], UserOnline(c, user_id))
if arcmap.unlock(): if arcmap.unlock():
return success_return(arcmap.to_dict()) return success_return(arcmap.to_dict())
except ArcError as e:
return error_return(e)
return error_return()
@bp.route('/map/me/<map_id>', methods=['GET']) # 获得单个地图完整信息 @bp.route('/map/me/<map_id>', methods=['GET']) # 获得单个地图完整信息
@auth_required(request) @auth_required(request)
@arc_try
def world_one(user_id, map_id): def world_one(user_id, map_id):
with Connect() as c: with Connect() as c:
try:
arcmap = UserMap(c, map_id, UserOnline(c, user_id)) arcmap = UserMap(c, map_id, UserOnline(c, user_id))
arcmap.change_user_current_map() arcmap.change_user_current_map()
return success_return({ return success_return({
@@ -52,6 +46,3 @@ def world_one(user_id, map_id):
"current_map": map_id, "current_map": map_id,
"maps": [arcmap.to_dict(has_map_info=True, has_steps=True)] "maps": [arcmap.to_dict(has_map_info=True, has_steps=True)]
}) })
except ArcError as e:
return error_return(e)
return error_return()

View File

@@ -1,11 +0,0 @@
class Config:
TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000
COUNTDOWM_TIME = 3999
PLAYER_PRE_TIMEOUT = 3000000
PLAYER_TIMEOUT = 20000000
LINK_PLAY_UNLOCK_LENGTH = 512

View File

@@ -1,223 +0,0 @@
import socketserver
import threading
import os
import random
import time
# import binascii
from . import aes
from .udp_parser import CommandParser
from .udp_class import Room, Player, bi
from .udp_config import Config
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
link_play_data = {}
room_id_dict = {} # 'room_id': Room
room_code_dict = {} # 'room_code': Room
player_dict = {} # 'player_id' : Player
clean_timer = 0
def random_room_code():
# 随机生成房间号
re = ''
for _ in range(4):
re += chr(random.randint(65, 90))
for _ in range(2):
re += str(random.randint(0, 9))
return re
def clear_player(token):
# 清除玩家信息和token
del player_dict[link_play_data[token]['player_id']]
del link_play_data[token]
def clear_room(room):
# 清除房间信息
room_id = room.room_id
room_code = room.room_code
del room_id_dict[room_id]
del room_code_dict[room_code]
del room
def memory_clean(now):
# 内存清理
clean_room_list = []
clean_player_list = []
for token in link_play_data:
room = link_play_data[token]['room']
if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id)
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token)
for room_id in room_id_dict:
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id)
for room_id in clean_room_list:
if room_id in room_id_dict:
clear_room(room_id_dict[room_id])
for token in clean_player_list:
clear_player(token)
class UDPhandler(socketserver.BaseRequestHandler):
def handle(self):
client_msg, server = self.request
token = client_msg[:8]
iv = client_msg[8:20]
tag = client_msg[20:32]
ciphertext = client_msg[32:]
if int.from_bytes(token, byteorder='little') in link_play_data:
user = link_play_data[bi(token)]
else:
return None
plaintext = aes.decrypt(user['key'], b'', iv, ciphertext, tag)
# print(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))
temp = []
for i in commands:
if i[:3] == b'\x06\x16\x12':
temp.append(i)
commands = temp
# 处理不能正确被踢的问题
for i in commands:
iv, ciphertext, tag = aes.encrypt(user['key'], i, b'')
# print(binascii.b2a_hex(i))
server.sendto(token + iv + tag[:12] +
ciphertext, self.client_address)
def server_run(ip, port):
server = socketserver.ThreadingUDPServer((ip, port), UDPhandler)
server.serve_forever()
def data_swap(conn):
clean_timer = 0
while True:
try:
data = conn.recv()
except EOFError:
break
now = round(time.time() * 1000)
if now - clean_timer >= Config.TIME_LIMIT:
clean_timer = now
memory_clean(now)
if data[0] == 1:
# 开房
key = os.urandom(16)
room_id = bi(os.urandom(8))
while room_id in room_id_dict and room_id == 0:
room_id = bi(os.urandom(8))
room = Room()
room.room_id = room_id
room_id_dict[room_id] = room
player_id = bi(os.urandom(3))
while player_id in player_dict and player_id == 0:
player_id = bi(os.urandom(3))
player = Player()
player.player_id = player_id
player.set_player_name(data[1])
player_dict[player_id] = player
player.song_unlock = data[2]
room.song_unlock = data[2]
room.host_id = player_id
room.players[0] = player
room.player_num = 1
room_code = random_room_code()
while room_code in room_code_dict:
room_code = random_room_code()
room.room_code = room_code
room_code_dict[room_code] = room
token = room_id
player.token = token
link_play_data[token] = {'key': key,
'room': room,
'player_index': 0,
'player_id': player_id}
conn.send((0, room_code, room_id, token, key, player_id))
elif data[0] == 2:
room_code = data[3].upper()
if room_code not in room_code_dict:
# 房间号错误
conn.send((1202, ))
else:
room = room_code_dict[room_code]
if room.player_num == 4:
# 满人
conn.send((1201, ))
elif room.state != 2:
# 无法加入
conn.send((1205, ))
else:
key = os.urandom(16)
token = bi(os.urandom(8))
player_id = bi(os.urandom(3))
while player_id in player_dict and player_id == 0:
player_id = bi(os.urandom(3))
player = Player()
player.player_id = player_id
player.set_player_name(data[1])
player.token = token
player_dict[player_id] = player
player.song_unlock = data[2]
room.update_song_unlock()
for i in range(4):
if room.players[i].player_id == 0:
room.players[i] = player
player_index = i
break
link_play_data[token] = {'key': key,
'room': room,
'player_index': player_index,
'player_id': player_id}
conn.send((0, room_code, room.room_id,
token, key, player_id, room.song_unlock))
elif data[0] == 3:
token = data[1]
if token in link_play_data:
r = link_play_data[token]
conn.send((0, r['room'].room_code, r['room'].room_id, r['key'],
r['room'].players[r['player_index']].player_id, r['room'].song_unlock))
else:
conn.send((108, ))
def link_play(conn, ip: str, port: int):
try:
server = threading.Thread(target=server_run, args=(ip, port))
data_exchange = threading.Thread(target=data_swap, args=(conn,))
server.start()
data_exchange.start()
except:
pass

View File

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

View File

@@ -1,10 +1,11 @@
import os import os
import time import time
import server.arcscore from core.init import FileChecker
from core.download import initialize_songfile from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem
from core.rank import RankList from core.rank import RankList
from core.sql import Connect from core.sql import Connect
from core.user import User
from flask import Blueprint, flash, redirect, render_template, request, url_for from flask import Blueprint, flash, redirect, render_template, request, url_for
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -270,7 +271,8 @@ def update_database():
file.save(os.path.join(UPLOAD_FOLDER, filename)) file.save(os.path.join(UPLOAD_FOLDER, filename))
flash('上传成功 Success upload.') flash('上传成功 Success upload.')
try: try:
web.system.update_database() FileChecker.update_database(
os.path.join(UPLOAD_FOLDER, filename))
flash('数据更新成功 Success update data.') flash('数据更新成功 Success update data.')
except: except:
flash('数据更新失败 Cannot update data.') flash('数据更新失败 Cannot update data.')
@@ -288,7 +290,7 @@ def update_database():
def update_song_hash(): def update_song_hash():
# 更新数据库内谱面文件hash值 # 更新数据库内谱面文件hash值
try: try:
initialize_songfile() RefreshSongFileCache().run()
flash('数据刷新成功 Success refresh data.') flash('数据刷新成功 Success refresh data.')
except: except:
flash('Something error!') flash('Something error!')
@@ -299,10 +301,7 @@ def update_song_hash():
@login_required @login_required
def update_song_rating(): def update_song_rating():
# 更新所有分数的rating # 更新所有分数的rating
error = server.arcscore.refresh_all_score_rating() RefreshAllScoreRating().run()
if error:
flash(error)
else:
flash('数据刷新成功 Success refresh data.') flash('数据刷新成功 Success refresh data.')
return render_template('web/updatedatabase.html') return render_template('web/updatedatabase.html')
@@ -423,7 +422,7 @@ def all_character():
def change_character(): def change_character():
# 修改角色数据 # 修改角色数据
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion'] 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
return render_template('web/changechar.html', skill_ids=skill_ids) return render_template('web/changechar.html', skill_ids=skill_ids)
@@ -607,7 +606,7 @@ def edit_user_purchase():
if 'name' not in request.form and 'user_code' not in request.form: if 'name' not in request.form and 'user_code' not in request.form:
flag = False flag = False
if method == '0': if method == '0':
web.system.unlock_all_user_item(c) UnlockUserItem().run()
else: else:
c.execute( c.execute(
'''delete from user_item where type in ('pack', 'single')''') '''delete from user_item where type in ('pack', 'single')''')
@@ -633,7 +632,9 @@ def edit_user_purchase():
user_id = user_id[0] user_id = user_id[0]
if method == '0': if method == '0':
web.system.unlock_user_item(c, user_id) x = UnlockUserItem()
x.set_params(user_id=user_id)
x.run()
else: else:
c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', { c.execute('''delete from user_item where type in ('pack', 'single') and user_id = :user_id''', {
'user_id': user_id}) 'user_id': user_id})
@@ -697,7 +698,7 @@ def change_item():
c.execute( c.execute(
'''select exists(select * from item where item_id=:a and type=:b)''', {'a': item_id, 'b': item_type}) '''select exists(select * from item where item_id=:a and type=:b)''', {'a': item_id, 'b': item_type})
if c.fetchone() == (0,): if c.fetchone() == (0,):
c.execute('''insert into item values(?,?,?,'')''', c.execute('''insert into item values(?,?,?)''',
(item_id, item_type, is_available)) (item_id, item_type, is_available))
flash('物品添加成功 Successfully add the item.') flash('物品添加成功 Successfully add the item.')
else: else:
@@ -939,7 +940,7 @@ def update_user_save():
# 全修改 # 全修改
if 'name' not in request.form and 'user_code' not in request.form: if 'name' not in request.form and 'user_code' not in request.form:
flag = False flag = False
web.system.update_all_save(c) SaveUpdateScore().run()
flash("全部用户存档同步成功 Successfully update all users' saves.") flash("全部用户存档同步成功 Successfully update all users' saves.")
else: else:
@@ -959,7 +960,9 @@ def update_user_save():
user_id = c.fetchone() user_id = c.fetchone()
if user_id: if user_id:
user_id = user_id[0] user_id = user_id[0]
web.system.update_one_save(c, user_id) user = User()
user.user_id = user_id
SaveUpdateScore(user).run()
flash("用户存档同步成功 Successfully update the user's saves.") flash("用户存档同步成功 Successfully update the user's saves.")
else: else:

View File

@@ -1,10 +1,10 @@
#import sqlite3
from flask import (Blueprint, flash, g, redirect,
render_template, request, session, url_for)
import functools import functools
from setting import Config
import hashlib import hashlib
from core.config_manager import Config
from flask import (Blueprint, flash, g, redirect, render_template, request,
session, url_for)
bp = Blueprint('login', __name__, url_prefix='/web') bp = Blueprint('login', __name__, url_prefix='/web')

View File

@@ -1,9 +1,8 @@
import os
from core.sql import Connect
import time
import hashlib import hashlib
import time
from random import Random from random import Random
from setting import Config
from core.sql import Connect
def int2b(x): def int2b(x):
@@ -25,139 +24,6 @@ def random_str(randomlength=10):
return s return s
def get_table_info(c, table_name):
# 得到表结构,返回主键列表和字段名列表
pk = []
name = []
c.execute('''pragma table_info ("'''+table_name+'''")''')
x = c.fetchall()
if x:
for i in x:
name.append(i[1])
if i[5] != 0:
pk.append(i[1])
return pk, name
def get_sql_select_table(table_name, get_field, where_field=[], where_value=[]):
# sql语句拼接select ... from ... where ...
sql = 'select '
sql_dict = {}
if len(get_field) >= 2:
sql += get_field[0]
for i in range(1, len(get_field)):
sql += ',' + get_field[i]
sql += ' from ' + table_name
elif len(get_field) == 1:
sql += get_field[0] + ' from ' + table_name
else:
sql += '* from ' + table_name
if where_field and where_value:
sql += ' where '
sql += where_field[0] + '=:' + where_field[0]
sql_dict[where_field[0]] = where_value[0]
if len(where_field) >= 1:
for i in range(1, len(where_field)):
sql_dict[where_field[i]] = where_value[i]
sql += ' and ' + where_field[i] + '=:' + where_field[i]
sql += ' order by rowid'
return sql, sql_dict
def get_sql_insert_table(table_name, field, value):
# sql语句拼接insert into ...(...) values(...)
sql = 'insert into ' + table_name + '('
sql_dict = {}
sql2 = ''
if len(field) >= 2:
sql += field[0]
sql2 += ':' + field[0]
sql_dict[field[0]] = value[0]
for i in range(1, len(field)):
sql += ',' + field[i]
sql2 += ', :' + field[i]
sql_dict[field[i]] = value[i]
sql += ') values('
elif len(field) == 1:
sql += field[0] + ') values('
sql2 += ':' + field[0]
sql_dict[field[0]] = value[0]
else:
return 'error', {}
sql += sql2 + ')'
return sql, sql_dict
def get_sql_delete_table(table_name, where_field=[], where_value=[]):
# sql语句拼接delete from ... where ...
sql = 'delete from ' + table_name
sql_dict = {}
if where_field and where_value:
sql += ' where '
sql += where_field[0] + '=:' + where_field[0]
sql_dict[where_field[0]] = where_value[0]
if len(where_field) >= 1:
for i in range(1, len(where_field)):
sql_dict[where_field[i]] = where_value[i]
sql += ' and ' + where_field[i] + '=:' + where_field[i]
return sql, sql_dict
def update_one_table(c1, c2, table_name):
# 从c1向c2更新数据表c1中存在的信息不变
c1.execute(
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
c2.execute(
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
if not c1.fetchone() or not c2.fetchone():
return 'error'
db1_pk, db1_name = get_table_info(c1, table_name)
db2_pk, db2_name = get_table_info(c2, table_name)
if db1_pk != db2_pk:
return 'error'
field = []
for i in db1_name:
if i in db2_name:
field.append(i)
sql, sql_dict = get_sql_select_table(table_name, db1_pk)
c1.execute(sql)
x = c1.fetchall()
sql, sql_dict = get_sql_select_table(table_name, field)
c1.execute(sql)
y = c1.fetchall()
if x:
for i in range(0, len(x)):
sql, sql_dict = get_sql_select_table(
table_name, [], db1_pk, list(x[i]))
sql = 'select exists(' + sql + ')'
c2.execute(sql, sql_dict)
if c2.fetchone() == (1,): # 如果c2里存在先删除
sql, sql_dict = get_sql_delete_table(
table_name, db1_pk, list(x[i]))
c2.execute(sql, sql_dict)
sql, sql_dict = get_sql_insert_table(
table_name, field, list(y[i]))
c2.execute(sql, sql_dict)
return None
def update_user_char(c): def update_user_char(c):
# 用character数据更新user_char # 用character数据更新user_char
c.execute('''select character_id, max_level, is_uncapped from character''') c.execute('''select character_id, max_level, is_uncapped from character''')
@@ -174,99 +40,6 @@ def update_user_char(c):
(j[0], i[0], i[1], exp, i[2], 0)) (j[0], i[0], i[1], exp, i[2], 0))
def update_user_epilogue(c):
c.execute('''select user_id from user''')
x = c.fetchall()
for i in x:
c.execute(
'''select exists(select * from user_item where user_id=? and item_id=? and type='pack')''', (i[0], 'epilogue'))
if c.fetchone() == (0,):
c.execute('''insert into user_item values(?,?,'pack',1)''',
(i[0], 'epilogue'))
def update_database():
# 将old数据库不存在数据加入到新数据库上并删除old数据库
# 对于arcaea_datebase.db更新一些表并用character数据更新user_char_full
if os.path.isfile("database/old_arcaea_database.db") and os.path.isfile("database/arcaea_database.db"):
with Connect('./database/old_arcaea_database.db') as c1:
with Connect() as c2:
update_one_table(c1, c2, 'user')
update_one_table(c1, c2, 'friend')
update_one_table(c1, c2, 'best_score')
update_one_table(c1, c2, 'recent30')
update_one_table(c1, c2, 'user_world')
update_one_table(c1, c2, 'item')
update_one_table(c1, c2, 'user_item')
update_one_table(c1, c2, 'purchase')
update_one_table(c1, c2, 'purchase_item')
update_one_table(c1, c2, 'user_save')
update_one_table(c1, c2, 'login')
update_one_table(c1, c2, 'present')
update_one_table(c1, c2, 'user_present')
update_one_table(c1, c2, 'present_item')
update_one_table(c1, c2, 'redeem')
update_one_table(c1, c2, 'user_redeem')
update_one_table(c1, c2, 'redeem_item')
update_one_table(c1, c2, 'role')
update_one_table(c1, c2, 'user_role')
update_one_table(c1, c2, 'power')
update_one_table(c1, c2, 'role_power')
update_one_table(c1, c2, 'api_login')
update_one_table(c1, c2, 'chart')
update_one_table(c1, c2, 'user_course')
update_one_table(c1, c2, 'course')
update_one_table(c1, c2, 'course_item')
update_one_table(c1, c2, 'course_chart')
update_one_table(c1, c2, 'course_requirement')
update_one_table(c1, c2, 'user_char')
if not Config.UPDATE_WITH_NEW_CHARACTER_DATA:
update_one_table(c1, c2, 'character')
update_user_char(c2) # 更新user_char_full
update_user_epilogue(c2) # 更新user的epilogue
os.remove('database/old_arcaea_database.db')
def unlock_all_user_item(c):
# 解锁所有用户购买
c.execute('''select user_id from user''')
x = c.fetchall()
c.execute('''select item_id, type from purchase_item''')
y = c.fetchall()
if x and y:
for i in x:
for j in y:
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
'a': i[0], 'b': j[0], 'c': j[1]})
if c.fetchone() == (0,) and j[1] != 'character':
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
'a': i[0], 'b': j[0], 'c': j[1]})
return
def unlock_user_item(c, user_id):
# 解锁用户购买
c.execute('''select item_id, type from purchase_item''')
y = c.fetchall()
for j in y:
c.execute('''select exists(select * from user_item where user_id=:a and item_id=:b and type=:c)''', {
'a': user_id, 'b': j[0], 'c': j[1]})
if c.fetchone() == (0,) and j[1] != 'character':
c.execute('''insert into user_item values(:a,:b,:c,1)''', {
'a': user_id, 'b': j[0], 'c': j[1]})
return
def get_all_item(): def get_all_item():
# 所有物品数据查询 # 所有物品数据查询
with Connect() as c: with Connect() as c:
@@ -324,61 +97,6 @@ def get_all_purchase():
return re return re
def update_one_save(c, user_id):
# 同步指定用户存档
# 注意best_score表不比较直接覆盖
return
# c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', {
# 'a': user_id})
# x = c.fetchone()
# if x:
# scores = json.loads(x[0])[""]
# clearlamps = json.loads(x[1])[""]
# clear_song_id_difficulty = []
# clear_state = []
# for i in clearlamps:
# clear_song_id_difficulty.append(i['song_id']+str(i['difficulty']))
# clear_state.append(i['clear_type'])
# for i in scores:
# rating = server.arcscore.get_one_ptt(
# i['song_id'], i['difficulty'], i['score'])
# if rating < 0:
# rating = 0
# try:
# index = clear_song_id_difficulty.index(
# i['song_id'] + str(i['difficulty']))
# except:
# index = -1
# if index != -1:
# clear_type = clear_state[index]
# else:
# clear_type = 0
# c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', {
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty']})
# c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', {
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating})
# ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT
# c.execute('''update user set rating_ptt=:a where user_id=:b''', {
# 'a': ptt, 'b': user_id})
# return
def update_all_save(c):
# 同步所有用户存档
c.execute('''select user_id from user_save''')
x = c.fetchall()
if x:
for i in x:
update_one_save(c, i[0])
return
def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount): def add_one_present(present_id, expire_ts, description, item_id, item_type, item_amount):
# 添加一个奖励 # 添加一个奖励

121
tools/clean_logdb.py Normal file
View File

@@ -0,0 +1,121 @@
import os
import sqlite3
from datetime import datetime
from time import mktime
LOG_DATABASE_PATH = './arcaea_log.db'
class Connect():
# 数据库连接类,上下文管理
def __init__(self, file_path=LOG_DATABASE_PATH):
"""
数据库连接
接受:文件路径
返回sqlite3连接操作对象
"""
self.file_path = file_path
def __enter__(self):
self.conn = sqlite3.connect(self.file_path)
self.c = self.conn.cursor()
return self.c
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
if exc_type is not None:
print(f'exc_type: {exc_type}')
print(f'exc_val: {exc_val}')
print(f'exc_tb: {exc_tb}')
if self.conn:
self.conn.rollback()
if self.conn:
self.conn.commit()
self.conn.close()
return True
def clean_by_time(table_name: str, time_colume_name: str, colume_count: int):
with Connect() as c:
today = datetime.now()
print(f'The time now is {today}.')
day = input(
'Please input the number of days the data before which you want to delete: ')
try:
day = int(day)
except ValueError:
print('Invalid input!')
return
time = mktime(today.timetuple()) - day * 24 * 60 * 60
delete_count = c.execute(
f'select count(*) from {table_name} where {time_colume_name} < ?', (time,)).fetchone()[0]
all_count = c.execute(
f'select count(*) from {table_name}').fetchone()[0]
print(
f'Before {day} days, there are {delete_count} records to be deleted, {all_count} records in total.')
flag = input('Are you sure to delete these records? (y/n) ')
if flag == 'y' or flag == 'Y':
if delete_count >= 1000000:
print(
'It will cost a long time to delete these records, please wait patiently...')
print('Deleting...')
c.execute('PRAGMA cache_size = 32768')
c.execute('PRAGMA synchronous = OFF')
c.execute('PRAGMA temp_store = MEMORY')
if delete_count / all_count >= 0.8:
data = c.execute(
f'select * from {table_name} where {time_colume_name} > ?', (time,)).fetchall()
c.execute(f'delete from {table_name}')
c.executemany(
f'insert into {table_name} values ({",".join(["?"]*colume_count)})', data)
else:
c.execute(
f'delete from {table_name} where {time_colume_name} < ?', (time,))
c.execute('PRAGMA temp_store = DEFAULT')
print('Delete successfully!')
else:
print('Delete canceled!')
def vacuum():
print('This operation will release unused space in the database file.')
print('It will cost a long time to release unused space if the database file is so large.')
flag = input('Are you sure to release unused space? (y/n) ')
if flag == 'y' or flag == 'Y':
with Connect() as c:
print('Releasing unused space...')
c.execute('vacuum')
print('Release unused space successfully!')
else:
print('Release unused space canceled!')
def main():
if not os.path.exists(LOG_DATABASE_PATH):
print('The database file `arcaea_log.db` does not exist!')
print('-- Arcaea Server Log Database Cleaner --')
print('Note: It is more recommended to delete the database file directly.')
while True:
print('-' * 40)
print('1. clean `user_score` table')
print('2. clean `user_rating` table')
print('3. release unused space (`vacuum` command)')
print('0. exit')
choice = input('Please input your choice: ')
if choice == '1':
clean_by_time('user_score', 'time_played', 13)
elif choice == '2':
clean_by_time('user_rating', 'time', 3)
elif choice == '3':
vacuum()
elif choice == '0':
break
else:
print('Invalid choice!')
if __name__ == '__main__':
main()
input('Press `Enter` key to exit.')

View File

@@ -1,11 +1,17 @@
import os
import sqlite3 import sqlite3
SONG_DATABASE_PATH = './arcsong.db'
SERVER_DATABASE_PATH = './arcaea_database.db'
class Connect(): class Connect():
# 数据库连接类,上下文管理 # 数据库连接类,上下文管理
def __init__(self, file_path='./arcaea_database.db'): def __init__(self, file_path=SERVER_DATABASE_PATH):
""" """
数据库连接默认连接arcaea_database.db\ 数据库连接\
接受:文件路径\ 接受:文件路径\
返回sqlite3连接操作对象 返回sqlite3连接操作对象
""" """
@@ -28,33 +34,81 @@ class Connect():
return True return True
def insert(cursor, song_id, name, a, b, c, d): def insert(cursor, song_id, name, a, b, c, d, update_type=0):
'''Insert a new song into database.''' '''Insert a new song into database.'''
if update_type == 0 or update_type == 1:
cursor.execute( cursor.execute(
'''select exists(select * from chart where song_id=?)''', (song_id, )) '''select exists(select * from chart where song_id=?)''', (song_id, ))
if cursor.fetchone()[0]: if cursor.fetchone()[0]:
return None if update_type == 0:
# 重复则不更新,以服务端数据库数据为准
return
elif update_type == 1:
# 重复则更新,以`arcsong.db`数据为准
cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''',
(name, a, b, c, d, song_id))
return
cursor.execute( cursor.execute(
'''insert into chart values (?,?,?,?,?,?)''', (song_id, name, a, b, c, d)) '''insert into chart values (?,?,?,?,?,?)''', (song_id, name, a, b, c, d))
def old_to_new(): def from_song_datebase():
'''Update old database to new database.''' '''Get song data from song database and insert them into server's database.'''
with Connect('./arcsong.db') as c: with Connect(SONG_DATABASE_PATH) as c:
c.execute('''select name from sqlite_master where type="table"''')
tables = [x[0] for x in c.fetchall()]
if 'songs' in tables:
c.execute( c.execute(
'''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''') '''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''')
data = c.fetchall() data = c.fetchall()
elif 'charts' in tables:
c.execute(
'''select song_id, rating_class, name_en, rating from charts''')
songs = {}
for song_id, rating_class, name_en, rating in c.fetchall():
if song_id not in songs:
songs[song_id] = [-1, -1, -1, -1, name_en]
songs[song_id][rating_class] = rating
data = [(x, y[-1], y[0], y[1], y[2], y[3])
for x, y in songs.items()]
else:
print('Error: Cannot find table `songs` or `charts` in the database.')
return
# 用户确认更新方式
update_type = 0
x = input('Type a number to decide the update type:\n0: Do not update if the song already exists in the server database.\n1: Update even if the song already exists in the server database.\n2: Clear chart data in the server database and then update.\nYour choice: ')
x = x.strip()
if x not in ('0', '1', '2'):
print('Error: Invalid input.')
return
update_type = int(x)
with Connect() as c: with Connect() as c:
if update_type == 2:
# 清空数据表后更新
c.execute('''delete from chart''')
for x in data: for x in data:
insert(c, x[0], x[1], x[2], x[3], x[4], x[5]) insert(c, x[0], x[1], x[2], x[3], x[4], x[5], update_type)
print('Seems to be done.')
def check_file():
if not os.path.isfile(SONG_DATABASE_PATH) or not os.path.isfile(SERVER_DATABASE_PATH):
print('Error: Files cannot be found.')
print('Note: Please make sure that both `arcsong.db` and `arcaea_server.db` are in this directory.')
return False
return True
def main(): def main():
old_to_new() if check_file():
from_song_datebase()
if __name__ == '__main__': if __name__ == '__main__':
main() main()
print('Done.') input('Press `Enter` key to exit.')
input()
exit()