mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-08 16:57:26 +08:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd9eb28832 | ||
|
|
e3d5c19569 | ||
|
|
e1ce4d9ec4 | ||
|
|
b70bfd6081 | ||
|
|
7cc17181e1 | ||
|
|
e22485e4e0 | ||
|
|
65aed56c3e | ||
|
|
e6cb0f8981 | ||
|
|
4a5083c597 | ||
|
|
78d28f8886 | ||
|
|
3fde61c639 | ||
|
|
cf350842ac | ||
|
|
e04c8ecf2d | ||
|
|
4da3f38f6e | ||
|
|
40630fff4d | ||
|
|
ca03360f0c | ||
|
|
d150553e6b | ||
|
|
ba36190f30 | ||
|
|
68a83a29d2 | ||
|
|
2a3ee0f0c7 | ||
|
|
a04df8bba6 | ||
|
|
fb110fd01c | ||
|
|
7a614eaa57 | ||
|
|
8f1c28eb5c | ||
|
|
299c8c582c | ||
|
|
6298b37ea2 | ||
|
|
b1575af0db | ||
|
|
7bf43c3e07 | ||
|
|
12cd187563 | ||
|
|
574d2c9d9f | ||
|
|
732947a3f0 | ||
|
|
27ee147989 | ||
|
|
b38dbf1a55 | ||
|
|
d852be8029 | ||
|
|
4f52df35ce | ||
|
|
cbd2198e43 | ||
|
|
cdf76fb93a | ||
|
|
5fba93524c | ||
|
|
93f4ad4999 | ||
|
|
47f05cdf1e | ||
|
|
dbfed37dda | ||
|
|
6801af197f | ||
|
|
210ba99606 | ||
|
|
0705c35be8 | ||
|
|
c9bcbb8ec4 | ||
|
|
ac0071bfd6 | ||
|
|
ef8e551261 | ||
|
|
55ab6c43cb | ||
|
|
15134173c9 | ||
|
|
9f41ef0a70 | ||
|
|
af3e91b3e8 | ||
|
|
9d746eab2d | ||
|
|
6fcca17918 | ||
|
|
9de62d3645 | ||
|
|
96fbd26342 | ||
|
|
aa77a4c903 | ||
|
|
6075bda5d8 | ||
|
|
409c2c032b | ||
|
|
e8c15f5069 | ||
|
|
35c34f25d5 | ||
|
|
4b8c5a520e | ||
|
|
1235733ddf | ||
|
|
cb6425a0d1 | ||
|
|
3a1c731e24 | ||
|
|
5de274fdf5 | ||
|
|
b60457c38b | ||
|
|
35b954e549 | ||
|
|
ceebba4664 | ||
|
|
5bc9b9a3dc | ||
|
|
d9a543bc5a | ||
|
|
4666c9c153 | ||
|
|
32bcbb0ccd | ||
|
|
34497d0638 | ||
|
|
23bf3c020f | ||
|
|
e1fc1bbcd1 | ||
|
|
355ec3557f | ||
|
|
cc4ac192e7 | ||
|
|
5068c7215c | ||
|
|
99f05cabb7 | ||
|
|
1a097bc4d0 | ||
|
|
849f4f7260 | ||
|
|
add81ee639 | ||
|
|
b927ad23f8 | ||
|
|
8bc74b36c7 | ||
|
|
7dc8bfea9a | ||
|
|
d0be49fe20 | ||
|
|
b494b93c14 | ||
|
|
a523ff5aae | ||
|
|
acbe9fc4a1 | ||
|
|
9422bf7182 | ||
|
|
53acad4ffb | ||
|
|
13048e57f4 | ||
|
|
39d3f073ba | ||
|
|
2cf6595478 | ||
|
|
7c3bc99570 | ||
|
|
168e5f0b12 | ||
|
|
9286caf900 | ||
|
|
b0647bedb6 | ||
|
|
f49a868707 | ||
|
|
28bca25092 | ||
|
|
0227d2b44a | ||
|
|
0d95b886bc | ||
|
|
9b61acd163 | ||
|
|
be134bbc6e | ||
|
|
c8d0463b9e | ||
|
|
871c2082a8 | ||
|
|
14b685850c | ||
|
|
3fbf473de2 | ||
|
|
8a3cec05b7 | ||
|
|
e3d81436d0 | ||
|
|
941a79ccc7 | ||
|
|
62902a561c | ||
|
|
1660bc5899 | ||
|
|
d577246d28 | ||
|
|
9fd626468e | ||
|
|
c51a1b4e85 | ||
|
|
0e42123417 | ||
|
|
e216e6144b | ||
|
|
b70c435a4a | ||
|
|
eb03d2be93 | ||
|
|
fcb5d264ef | ||
|
|
d617951e1f | ||
|
|
febf931f1a | ||
|
|
981961d6a1 | ||
|
|
a90380df4c | ||
|
|
4f66f492fc | ||
|
|
df181f104e | ||
|
|
a890a9af4a | ||
|
|
c6bb4c9afb | ||
|
|
98358ad216 | ||
|
|
e0e6128826 | ||
|
|
08872399cc | ||
|
|
29a6cfaac1 | ||
|
|
f25bd4130e | ||
|
|
68c757f9d7 | ||
|
|
26216bf84a | ||
|
|
5771d0c669 | ||
|
|
80122d749e | ||
|
|
5c02ecdf37 | ||
|
|
dfcbb6f9c0 | ||
|
|
30690b2fbf | ||
|
|
56ae7047fc | ||
|
|
f5d2589971 | ||
|
|
8bb5c28663 | ||
|
|
123554e24d | ||
|
|
bb7ba249e9 | ||
|
|
3ecefcb802 | ||
|
|
798a838ca4 |
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use a clear and concise title 能不能来个正常点的标题 -->
|
||||
<!-- English or Chinese is ok 中英皆可 -->
|
||||
|
||||
**Describe the bug 问题描述**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce 复现过程**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Error log 报错日志**
|
||||
<!-- Including logs, texts in CMD, some messages in some softwares... -->
|
||||
```
|
||||
[Error] Balabalabalabala...
|
||||
```
|
||||
|
||||
**Screenshots 截图**
|
||||
<!-- Well, if you have error logs, pictures are optional. It is too slow for me to open a picture. -->
|
||||
<!-- 有错误日志就别放图了,网速有多慢你懂的 -->
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment 环境**
|
||||
- Server OS: [e.g. Windows 11 Beta]
|
||||
- Client OS: [e.g. Android 10]
|
||||
- Server Version: [e.g. v2.8.1]
|
||||
- Client Version [e.g. 4.0.256c]
|
||||
- Others like python version, flask version, some proxy softwares if you used...
|
||||
|
||||
**Additional context 附加内容**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
14
.github/ISSUE_TEMPLATE/q-a.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/q-a.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Q&A
|
||||
about: Ask a question or wonder something
|
||||
title: ''
|
||||
labels: Q&A
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What you want to know 你想知道什么**
|
||||
Clearly describe your question here.
|
||||
|
||||
**Additional context 附加内容**
|
||||
Add anything other here. Your abilities or knowledge base, your contact information, some pictures...
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# log files
|
||||
*.log
|
||||
*.log.*
|
||||
|
||||
# SSL cert
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# sqlite3 database
|
||||
*.db
|
||||
*.db.bak*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# setting/config files
|
||||
latest version/config/
|
||||
latest version/config.py
|
||||
135
README.md
135
README.md
@@ -2,60 +2,108 @@
|
||||
一个微型的Arcaea本地服务器 A small local server for Arcaea
|
||||
|
||||
## 简介 Introduction
|
||||
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能,一时兴起在五天之内糊了出来。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||
This is a small local Arcaea server based on Python and Flask, which can simulate the main functions of the game. I completed it on the spur of the moment in five days. This may be the first time I have written such a large program. Please understand if there is something wrong with it.
|
||||
这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。
|
||||
|
||||
本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。
|
||||
|
||||
This is a small local Arcaea server based on Python and Flask, which can simulate the main functions of the game. This may be the first time I have written such a large program. Please understand if there is something wrong with it.
|
||||
|
||||
This procedure is mainly used for study and research, and shall not be used for any commercial activities, otherwise the consequences will be borne by oneself. This is not a mandatory requirement, just a reminder and warning.
|
||||
|
||||
> 虽然看起来很蠢,但是可以用!
|
||||
> It looks stupid, but it works!
|
||||
|
||||
## 特性 Features
|
||||
有以下 We have:
|
||||
- 登录、注册 Login and registration
|
||||
- 多设备登录 Multi device login
|
||||
- 成绩上传 Score upload
|
||||
- PTT
|
||||
- 世界排名 Global rank
|
||||
- 排名 Rank
|
||||
- 段位系统 Course system
|
||||
- Link Play
|
||||
- 好友系统 Friends
|
||||
- 数据同步 Data synchronization
|
||||
- 爬梯 Climbing steps
|
||||
- 自定义世界模式 Customizable World Mode
|
||||
- 自定义歌曲下载 Customizable songs download
|
||||
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
|
||||
- 奖励系统 Present system
|
||||
- 兑换码系统 Redeem code system
|
||||
- 角色系统 Character system
|
||||
- 全剧情解锁 Unlock all the stories
|
||||
- 后台查询 Background search
|
||||
- 后台自定义信息 Customize some things in the background
|
||||
- 成绩校验 Score check
|
||||
- 下载校验 Download check
|
||||
- 服务器日志 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
|
||||
只是很有趣,用处探索中。
|
||||
It is just so interesting. What it can do is under exploration.
|
||||
|
||||
|
||||
## 下载 Download
|
||||
[这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases)
|
||||
|
||||
[Arcaea-CN official](https://arcaea.lowiro.com/zh)
|
||||
[Arcaea-lowi.ro](https://lowi.ro)
|
||||
[Arcaea-RhyDown](https://rhydown.com)
|
||||
|
||||
## 更新日志 Update log
|
||||
### Version 1.3
|
||||
- 适用于Arcaea 3.2.1版本 For Arcaea 3.2.1
|
||||
- 新增了可自定义的世界模式 Add customizable World Mode.
|
||||
- 对于官方版本,解锁几乎所有歌曲 For the official version, unlock almost all songs.
|
||||
只保留最新版本 Only keep the latest version.
|
||||
|
||||
> 更新说明:本次更新无法从旧版本同步数据,请使用游戏内数据同步功能上传best_score数据
|
||||
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
|
||||
>
|
||||
> Tips: When updating, please keep the original database in case of data loss.
|
||||
|
||||
> Update note: In this update you cannot synchronize data from an older version. Please use the data synchronization function in game to upload best_score data.
|
||||
|
||||
### Version 1.2
|
||||
- 仍然适用于Arcaea 3.2.0版本 Still for Arcaea 3.2.0
|
||||
- 新增了网页管理页面,可以通过 http:// HOST IP : PORT /web 进行访问 Add a new management webpage, you can visit http:// HOST IP : PORT /web to use it.
|
||||
- 修改了character表结构,这将方便以后的角色更新 The character table structure has been modified, which will facilitate the character update in the future.
|
||||
- 修复了best_score表的时间记录,应该是精确到秒的Unix时间戳 Fix the time record of best_score table. It should be a UNIX timestamp accurate to seconds.
|
||||
- 修复了一个bug,recent30现在会考虑同一首歌的不同难度 Fix a bug. Now recent30 will consider different difficulties of one song.
|
||||
> 更新说明:现在你可以使用web管理页面来迁移旧数据了,后台固定账号admin,密码admin
|
||||
> 如果使用数据迁移,那么请先清空recent30表数据,或者在迁移后打30首歌手动清除recent30,这次更新后web页面读取原来的recent30表将会报错
|
||||
|
||||
> Update note: Now you can use the web management page to migrate old data. Background account is fixed. Username is admin, and password is admin.
|
||||
> If you use data migration, please clear the data in the recent30 table first, or manually clear recent30 by playing 30 songs after the migration. After this update, an error will be reported when the web page reads the past recent30 table.
|
||||
|
||||
### Version 1.1
|
||||
- 适用于Arcaea 3.2.0版本 For Arcaea 3.2.0
|
||||
- 更新了歌曲数据库 Update the song database.
|
||||
- 角色**梦**可以看到觉醒立绘了 The drawing of uncapped character **Yume** can be seen.
|
||||
- 修复了一个Bug,现在剧情可以全部解锁了 Fix a bug. Now the plot can be unlocked.
|
||||
> 更新说明:您若需要做数据迁移,请使用原来的数据库文件**arcaea_database.db**,并使用数据库操作软件打开数据库修改角色数据,SQL命令为
|
||||
> `Update user_char set is_uncapped=1, is_uncapped_override=1 where character_id=21;`
|
||||
|
||||
> Update note: If you need to do data migration, please use the original database file **arcaea_ database.db**, and use the database operation software to open the database to modify the character data. The SQL command is
|
||||
> `Update user_char set is_uncapped=1, is_uncapped_override=1 where character_id=21;`
|
||||
|
||||
### Version 1.0
|
||||
- 适用于Arcaea 3.1.2版本 For Arcaea 3.1.2
|
||||
### Version 2.10.1
|
||||
- 适用于Arcaea 4.1.4版本 For Arcaea 4.1.4
|
||||
- 新搭档 **天音** 已解锁 Unlock the character **Amane**.
|
||||
- 为**天音**技能提供支持 Add support for the skill of **Amane**.
|
||||
- 现在配置文件可以是含有部分选项的文件或模块 At present the setting file can be a module or a file with some of options.
|
||||
- 添加`waitress`和`gevent`的部署方案支持,并支持日志记录 Add deployment mode `waitress` and `gevent`, and add support for the info log recording of them.
|
||||
- 为`songlist`添加解析器以指定可下载的文件 Add a parser for `songlist` to specify downloadable files.
|
||||
- 重构数据库初始化和数据迁移部分 Code refactoring for database initialization and migration.
|
||||
- 限制用户下载频率将使用第三方限制器,替代数据库 Add a custom limiter and use it for limiting users' download rate instead of using database.
|
||||
> 现在需要`limits`模块
|
||||
> Now `limits` module is required.
|
||||
- 为登录和API登录添加限制器 Add limiter for login and API login.
|
||||
- `sqlite3`数据库调整为WAL模式并增大缓存 Change journal mode to WAL and enlarge cache size for `sqlite3` database.
|
||||
- 将下载token放入内存中而不是文件数据库中 Put download token in memory database instead of filesystem database.
|
||||
- 加速`best_score`表多次查询,表现为歌曲排行榜查询性能提升 Accelerate multiple querying in `best_score` table, which results in performance improvement of song ranklist query.
|
||||
- 优化歌曲下载部分 Make some optimization for downloading songs.
|
||||
- **修复更新recent 10时可能出现的死循环问题 Fix a bug that there is a endless loop in calculating recent 10 updating.** (due to 6fcca179182775615115cdb255b3a8223831a8a0)
|
||||
- 修复课题模式成绩没有rating的问题 Fix a bug that scores in course mode cannot calculate rating.
|
||||
- 修正搭档数值 Fix a character's value.
|
||||
- 邮箱长度最大限制提升到64 Change the email max length to 64.
|
||||
- 新增API接口来获取用户身份与权限 Add a method of API for getting users' roles and powers.
|
||||
- 新增API接口来修改用户信息 Add a method of API to change the user's info.
|
||||
- 为API的`GET`请求添加`query`参数支持 Add support for the `query` param in API's `GET` requests.
|
||||
- 修复API的`best30`接口曲目无数据导致出错的问题 Fix a bug that `best30` of API cannot have scores whose songs are not in database.
|
||||
- 修复API的`recent30`接口用户成绩数量不足导致出错的问题 Fix a bug that users with no recent scores cannot get `recent30` via API.
|
||||
|
||||
## 运行环境与依赖 Running environment and requirements
|
||||
- Windows操作系统 Windows operating system
|
||||
- Python 3
|
||||
- Flask模块 Flask module
|
||||
- Charles
|
||||
- Windows/Linux/Mac OS/Android...
|
||||
- Python >= 3.6
|
||||
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
|
||||
- Charles, IDA, proxy app... (optional)
|
||||
|
||||
<!--
|
||||
## 环境搭建 Environment construction
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Environment-construction)
|
||||
-->
|
||||
|
||||
## 使用说明 Instruction for use
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)
|
||||
@@ -65,9 +113,18 @@ It is just so interesting. What it can do is under exploration.
|
||||
[中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F)
|
||||
[English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions)
|
||||
|
||||
|
||||
## Q&A
|
||||
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
|
||||
|
||||
|
||||
## 鸣谢 Thanks
|
||||
歌曲数据库来自 Using song database from
|
||||
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)
|
||||
~~歌曲数据库来自 Using song database from
|
||||
[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~
|
||||
> 从v2.9开始不再提供歌曲数据
|
||||
> Since v2.9, song data will not be provided.
|
||||
|
||||
网站图标来自 Using favicon from [black fate - てんてん - pixiv](https://www.pixiv.net/artworks/82374369)
|
||||
|
||||
## 联系方式 Contact
|
||||
如有必要,可以联系本人 Contact me if necessary
|
||||
|
||||
10
latest version/api/__init__.py
Normal file
10
latest version/api/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from . import users
|
||||
from . import songs
|
||||
from . import token
|
||||
|
||||
bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
bp.register_blueprint(users.bp)
|
||||
bp.register_blueprint(songs.bp)
|
||||
bp.register_blueprint(token.bp)
|
||||
105
latest version/api/api_auth.py
Normal file
105
latest version/api/api_auth.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from functools import wraps
|
||||
from traceback import format_exc
|
||||
from base64 import b64decode
|
||||
from json import loads
|
||||
|
||||
from core.api_user import APIUser
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError, NoAccess, PostError
|
||||
from core.sql import Connect
|
||||
from flask import current_app
|
||||
|
||||
from .api_code import error_return
|
||||
|
||||
|
||||
def role_required(request, powers=[]):
|
||||
'''api token验证,写成了修饰器'''
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
if request.data:
|
||||
request.json # 检查请求json格式
|
||||
except:
|
||||
return error_return(PostError('Payload must be a valid json', api_error_code=-1), 400)
|
||||
|
||||
if not 'Token' in request.headers:
|
||||
return error_return(PostError('No token', api_error_code=-1), 401)
|
||||
|
||||
user = APIUser()
|
||||
with Connect() as c:
|
||||
user.c = c
|
||||
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
|
||||
user.set_role_system()
|
||||
else:
|
||||
try:
|
||||
user.select_user_id_from_api_token(
|
||||
request.headers['Token'])
|
||||
user.select_role_and_powers()
|
||||
|
||||
if not any(user.role.has_power(y) for y in powers):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
except ArcError as e:
|
||||
return error_return(e, 401)
|
||||
|
||||
return view(user, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
|
||||
def request_json_handle(request, required_keys=[], optional_keys=[]):
|
||||
'''
|
||||
提取post参数,返回dict,写成了修饰器\
|
||||
parameters: \
|
||||
`request`: `Request` - 当前请求\
|
||||
`required_keys`: `list` - 必须的参数\
|
||||
`optional_keys`: `list` - 可选的参数
|
||||
'''
|
||||
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
data = {}
|
||||
if request.data:
|
||||
json_data = request.json
|
||||
else:
|
||||
if request.method == 'GET' and 'query' in request.args:
|
||||
# 处理axios没法GET传data的问题
|
||||
json_data = loads(
|
||||
b64decode(request.args['query']).decode())
|
||||
else:
|
||||
json_data = {}
|
||||
|
||||
for key in required_keys:
|
||||
if key not in json_data:
|
||||
return error_return(PostError(f'Missing parameter: {key}', api_error_code=-100))
|
||||
data[key] = json_data[key]
|
||||
|
||||
for key in optional_keys:
|
||||
if key in json_data:
|
||||
data[key] = json_data[key]
|
||||
|
||||
return view(data, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
|
||||
def api_try(view):
|
||||
'''替代try/except,记录`ArcError`为warning'''
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
data = view(*args, **kwargs)
|
||||
if data is None:
|
||||
return error_return()
|
||||
else:
|
||||
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
|
||||
35
latest version/api/api_code.py
Normal file
35
latest version/api/api_code.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from core.error import ArcError
|
||||
from flask import jsonify
|
||||
|
||||
default_error = ArcError('Unknown Error')
|
||||
|
||||
|
||||
CODE_MSG = {
|
||||
0: '',
|
||||
-1: 'See status code', # 基础错误
|
||||
-2: 'No data',
|
||||
-3: 'No data or user', # 不确定是无数据还是无用户
|
||||
-100: 'Invalid post data', # 1xx数据错误
|
||||
-101: 'Invalid data type',
|
||||
-102: 'Invalid query parameter',
|
||||
-103: 'Invalid sort parameter',
|
||||
-104: 'Invalid sort order parameter',
|
||||
-105: 'Invalid URL parameter',
|
||||
-110: 'Invalid user_id',
|
||||
-200: 'No permission', # 2xx用户相关错误
|
||||
-201: 'Wrong username or password',
|
||||
-202: 'User is banned',
|
||||
-203: 'Too many login attempts',
|
||||
-210: 'Username exists',
|
||||
-211: 'Email address exists',
|
||||
-212: 'User code exists',
|
||||
-999: 'Unknown error'
|
||||
}
|
||||
|
||||
|
||||
def success_return(data: dict = {}, status: int = 200, msg: str = ''):
|
||||
return jsonify({'code': 0, 'data': data, 'msg': msg}), status
|
||||
|
||||
|
||||
def error_return(e: 'ArcError' = default_error, status: int = 200):
|
||||
return jsonify({'code': e.api_error_code, 'data': {} if e.extra_data is None else e.extra_data, 'msg': CODE_MSG[e.api_error_code] if e.message is None else e.message}), status
|
||||
2
latest version/api/constant.py
Normal file
2
latest version/api/constant.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class Constant:
|
||||
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
|
||||
42
latest version/api/songs.py
Normal file
42
latest version/api/songs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from core.error import NoData
|
||||
from core.song import Song
|
||||
from core.sql import Connect, Query, Sql
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import success_return
|
||||
from .constant import Constant
|
||||
|
||||
bp = Blueprint('songs', __name__, url_prefix='/songs')
|
||||
|
||||
|
||||
@bp.route('/<string:song_id>', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_song_info'])
|
||||
@api_try
|
||||
def songs_song_get(user, song_id):
|
||||
'''查询歌曲信息'''
|
||||
with Connect() as c:
|
||||
s = Song(c, song_id).select()
|
||||
return success_return(s.to_dict())
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_song_info'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def songs_get(data, user):
|
||||
'''查询全歌曲信息'''
|
||||
A = ['song_id', 'name']
|
||||
B = ['song_id', 'name', 'rating_pst',
|
||||
'rating_prs', 'rating_ftr', 'rating_byn']
|
||||
with Connect() as c:
|
||||
query = Query(A, A, B).from_dict(data)
|
||||
x = Sql(c).select('chart', query=query)
|
||||
r = []
|
||||
for i in x:
|
||||
r.append(Song(c).from_list(i))
|
||||
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
return success_return([x.to_dict() for x in r])
|
||||
52
latest version/api/token.py
Normal file
52
latest version/api/token.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from base64 import b64decode
|
||||
|
||||
from core.api_user import APIUser
|
||||
from core.error import PostError
|
||||
from core.sql import Connect
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import success_return
|
||||
|
||||
bp = Blueprint('token', __name__, url_prefix='/token')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@request_json_handle(request, required_keys=['auth'])
|
||||
@api_try
|
||||
def token_post(data):
|
||||
'''
|
||||
登录,获取token\
|
||||
{'auth': base64('<user_id>:<password>')}
|
||||
'''
|
||||
try:
|
||||
auth_decode = bytes.decode(b64decode(data['auth']))
|
||||
except:
|
||||
raise PostError(api_error_code=-100)
|
||||
if not ':' in auth_decode:
|
||||
raise PostError(api_error_code=-100)
|
||||
name, password = auth_decode.split(':', 1)
|
||||
|
||||
with Connect() as c:
|
||||
user = APIUser(c)
|
||||
user.login(name, password, request.remote_addr)
|
||||
return success_return({'token': user.token, 'user_id': user.user_id})
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@role_required(request, ['select_me', 'select'])
|
||||
@api_try
|
||||
def token_get(user):
|
||||
'''判断登录有效性'''
|
||||
return success_return()
|
||||
|
||||
|
||||
@bp.route('', methods=['DELETE'])
|
||||
@role_required(request, ['change_me', 'select_me', 'select'])
|
||||
@api_try
|
||||
def token_delete(user):
|
||||
'''登出'''
|
||||
with Connect() as c:
|
||||
user.c = c
|
||||
user.logout()
|
||||
return success_return()
|
||||
193
latest version/api/users.py
Normal file
193
latest version/api/users.py
Normal file
@@ -0,0 +1,193 @@
|
||||
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.api_user import APIUser
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .api_auth import api_try, request_json_handle, role_required
|
||||
from .api_code import error_return, success_return
|
||||
from .constant import Constant
|
||||
|
||||
bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, ['name', 'password', 'email'])
|
||||
@api_try
|
||||
def users_post(data, user):
|
||||
'''注册一个用户'''
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
new_user.set_name(data['name'])
|
||||
new_user.set_password(data['password'])
|
||||
new_user.set_email(data['email'])
|
||||
new_user.register()
|
||||
return success_return({'user_id': new_user.user_id, 'user_code': new_user.user_code})
|
||||
|
||||
|
||||
@bp.route('', methods=['GET'])
|
||||
@role_required(request, ['select'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def users_get(data, user):
|
||||
'''查询全用户信息'''
|
||||
A = ['user_id', 'name', 'user_code']
|
||||
B = ['user_id', 'name', 'user_code', 'join_date',
|
||||
'rating_ptt', 'time_played', 'ticket', 'world_rank_score']
|
||||
with Connect() as c:
|
||||
query = Query(A, A, B).from_dict(data)
|
||||
x = Sql(c).select('user', query=query)
|
||||
r = []
|
||||
for i in x:
|
||||
r.append(UserInfo(c).from_list(i))
|
||||
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
return success_return([{
|
||||
'user_id': x.user_id,
|
||||
'name': x.name,
|
||||
'join_date': x.join_date,
|
||||
'user_code': x.user_code,
|
||||
'rating_ptt': x.rating_ptt,
|
||||
'character_id': x.character.character_id,
|
||||
'is_char_uncapped': x.character.is_uncapped,
|
||||
'is_char_uncapped_override': x.character.is_uncapped_override,
|
||||
'is_hide_rating': x.is_hide_rating,
|
||||
'ticket': x.ticket
|
||||
} for x in r])
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_get(user, user_id):
|
||||
'''查询用户信息'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要select权限
|
||||
if user_id != user.user_id and not user.role.has_power('select'):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
|
||||
with Connect() as c:
|
||||
u = UserInfo(c, user_id)
|
||||
return success_return(u.to_dict())
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>', methods=['PUT'])
|
||||
@role_required(request, ['change'])
|
||||
@request_json_handle(request, optional_keys=['name', 'password', 'user_code', 'ticket', 'email'])
|
||||
@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
|
||||
if r:
|
||||
u.update_columns(d=r)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/b30', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_b30_get(user, user_id):
|
||||
'''查询用户b30'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要select权限
|
||||
if user_id != user.user_id and not user.role.has_power('select'):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
|
||||
with Connect() as c:
|
||||
x = UserScoreList(c, UserInfo(c, user_id))
|
||||
x.query.limit = 30
|
||||
x.select_from_user()
|
||||
if not x.scores:
|
||||
raise NoData(
|
||||
f'No best30 data of user `{user_id}`', api_error_code=-3)
|
||||
x.select_song_name()
|
||||
r = x.to_dict_list()
|
||||
rating_sum = sum([i.rating for i in x.scores])
|
||||
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/best', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
|
||||
@api_try
|
||||
def users_user_best_get(data, user, user_id):
|
||||
'''查询用户所有best成绩'''
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要select权限
|
||||
if user_id != user.user_id and not user.role.has_power('select'):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
|
||||
with Connect() as c:
|
||||
x = UserScoreList(c, UserInfo(c, user_id))
|
||||
x.query.from_dict(data)
|
||||
x.select_from_user()
|
||||
if not x.scores:
|
||||
raise NoData(
|
||||
f'No best score data of user `{user_id}`', api_error_code=-3)
|
||||
r = x.to_dict_list()
|
||||
return success_return({'user_id': user_id, 'data': r})
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/r30', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_r30_get(user, user_id):
|
||||
'''查询用户r30'''
|
||||
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
# 查别人需要select权限
|
||||
if user_id != user.user_id and not user.role.has_power('select'):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
|
||||
with Connect() as c:
|
||||
p = Potential(c, UserInfo(c, user_id))
|
||||
return success_return({'user_id': user_id, 'r10_ptt': p.recent_10 / 10, 'data': p.recent_30_to_dict_list()})
|
||||
|
||||
|
||||
@bp.route('/<int:user_id>/role', methods=['GET'])
|
||||
@role_required(request, ['select', 'select_me'])
|
||||
@api_try
|
||||
def users_user_role_get(user, user_id):
|
||||
'''查询用户role和powers'''
|
||||
|
||||
if user_id <= 0:
|
||||
return error_return(InputError(api_error_code=-110))
|
||||
|
||||
if user_id == user.user_id:
|
||||
return success_return({'user_id': user.user_id, 'role': user.role.role_id, 'powers': [i.power_id for i in user.role.powers]})
|
||||
# 查别人需要select权限
|
||||
if not user.role.has_power('select'):
|
||||
return error_return(NoAccess('No permission', api_error_code=-1), 403)
|
||||
|
||||
with Connect() as c:
|
||||
x = APIUser(c, user_id)
|
||||
x.select_role_and_powers()
|
||||
return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
|
||||
259
latest version/config.example.py
Normal file
259
latest version/config.example.py
Normal file
@@ -0,0 +1,259 @@
|
||||
class Config():
|
||||
'''
|
||||
This is the example setting file.
|
||||
The user's setting file's name is `config.py`.
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
主机的地址和端口号
|
||||
Host and port of your server
|
||||
'''
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 80
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
游戏API地址前缀
|
||||
Game API's URL prefix
|
||||
'''
|
||||
GAME_API_PREFIX = '/join/21'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
允许使用的游戏版本,若为空,则默认全部允许
|
||||
Allowed game versions
|
||||
If it is blank, all are allowed.
|
||||
'''
|
||||
ALLOW_APPVERSION = ['3.12.6', '3.12.6c',
|
||||
'4.0.256', '4.0.256c', '4.1.0', '4.1.0c']
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
联机功能相关设置,请确保与Link Play服务器端的设置一致
|
||||
Setting of your link play server
|
||||
Please ensure that the settings on the side of Link Play server are consistent.
|
||||
'''
|
||||
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: 是否同时在本地启动Link Play服务器
|
||||
# SET_LINKPLAY_SERVER_AS_SUB_PROCESS: If it is `True`, the link play server will run with the main server locally at the same time.
|
||||
SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True
|
||||
# LINKPLAY_HOST: 对主服务器来说的Link Play服务器的地址
|
||||
# LINKPLAY_HOST: The address of the linkplay server based on the main server. If it is blank, the link play feature will be disabled.
|
||||
LINKPLAY_HOST = '0.0.0.0'
|
||||
LINKPLAY_UDP_PORT = 10900
|
||||
LINKPLAY_TCP_PORT = 10901
|
||||
LINKPLAY_AUTHENTICATION = 'my_link_play_server'
|
||||
# LINKPLAY_DISPLAY_HOST: 对客户端来说的Link Play服务器地址,如果为空,则自动获取
|
||||
# LINKPLAY_DISPLAY_HOST: The address of the linkplay server based on the client. If it is blank, the host of link play server for the client will be obtained automatically.
|
||||
LINKPLAY_DISPLAY_HOST = ''
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
SSL证书路径
|
||||
留空则使用HTTP
|
||||
SSL certificate path
|
||||
If left blank, use HTTP.
|
||||
'''
|
||||
SSL_CERT = '' # *.pem
|
||||
SSL_KEY = '' # *.key
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
愚人节模式开关
|
||||
Switch of April Fool's Day
|
||||
'''
|
||||
IS_APRILFOOLS = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
世界排名的最大显示数量
|
||||
The largest number of global rank
|
||||
'''
|
||||
WORLD_RANK_MAX = 200
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
世界模式当前活动图设置
|
||||
Current available maps in world mode
|
||||
'''
|
||||
AVAILABLE_MAP = [] # Ex. ['test', 'test2']
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的用户名和密码
|
||||
Username and password of web background management page
|
||||
'''
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'admin'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Web后台管理页面的session秘钥,如果不知道是什么,请不要修改
|
||||
Session key of web background management page
|
||||
If you don't know what it is, please don't modify it.
|
||||
'''
|
||||
SECRET_KEY = '1145141919810'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
API接口完全控制权限Token,留空则不使用
|
||||
API interface full control permission Token
|
||||
If you don't want to use it, leave it blank.
|
||||
'''
|
||||
API_TOKEN = ''
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
歌曲下载地址前缀,留空则自动获取
|
||||
Song download address prefix
|
||||
If left blank, it will be obtained automatically.
|
||||
'''
|
||||
DOWNLOAD_LINK_PREFIX = '' # http://***.com/download/
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
玩家歌曲下载的24小时次数限制,每个文件算一次
|
||||
Player's song download limit times in 24 hours, once per file
|
||||
'''
|
||||
DOWNLOAD_TIMES_LIMIT = 3000
|
||||
'''
|
||||
歌曲下载链接的有效时长,单位:秒
|
||||
Effective duration of song download link, unit: seconds
|
||||
'''
|
||||
DOWNLOAD_TIME_GAP_LIMIT = 1000
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
Arcaea登录的最大允许设备数量,最小值为1
|
||||
The maximum number of devices allowed to log in Arcaea, minimum: 1
|
||||
'''
|
||||
LOGIN_DEVICE_NUMBER_LIMIT = 1
|
||||
'''
|
||||
是否允许同设备多应用共存登录
|
||||
请注意,这个选项设置为True时,下一个选项将自动变为False
|
||||
If logging in from multiple applications on the same device is allowed
|
||||
Note that when this option is set to True, the next option automatically becomes False
|
||||
'''
|
||||
ALLOW_LOGIN_SAME_DEVICE = False
|
||||
'''
|
||||
24小时内登陆设备数超过最大允许设备数量时,是否自动封号(1天、3天、7天、15天、31天)
|
||||
When the number of login devices exceeds the maximum number of devices allowed to log in Arcaea within 24 hours, whether the account will be automatically banned (1 day, 3 days, 7 days, 15 days, 31 days)
|
||||
'''
|
||||
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否记录详细的服务器日志
|
||||
If recording detailed server logs is enabled
|
||||
'''
|
||||
ALLOW_INFO_LOG = False
|
||||
ALLOW_WARNING_LOG = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
用户注册时的默认记忆源点数量
|
||||
The default amount of memories at the time of user registration
|
||||
'''
|
||||
DEFAULT_MEMORIES = 0
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
数据库更新时,是否采用最新的角色数据,如果你想采用最新的官方角色数据
|
||||
注意:如果是,旧的数据将丢失;如果否,某些角色的数据变动将无法同步
|
||||
If using the latest character data when updating database. If you want to only keep newest official character data, please set it `True`.
|
||||
Note: If `True`, the old data will be lost; If `False`, the data changes of some characters will not be synchronized.
|
||||
'''
|
||||
UPDATE_WITH_NEW_CHARACTER_DATA = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁搭档
|
||||
If unlocking all partners is enabled
|
||||
'''
|
||||
CHARACTER_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁世界歌曲
|
||||
If unlocking all world songs is enabled
|
||||
'''
|
||||
WORLD_SONG_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否全解锁世界场景
|
||||
If unlocking all world sceneries is enabled
|
||||
'''
|
||||
WORLD_SCENERY_FULL_UNLOCK = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否强制使用全解锁云端存档
|
||||
If forcing full unlocked cloud save is enabled
|
||||
请注意,当前对于最终结局的判定为固定在`Testify`解锁之前
|
||||
Please note that the current setting of the finale state is before the unlock of `Testify`
|
||||
'''
|
||||
SAVE_FULL_UNLOCK = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
0
latest version/core/__init__.py
Normal file
0
latest version/core/__init__.py
Normal file
137
latest version/core/api_user.py
Normal file
137
latest version/core/api_user.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
from time import time
|
||||
|
||||
from .config_manager import Config
|
||||
from .error import NoAccess, NoData, RateLimit, UserBan
|
||||
from .limiter import ArcLimiter
|
||||
from .user import UserOnline
|
||||
|
||||
|
||||
class Power:
|
||||
def __init__(self, c=None):
|
||||
self.c = c
|
||||
self.power_id: str = None
|
||||
self.caption: str = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict, c=None) -> 'Power':
|
||||
p = cls(c)
|
||||
p.power_id = d['power_id']
|
||||
p.caption = d['caption']
|
||||
return p
|
||||
|
||||
|
||||
class Role:
|
||||
def __init__(self, c=None):
|
||||
self.c = c
|
||||
self.role_id: str = None
|
||||
self.caption: str = None
|
||||
|
||||
self.powers: list = None
|
||||
|
||||
def has_power(self, power_id: str) -> bool:
|
||||
'''判断role是否有power'''
|
||||
return any(power_id == i.power_id for i in self.powers)
|
||||
|
||||
def select_from_id(self, role_id: int = None) -> 'Role':
|
||||
'''用role_id查询role'''
|
||||
if role_id is not None:
|
||||
self.role_id = role_id
|
||||
self.c.execute('''select caption from role where role_id = :a''',
|
||||
{'a': self.role_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The role `%s` does not exist.' %
|
||||
self.role_id, api_error_code=-200)
|
||||
self.caption = x[0]
|
||||
return self
|
||||
|
||||
def select_powers(self) -> None:
|
||||
'''查询role的全部powers'''
|
||||
self.powers = []
|
||||
self.c.execute('''select * from power where power_id in (select power_id from role_power where role_id=:a)''', {
|
||||
'a': self.role_id})
|
||||
x = self.c.fetchall()
|
||||
for i in x:
|
||||
self.powers.append(Power.from_dict(
|
||||
{'power_id': i[0], 'caption': i[1]}, self.c))
|
||||
|
||||
|
||||
class APIUser(UserOnline):
|
||||
limiter = ArcLimiter(Config.API_LOGIN_RATE_LIMIT, 'api_login')
|
||||
|
||||
def __init__(self, c=None, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
self.api_token: str = None
|
||||
self.role: 'Role' = None
|
||||
|
||||
self.ip: str = None
|
||||
|
||||
def set_role_system(self) -> None:
|
||||
'''设置为最高权限用户,API接口'''
|
||||
self.user_id = 0
|
||||
self.role = Role(self.c)
|
||||
self.role.role_id = 'system'
|
||||
self.role.select_powers()
|
||||
|
||||
def select_role(self) -> None:
|
||||
'''查询user的role'''
|
||||
self.c.execute('''select role_id from user_role where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
self.role = Role(self.c)
|
||||
if x is None:
|
||||
# 默认role为user
|
||||
self.role.role_id = 'user'
|
||||
else:
|
||||
self.role.role_id = x[0]
|
||||
|
||||
def select_role_and_powers(self) -> None:
|
||||
'''查询user的role,以及role的powers'''
|
||||
self.select_role()
|
||||
self.role.select_powers()
|
||||
|
||||
def select_user_id_from_api_token(self, api_token: str = None) -> None:
|
||||
if api_token is not None:
|
||||
self.api_token = api_token
|
||||
self.c.execute('''select user_id from api_login where token = :token''', {
|
||||
'token': self.api_token})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoAccess('No token', api_error_code=-1)
|
||||
self.user_id = x[0]
|
||||
|
||||
def logout(self) -> None:
|
||||
self.c.execute(
|
||||
'''delete from api_login where user_id=?''', (self.user_id,))
|
||||
|
||||
def login(self, name: str = None, password: str = None, ip: str = None) -> None:
|
||||
if name is not None:
|
||||
self.name = name
|
||||
if password is not None:
|
||||
self.password = password
|
||||
if ip is not None:
|
||||
self.ip = ip
|
||||
if not self.limiter.hit(name):
|
||||
raise RateLimit('Too many login attempts', api_error_code=-205)
|
||||
|
||||
self.c.execute('''select user_id, password from user where name = :a''', {
|
||||
'a': self.name})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The user `%s` does not exist.' %
|
||||
self.name, api_error_code=-201)
|
||||
if x[1] == '':
|
||||
raise UserBan('The user `%s` is banned.' % self.name)
|
||||
if self.hash_pwd != x[1]:
|
||||
raise NoAccess('The password is incorrect.', api_error_code=-201)
|
||||
|
||||
self.user_id = x[0]
|
||||
now = int(time() * 1000)
|
||||
self.token = sha256(
|
||||
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
|
||||
|
||||
self.logout()
|
||||
self.c.execute('''insert into api_login values(?,?,?,?)''',
|
||||
(self.user_id, self.token, now, self.ip))
|
||||
377
latest version/core/character.py
Normal file
377
latest version/core/character.py
Normal file
@@ -0,0 +1,377 @@
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||
from .item import Item, ItemCore
|
||||
|
||||
|
||||
class Level:
|
||||
mid_level = 20
|
||||
min_level = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.max_level = None
|
||||
self.level = None
|
||||
self.exp = None
|
||||
|
||||
@property
|
||||
def level_exp(self):
|
||||
return Constant.LEVEL_STEPS[self.level]
|
||||
|
||||
def add_exp(self, exp_addition: float):
|
||||
# 添加经验计算
|
||||
|
||||
exp = self.exp + exp_addition
|
||||
|
||||
if exp >= Constant.LEVEL_STEPS[self.max_level]:
|
||||
self.exp = Constant.LEVEL_STEPS[self.max_level]
|
||||
self.level = self.max_level
|
||||
return None
|
||||
|
||||
a = []
|
||||
b = []
|
||||
for i in Constant.LEVEL_STEPS:
|
||||
a.append(i)
|
||||
b.append(Constant.LEVEL_STEPS[i])
|
||||
|
||||
if exp < b[0]: # 向下溢出,是异常状态,不该被try捕获,不然数据库无法回滚
|
||||
raise ValueError('EXP value error.')
|
||||
|
||||
i = len(a) - 1
|
||||
while exp < b[i]:
|
||||
i -= 1
|
||||
|
||||
self.exp = exp
|
||||
self.level = a[i]
|
||||
|
||||
|
||||
class Skill:
|
||||
def __init__(self) -> None:
|
||||
self.skill_id = None
|
||||
self.skill_id_uncap = None
|
||||
self.skill_unlock_level = None
|
||||
self.skill_requires_uncap = 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:
|
||||
def __init__(self, start: float = 0, mid: float = 0, end: float = 0) -> None:
|
||||
self.set_parameter(start, mid, end)
|
||||
|
||||
@staticmethod
|
||||
def _calc_char_value_20(level, stata, statb, lva=1, lvb=20):
|
||||
# 计算1~20级搭档数值的核心函数,返回浮点数,来自https://redive.estertion.win/arcaea/calc/
|
||||
n = [0, 0, 0.0005831753900000081, 0.004665403120000065, 0.015745735529959858, 0.03732322495992008, 0.07289692374980007, 0.12596588423968, 0.2000291587694801, 0.29858579967923987, 0.42513485930893946,
|
||||
0.5748651406910605, 0.7014142003207574, 0.7999708412305152, 0.8740341157603029, 0.9271030762501818, 0.962676775040091, 0.9842542644700301, 0.9953345968799998, 0.9994168246100001, 1]
|
||||
e = n[lva] - n[lvb]
|
||||
a = stata - statb
|
||||
r = a / e
|
||||
d = stata - n[lva] * r
|
||||
|
||||
return d + r * n[level]
|
||||
|
||||
@staticmethod
|
||||
def _calc_char_value_30(level, stata, statb, lva=20, lvb=30):
|
||||
# 计算21~30级搭档数值,返回浮点数
|
||||
return (level - lva) * (statb - stata) / (lvb - lva) + stata
|
||||
|
||||
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
|
||||
self.start = start
|
||||
self.mid = mid
|
||||
self.end = end
|
||||
|
||||
def get_value(self, level: Level):
|
||||
if level.min_level <= level.level <= level.mid_level:
|
||||
return self._calc_char_value_20(level.level, self.start, self.mid)
|
||||
elif level.mid_level < level.level <= level.max_level:
|
||||
return self._calc_char_value_30(level.level, self.mid, self.end)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class Character:
|
||||
database_table_name = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.character_id = None
|
||||
self.name = None
|
||||
self.char_type = None
|
||||
self.is_uncapped = None
|
||||
self.is_uncapped_override = None
|
||||
self.skill = Skill()
|
||||
self.level = Level()
|
||||
self.frag = CharacterValue()
|
||||
self.prog = CharacterValue()
|
||||
self.overdrive = CharacterValue()
|
||||
self.uncap_cores = []
|
||||
self.voice = None
|
||||
|
||||
@property
|
||||
def skill_id_displayed(self) -> str:
|
||||
return None
|
||||
|
||||
def uncap_cores_to_dict(self):
|
||||
return [x.to_dict() for x in self.uncap_cores]
|
||||
|
||||
@property
|
||||
def is_uncapped_displayed(self) -> bool:
|
||||
'''对外显示的uncap状态'''
|
||||
return False if self.is_uncapped_override else self.is_uncapped
|
||||
|
||||
@property
|
||||
def is_base_character(self) -> bool:
|
||||
# 应该是只有对立这样
|
||||
return self.character_id == 1
|
||||
|
||||
|
||||
class UserCharacter(Character):
|
||||
'''
|
||||
用户角色类\
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||
|
||||
def __init__(self, c, character_id=None, user=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.character_id = character_id
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def skill_id_displayed(self) -> str:
|
||||
'''对外显示的技能id'''
|
||||
if self.is_uncapped_displayed and self.skill.skill_id_uncap:
|
||||
return self.skill.skill_id_uncap
|
||||
elif self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
|
||||
return self.skill.skill_id
|
||||
else:
|
||||
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):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取此角色的觉醒信息
|
||||
if 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,
|
||||
{'a': self.user.user_id, 'b': self.character_id})
|
||||
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.is_uncapped = False
|
||||
self.is_uncapped_override = False
|
||||
# raise NoData('The character of the user does not exist.')
|
||||
else:
|
||||
self.is_uncapped = x[0] == 1
|
||||
self.is_uncapped_override = x[1] == 1
|
||||
|
||||
def select_character_info(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取所给用户此角色信息
|
||||
if user:
|
||||
self.user = user
|
||||
self.c.execute('''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.user.user_id, self.character_id))
|
||||
|
||||
y = self.c.fetchone()
|
||||
if y is None:
|
||||
raise NoData('The character of the user does not exist.')
|
||||
|
||||
self.name = y[7]
|
||||
self.char_type = y[22]
|
||||
self.is_uncapped = y[4] == 1
|
||||
self.is_uncapped_override = y[5] == 1
|
||||
self.level.level = y[2]
|
||||
self.level.exp = y[3]
|
||||
self.level.max_level = y[8]
|
||||
self.frag.set_parameter(y[9], y[12], y[15])
|
||||
self.prog.set_parameter(y[10], y[13], y[16])
|
||||
self.overdrive.set_parameter(y[11], y[14], y[17])
|
||||
self.skill.skill_id = y[18]
|
||||
self.skill.skill_id_uncap = y[21]
|
||||
self.skill.skill_unlock_level = y[19]
|
||||
self.skill.skill_requires_uncap = y[20] == 1
|
||||
|
||||
if self.character_id == 21 or self.character_id == 46:
|
||||
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
|
||||
self.select_character_core()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.char_type is None:
|
||||
self.select_character_info(self.user)
|
||||
r = {'base_character': self.is_base_character,
|
||||
"is_uncapped_override": self.is_uncapped_override,
|
||||
"is_uncapped": self.is_uncapped,
|
||||
"uncap_cores": self.uncap_cores_to_dict(),
|
||||
"char_type": self.char_type,
|
||||
"skill_id_uncap": self.skill.skill_id_uncap,
|
||||
"skill_requires_uncap": self.skill.skill_requires_uncap,
|
||||
"skill_unlock_level": self.skill.skill_unlock_level,
|
||||
"skill_id": self.skill.skill_id,
|
||||
"overdrive": self.overdrive.get_value(self.level),
|
||||
"prog": self.prog.get_value(self.level),
|
||||
"frag": self.frag.get_value(self.level),
|
||||
"level_exp": self.level.level_exp,
|
||||
"exp": self.level.exp,
|
||||
"level": self.level.level,
|
||||
"name": self.name,
|
||||
"character_id": self.character_id
|
||||
}
|
||||
if self.voice:
|
||||
r['voice'] = self.voice
|
||||
if self.character_id == 55:
|
||||
r['fatalis_is_limited'] = False # emmmmmmm
|
||||
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
|
||||
r['base_character_id'] = 1
|
||||
return r
|
||||
|
||||
def change_uncap_override(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 切换觉醒状态
|
||||
if 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,
|
||||
{'a': self.user.user_id, 'b': self.character_id})
|
||||
|
||||
x = self.c.fetchone()
|
||||
if x is None or x[0] == 0:
|
||||
raise ArcError('Unknown Error')
|
||||
|
||||
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
|
||||
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
|
||||
|
||||
self.c.execute('''update %s set is_uncapped_override = :a where user_id = :b and character_id = :c''' % self.database_table_name, {
|
||||
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id})
|
||||
|
||||
self.is_uncapped_override = x[1] == 0
|
||||
|
||||
def character_uncap(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 觉醒角色
|
||||
if user:
|
||||
self.user = user
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
# 全解锁了你觉醒个鬼啊
|
||||
raise ArcError('All characters are available.')
|
||||
|
||||
if not self.uncap_cores:
|
||||
self.select_character_core()
|
||||
|
||||
if self.is_uncapped is None:
|
||||
self.c.execute(
|
||||
'''select is_uncapped from user_char where user_id=? and character_id=?''', (self.user.user_id, self.character_id))
|
||||
x = self.c.fetchone()
|
||||
if x and x[0] == 1:
|
||||
raise ArcError('The character has been uncapped.')
|
||||
elif self.is_uncapped:
|
||||
raise ArcError('The character has been uncapped.')
|
||||
|
||||
for i in self.uncap_cores:
|
||||
self.c.execute(
|
||||
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
|
||||
y = self.c.fetchone()
|
||||
if not y or i.amount > y[0]:
|
||||
raise ItemNotEnough('The cores are not enough.')
|
||||
|
||||
for i in self.uncap_cores:
|
||||
ItemCore(self.c, i, reverse=True).user_claim_item(self.user)
|
||||
|
||||
self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
|
||||
(self.user.user_id, self.character_id))
|
||||
|
||||
self.is_uncapped = True
|
||||
self.is_uncapped_override = False
|
||||
|
||||
def upgrade(self, user=None, exp_addition: float = 0) -> None:
|
||||
# parameter: user - User类或子类的实例
|
||||
# 升级角色
|
||||
if user:
|
||||
self.user = user
|
||||
if exp_addition == 0:
|
||||
return None
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
# 全解锁了你升级个鬼啊
|
||||
raise ArcError('All characters are available.')
|
||||
|
||||
if self.level.exp is None:
|
||||
self.select_character_info(self.user)
|
||||
|
||||
if self.is_uncapped is None:
|
||||
self.c.execute(
|
||||
'''select is_uncapped from user_char where user_id=? and character_id=?''', (self.user.user_id, self.character_id))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.is_uncapped = x[0] == 1
|
||||
|
||||
self.level.max_level = 30 if self.is_uncapped else 20
|
||||
self.level.add_exp(exp_addition)
|
||||
|
||||
self.c.execute('''update user_char set level=?, exp=? where user_id=? and character_id=?''',
|
||||
(self.level.level, self.level.exp, self.user.user_id, self.character_id))
|
||||
|
||||
def upgrade_by_core(self, user=None, core=None):
|
||||
'''
|
||||
以太之滴升级,注意这里core.amount应该是负数\
|
||||
parameter: `user` - `User`类或子类的实例\
|
||||
`core` - `ItemCore`类或子类的实例
|
||||
'''
|
||||
if user:
|
||||
self.user = user
|
||||
if not core:
|
||||
raise InputError('No `core_generic`.')
|
||||
if core.item_id != 'core_generic':
|
||||
raise ArcError('Core type error.')
|
||||
|
||||
if core.amount >= 0:
|
||||
raise InputError(
|
||||
'The amount of `core_generic` should be negative.')
|
||||
|
||||
core.user_claim_item(self.user)
|
||||
self.upgrade(self.user, - core.amount * Constant.CORE_EXP)
|
||||
|
||||
|
||||
class UserCharacterList:
|
||||
'''
|
||||
用户拥有角色列表类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
database_table_name = 'user_char_full' if Config.CHARACTER_FULL_UNLOCK else 'user_char'
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.characters: list = []
|
||||
|
||||
def select_user_characters(self):
|
||||
self.c.execute(
|
||||
'''select character_id from %s where user_id=?''' % self.database_table_name, (self.user.user_id,))
|
||||
x = self.c.fetchall()
|
||||
self.characters: list = []
|
||||
if x:
|
||||
for i in x:
|
||||
self.characters.append(UserCharacter(self.c, i[0], self.user))
|
||||
|
||||
def select_characters_info(self):
|
||||
for i in self.characters:
|
||||
i.select_character_info(self.user)
|
||||
93
latest version/core/config_manager.py
Normal file
93
latest version/core/config_manager.py
Normal file
@@ -0,0 +1,93 @@
|
||||
class Config:
|
||||
'''
|
||||
Default config
|
||||
'''
|
||||
|
||||
HOST = '0.0.0.0'
|
||||
PORT = 80
|
||||
|
||||
DEPLOY_MODE = 'flask_multithread'
|
||||
USE_PROXY_FIX = False
|
||||
USE_CORS = False
|
||||
|
||||
GAME_API_PREFIX = '/join/21'
|
||||
|
||||
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_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/'
|
||||
|
||||
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)
|
||||
101
latest version/core/constant.py
Normal file
101
latest version/core/constant.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from .config_manager import Config
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.10.1'
|
||||
|
||||
|
||||
class Constant:
|
||||
|
||||
BAN_TIME = [1, 3, 7, 15, 31]
|
||||
|
||||
MAX_STAMINA = 12
|
||||
|
||||
STAMINA_RECOVER_TICK = 1800000
|
||||
|
||||
COURSE_STAMINA_COST = 4
|
||||
|
||||
CORE_EXP = 250
|
||||
|
||||
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
|
||||
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
|
||||
|
||||
ETO_UNCAP_BONUS_PROGRESS = 7
|
||||
LUNA_UNCAP_BONUS_PROGRESS = 7
|
||||
AYU_UNCAP_BONUS_PROGRESS = 5
|
||||
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
|
||||
|
||||
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
|
||||
|
||||
MY_RANK_MAX_LOCAL_POSITION = 5
|
||||
MY_RANK_MAX_GLOBAL_POSITION = 9999
|
||||
|
||||
BEST30_WEIGHT = Config.BEST30_WEIGHT
|
||||
RECENT10_WEIGHT = Config.RECENT10_WEIGHT
|
||||
|
||||
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
|
||||
|
||||
DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT
|
||||
DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT
|
||||
DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX
|
||||
|
||||
LINKPLAY_UNLOCK_LENGTH = 512 # Units: bytes
|
||||
LINKPLAY_TIMEOUT = 5 # Units: seconds
|
||||
|
||||
LINKPLAY_HOST = '127.0.0.1' if Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS else Config.LINKPLAY_HOST
|
||||
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
|
||||
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
|
||||
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
|
||||
|
||||
# Well, I can't say a word when I see this.
|
||||
FINALE_SWITCH = [
|
||||
(0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B),
|
||||
(0x02429E, 0x0449A4), (0x099C9C,
|
||||
0x07CFB4), (0x0785BF, 0x019B2C),
|
||||
(0x0EFF43, 0x0841BF), (0x07C88B,
|
||||
0x0DE9FC), (0x000778, 0x064815),
|
||||
(0x0E62E3, 0x079F02), (0x0188FE,
|
||||
0x0923EB), (0x0E06CD, 0x0E1A26),
|
||||
(0x00669E, 0x0C8BE1), (0x0BEB7A, 0x05D635), (0x040E6F,
|
||||
0x0B465B), (0x0568EC, 0x07ED2B),
|
||||
(0x189614, 0x00A3D2), (0x62D98D,
|
||||
0x45E5CA), (0x6D8769, 0x473F0E),
|
||||
(0x922E4F, 0x667D6C), (0x021F5C,
|
||||
0x298839), (0x2A1201, 0x49FB7E),
|
||||
(0x158B81, 0x8D905D), (0x2253A5,
|
||||
0x7E7067), (0x3BEF79, 0x9368E9),
|
||||
(0x00669E, 0x0C8BE1), (0x0BEB7A,
|
||||
0x05D635), (0x040E6F, 0x0B465B),
|
||||
(0x756276, 0x55CD57), (0x130055, 0x7010E7), (0x55E28D,
|
||||
0x4477FB), (0x5E99CB, 0x81060E),
|
||||
(0x7F43A4, 0x8FEC56), (0x69412F,
|
||||
0x32735C), (0x8FF846, 0x14B5A1),
|
||||
(0x8716BE, 0x5C78BE), (0x62ED0E,
|
||||
0x348E4B), (0x4B20C8, 0x56E0C3),
|
||||
(0x0AF6BC, 0x872441), (0x8825BC,
|
||||
0x94B315), (0x792784, 0x5B2C8E),
|
||||
(0x1AE3A7, 0x688E97), (0x0D630F,
|
||||
0x06BE78), (0x792784, 0x5B2C8E),
|
||||
(0x314869, 0x41CCC1), (0x311934, 0x24DD94), (0x190EDB,
|
||||
0x33993D), (0x25F5C5, 0x15FAE6),
|
||||
(0x18CA10, 0x1B761A), (0x51BE82,
|
||||
0x120089), (0x51D3B6, 0x2C29A2),
|
||||
(0x402075, 0x4A89B2), (0x00697B,
|
||||
0x0E6497), (0x6D872D, 0x618AE7),
|
||||
(0x3DC0BE, 0x4E2AC8), (0x8C6ACF,
|
||||
0x9776CF), (0x84673B, 0x5CA060),
|
||||
(0x4B05EC, 0x97FDFE), (0x207258,
|
||||
0x02BB9B), (0x20A9EE, 0x1BA4BB),
|
||||
(0x503D21, 0x6A41D0), (0x1C256C,
|
||||
0x6DD3BC), (0x6E4E0C, 0x89FDAA), (0x3C5F95, 0x3BA786),
|
||||
(0XFEA5, 0x2e4ca), (0X7BF653, 0x4befd11), (0X46BEA7B,
|
||||
0x11d3684), (0X8BFB04, 0xa83d6c1),
|
||||
(0X5D6FC5, 0xab97ef), (0X237206D, 0xdfef2), (0XA3DEE,
|
||||
0x6CB300), (0XA35687B, 0xE456CDEA)
|
||||
]
|
||||
|
||||
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
|
||||
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
|
||||
|
||||
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA
|
||||
298
latest version/core/course.py
Normal file
298
latest version/core/course.py
Normal file
@@ -0,0 +1,298 @@
|
||||
from .error import NoData
|
||||
from .item import ItemFactory
|
||||
from .song import Chart
|
||||
|
||||
|
||||
class CourseChart(Chart):
|
||||
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
||||
super().__init__(c, song_id, difficulty)
|
||||
self.flag_as_hidden: bool = None
|
||||
|
||||
def from_dict(self, d: dict) -> 'CourseChart':
|
||||
self.song_id = d['id']
|
||||
self.difficulty = d['difficulty']
|
||||
self.flag_as_hidden = d.get('flag_as_hidden', False)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'id': self.song_id,
|
||||
'difficulty': self.difficulty,
|
||||
'flag_as_hidden': self.flag_as_hidden
|
||||
}
|
||||
|
||||
def insert_course_chart(self, course_id: str, song_index: int) -> None:
|
||||
self.c.execute('insert into course_chart values (?,?,?,?,?)',
|
||||
(course_id, self.song_id, self.difficulty, self.flag_as_hidden, song_index))
|
||||
|
||||
|
||||
class Course:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.course_id: str = None
|
||||
self.course_name: str = None
|
||||
self.dan_name: str = None
|
||||
self.style: int = None
|
||||
self.gauge_requirement: str = None
|
||||
self.flag_as_hidden_when_requirements_not_met: bool = None
|
||||
self.can_start: bool = None # 这有什么鬼用?
|
||||
|
||||
self.requirements: list = []
|
||||
self.charts: list = [None, None, None, None]
|
||||
self.items: list = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.course_name is None:
|
||||
self.select_course()
|
||||
if not self.items:
|
||||
self.select_course_item()
|
||||
if not self.charts:
|
||||
self.select_course_chart()
|
||||
return {
|
||||
'course_id': self.course_id,
|
||||
'course_name': self.course_name,
|
||||
'dan_name': self.dan_name,
|
||||
'style': self.style,
|
||||
'gauge_requirement': self.gauge_requirement,
|
||||
'flag_as_hidden_when_requirements_not_met': self.flag_as_hidden_when_requirements_not_met,
|
||||
'can_start': self.can_start,
|
||||
'requirements': [{'value': x, 'type': 'course'} for x in self.requirements],
|
||||
'songs': [x.to_dict() for x in self.charts],
|
||||
'rewards': [str(x) for x in self.items]
|
||||
}
|
||||
|
||||
def from_dict(self, d: dict) -> 'Course':
|
||||
self.course_id = d['course_id']
|
||||
self.course_name = d.get('course_name', '')
|
||||
self.dan_name = d.get('dan_name', '')
|
||||
self.style = d.get('style', 1)
|
||||
self.gauge_requirement = d.get('gauge_requirement', 'default')
|
||||
self.flag_as_hidden_when_requirements_not_met = d.get(
|
||||
'flag_as_hidden_when_requirements_not_met', False)
|
||||
self.can_start = d.get('can_start', True)
|
||||
self.requirements = [x['value'] for x in d.get('requirements', [])]
|
||||
self.charts = [CourseChart(self.c).from_dict(x)
|
||||
for x in d.get('songs', [])]
|
||||
self.items = [ItemFactory.from_str(x, self.c)
|
||||
for x in d.get('rewards', [])]
|
||||
|
||||
return self
|
||||
|
||||
def from_list(self, l: list) -> 'Course':
|
||||
self.course_id = l[0]
|
||||
self.course_name = l[1]
|
||||
self.dan_name = l[2]
|
||||
self.style = l[3] if l[3] else 1
|
||||
self.gauge_requirement = l[4] if l[4] else 'default'
|
||||
self.flag_as_hidden_when_requirements_not_met = l[5] == 1
|
||||
self.can_start = l[6] == 1
|
||||
|
||||
return self
|
||||
|
||||
def select_course(self, course_id: str = None) -> 'Course':
|
||||
if course_id is not None:
|
||||
self.course_id = course_id
|
||||
self.c.execute(
|
||||
'''select * from course where course_id = ?''', (self.course_id,))
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The course `%s` is not found.' % self.course_id)
|
||||
return self.from_list(x)
|
||||
|
||||
def select_course_chart(self) -> None:
|
||||
self.c.execute(
|
||||
'''select * from course_chart where course_id = ?''', (self.course_id,))
|
||||
|
||||
for i in self.c.fetchall():
|
||||
self.charts[i[4]] = CourseChart(self.c).from_dict({
|
||||
'id': i[1],
|
||||
'difficulty': i[2],
|
||||
'flag_as_hidden': i[3] == 1
|
||||
})
|
||||
|
||||
def select_course_requirement(self) -> None:
|
||||
self.c.execute(
|
||||
'''select required_id from course_requirement where course_id = ?''', (self.course_id,))
|
||||
self.requirements = [x[0] for x in self.c.fetchall()]
|
||||
|
||||
def select_course_item(self) -> None:
|
||||
self.c.execute(
|
||||
'''select * from course_item where course_id = ?''', (self.course_id,))
|
||||
self.items = [ItemFactory.from_dict({
|
||||
'item_id': x[1],
|
||||
'type': x[2],
|
||||
'amount': x[3] if x[3] else 1,
|
||||
}, self.c) for x in self.c.fetchall()]
|
||||
|
||||
def insert_course(self) -> None:
|
||||
self.c.execute(
|
||||
'''insert into course values (?,?,?,?,?,?,?)''', (self.course_id, self.course_name, self.dan_name, self.style, self.gauge_requirement, self.flag_as_hidden_when_requirements_not_met, self.can_start))
|
||||
|
||||
def insert_course_item(self) -> None:
|
||||
for i in self.items:
|
||||
self.c.execute('''insert into course_item values (?,?,?,?)''',
|
||||
(self.course_id, i.item_id, i.item_type, i.amount))
|
||||
|
||||
def insert_course_chart(self) -> None:
|
||||
for i, x in enumerate(self.charts):
|
||||
x.insert_course_chart(self.course_id, i)
|
||||
|
||||
def insert_course_requirement(self) -> None:
|
||||
for i in self.requirements:
|
||||
self.c.execute('''insert into course_requirement values (?,?)''',
|
||||
(self.course_id, i))
|
||||
|
||||
def insert_all(self) -> None:
|
||||
self.insert_course()
|
||||
self.insert_course_item()
|
||||
self.insert_course_chart()
|
||||
self.insert_course_requirement()
|
||||
|
||||
|
||||
class UserCourse(Course):
|
||||
'''
|
||||
用户课题类\
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
self.is_completed: bool = False
|
||||
self.high_score: int = None
|
||||
self.best_clear_type: int = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = super().to_dict()
|
||||
if self.is_completed is None:
|
||||
self.select_user_course()
|
||||
r.update({
|
||||
'is_completed': self.is_completed,
|
||||
'high_score': self.high_score,
|
||||
'best_clear_type': self.best_clear_type
|
||||
})
|
||||
return r
|
||||
|
||||
def select_user_course(self, course_id: str = None) -> None:
|
||||
if course_id is not None:
|
||||
self.course_id = course_id
|
||||
self.c.execute('''select * from user_course where user_id = ? and course_id = ?''',
|
||||
(self.user.user_id, self.course_id))
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
self.is_completed = False
|
||||
self.high_score = 0
|
||||
self.best_clear_type = 0
|
||||
else:
|
||||
self.is_completed = True
|
||||
self.high_score = x[2]
|
||||
self.best_clear_type = x[3]
|
||||
|
||||
def insert_user_course(self) -> None:
|
||||
self.c.execute('''insert into user_course values (?,?,?,?)''',
|
||||
(self.user.user_id, self.course_id, self.high_score, self.best_clear_type))
|
||||
|
||||
def update_user_course(self) -> None:
|
||||
self.c.execute('''update user_course set high_score = ?, best_clear_type = ? where user_id = ? and course_id = ?''',
|
||||
(self.high_score, self.best_clear_type, self.user.user_id, self.course_id))
|
||||
|
||||
|
||||
class UserCourseList:
|
||||
'''
|
||||
用户课题列表类\
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.courses: list = []
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
# 临时修复满足条件也无法解锁隐藏段位的问题
|
||||
completed_course_id_list: list = []
|
||||
r: list = []
|
||||
for x in self.courses:
|
||||
if x.is_completed:
|
||||
completed_course_id_list.append(x.course_id)
|
||||
r.append(x.to_dict())
|
||||
for x in r:
|
||||
for i in x['requirements']:
|
||||
if i['value'] in completed_course_id_list:
|
||||
i['is_fullfilled'] = True
|
||||
return r
|
||||
|
||||
def select_all(self) -> None:
|
||||
self.c.execute('''select * from course''')
|
||||
self.courses = [UserCourse(self.c, self.user).from_list(x)
|
||||
for x in self.c.fetchall()]
|
||||
for i in self.courses:
|
||||
i.select_user_course()
|
||||
i.select_course_chart()
|
||||
i.select_course_item()
|
||||
i.select_course_requirement()
|
||||
|
||||
|
||||
class CoursePlay(UserCourse):
|
||||
'''
|
||||
课题模式打歌类,联动UserPlay\
|
||||
parameter: `user` - `UserOnline`类或子类的实例\
|
||||
'user_play` - `UserPlay`类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None, user_play=None) -> None:
|
||||
super().__init__(c, user)
|
||||
self.user_play = user_play
|
||||
|
||||
self.score: int = None
|
||||
self.clear_type: int = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'rewards': [x.to_dict() for x in self.items],
|
||||
"current_stamina": self.user.stamina.stamina,
|
||||
"max_stamina_ts": self.user.stamina.max_stamina_ts,
|
||||
'user_course_banners': self.user.course_banners
|
||||
}
|
||||
|
||||
def update(self) -> None:
|
||||
'''课题模式更新'''
|
||||
if self.user_play.health < 0:
|
||||
# 你挂了
|
||||
self.user_play.course_play_state = 5
|
||||
self.score = 0
|
||||
self.clear_type = 0
|
||||
self.user_play.update_play_state_for_course()
|
||||
return None
|
||||
self.user_play.course_play_state += 1
|
||||
self.score += self.user_play.score
|
||||
from .score import Score
|
||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type):
|
||||
self.clear_type = self.user_play.clear_type
|
||||
self.user_play.update_play_state_for_course()
|
||||
|
||||
if self.user_play.course_play_state == 4:
|
||||
self.user.select_user_about_stamina()
|
||||
self.select_course_item()
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
|
||||
self.select_user_course()
|
||||
if not self.is_completed:
|
||||
self.high_score = self.score
|
||||
self.best_clear_type = self.clear_type
|
||||
self.is_completed = True
|
||||
self.insert_user_course()
|
||||
return None
|
||||
|
||||
flag = False
|
||||
if self.score > self.high_score:
|
||||
self.high_score = self.score
|
||||
flag = True
|
||||
if Score.get_song_state(self.clear_type) > Score.get_song_state(self.best_clear_type):
|
||||
self.best_clear_type = self.clear_type
|
||||
flag = True
|
||||
if flag:
|
||||
self.update_user_course()
|
||||
286
latest version/core/download.py
Normal file
286
latest version/core/download.py
Normal file
@@ -0,0 +1,286 @@
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from json import loads
|
||||
from time import time
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .constant import Constant
|
||||
from .error import NoAccess
|
||||
from .limiter import ArcLimiter
|
||||
from .user import User
|
||||
from .util import get_file_md5, md5
|
||||
|
||||
|
||||
@lru_cache(maxsize=8192)
|
||||
def get_song_file_md5(song_id: str, file_name: str) -> str:
|
||||
path = os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, file_name)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
return get_file_md5(path)
|
||||
|
||||
|
||||
def initialize_songfile():
|
||||
'''初始化歌曲数据的md5信息'''
|
||||
get_song_file_md5.cache_clear()
|
||||
x = DownloadList()
|
||||
x.url_flag = False
|
||||
x.add_songs()
|
||||
del x
|
||||
|
||||
|
||||
class SonglistParser:
|
||||
'''songlist文件解析器'''
|
||||
|
||||
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
|
||||
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg']
|
||||
songs: dict = {} # {song_id: value, ...}
|
||||
# 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) -> list:
|
||||
'''判断文件是否允许被下载'''
|
||||
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:
|
||||
'''
|
||||
用户下载类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
limiter = ArcLimiter(
|
||||
str(Constant.DOWNLOAD_TIMES_LIMIT) + '/day', 'download')
|
||||
|
||||
def __init__(self, c_m=None, user=None) -> None:
|
||||
self.c_m = c_m
|
||||
self.user = user
|
||||
|
||||
self.song_id: str = None
|
||||
self.file_name: str = None
|
||||
|
||||
self.token: str = None
|
||||
self.token_time: int = None
|
||||
|
||||
@property
|
||||
def is_limited(self) -> bool:
|
||||
'''是否达到用户最大下载量'''
|
||||
if self.user is None:
|
||||
self.select_for_check()
|
||||
|
||||
return not self.limiter.test(str(self.user.user_id))
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
'''链接是否有效且未过期'''
|
||||
if self.token_time is None:
|
||||
self.select_for_check()
|
||||
return int(time()) - self.token_time <= Constant.DOWNLOAD_TIME_GAP_LIMIT
|
||||
|
||||
def download_hit(self) -> bool:
|
||||
'''下载次数+1,返回成功与否bool值'''
|
||||
return self.limiter.hit(str(self.user.user_id))
|
||||
|
||||
def select_for_check(self) -> None:
|
||||
'''利用token、song_id、file_name查询其它信息'''
|
||||
self.c_m.execute('''select user_id, time from download_token where song_id=? and file_name=? and token = ? limit 1;''',
|
||||
(self.song_id, self.file_name, self.token))
|
||||
|
||||
x = self.c_m.fetchone()
|
||||
if not x:
|
||||
raise NoAccess('The token `%s` is not valid.' %
|
||||
self.token, status=403)
|
||||
self.user = User()
|
||||
self.user.user_id = x[0]
|
||||
self.token_time = x[1]
|
||||
|
||||
def generate_token(self) -> None:
|
||||
self.token_time = int(time())
|
||||
self.token = md5(str(self.user.user_id) + self.song_id +
|
||||
self.file_name + str(self.token_time) + str(os.urandom(8)))
|
||||
|
||||
def insert_download_token(self) -> None:
|
||||
'''将数据插入数据库,让这个下载链接可用'''
|
||||
self.c_m.execute('''insert or replace into download_token values(:a,:b,:c,:d,:e)''', {
|
||||
'a': self.user.user_id, 'b': self.song_id, 'c': self.file_name, 'd': self.token, 'e': self.token_time})
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
'''生成下载链接'''
|
||||
if self.token is None:
|
||||
self.generate_token()
|
||||
# self.insert_download_token() # 循环插入速度慢,改为executemany
|
||||
if Constant.DOWNLOAD_LINK_PREFIX:
|
||||
prefix = Constant.DOWNLOAD_LINK_PREFIX
|
||||
if prefix[-1] != '/':
|
||||
prefix += '/'
|
||||
return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
|
||||
else:
|
||||
return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
|
||||
|
||||
@property
|
||||
def hash(self) -> str:
|
||||
return get_song_file_md5(self.song_id, self.file_name)
|
||||
|
||||
|
||||
class DownloadList(UserDownload):
|
||||
'''
|
||||
下载列表类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c_m=None, user=None) -> None:
|
||||
super().__init__(c_m, user)
|
||||
|
||||
self.song_ids: list = None
|
||||
self.url_flag: bool = None
|
||||
|
||||
self.downloads: list = []
|
||||
self.urls: dict = {}
|
||||
|
||||
@staticmethod
|
||||
def clear_all_cache():
|
||||
'''清除所有歌曲文件有关缓存'''
|
||||
get_song_file_md5.cache_clear()
|
||||
DownloadList.get_one_song_file_names.cache_clear()
|
||||
DownloadList.get_all_song_ids.cache_clear()
|
||||
SonglistParser()
|
||||
|
||||
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.listdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id)):
|
||||
if os.path.isfile(os.path.join(Constant.SONG_FILE_FOLDER_PATH, song_id, i)) and SonglistParser.is_available_file(song_id, i):
|
||||
r.append(i)
|
||||
return r
|
||||
|
||||
def add_one_song(self, song_id: str) -> None:
|
||||
|
||||
re = {}
|
||||
for i in self.get_one_song_file_names(song_id):
|
||||
x = UserDownload(self.c_m, self.user)
|
||||
self.downloads.append(x)
|
||||
x.song_id = song_id
|
||||
x.file_name = i
|
||||
if i == 'base.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
re['audio']["checksum"] = x.hash
|
||||
if self.url_flag:
|
||||
re['audio']["url"] = x.url
|
||||
elif i == '3.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
if self.url_flag:
|
||||
re['audio']['3'] = {"checksum": x.hash, "url": x.url}
|
||||
else:
|
||||
re['audio']['3'] = {"checksum": x.hash}
|
||||
elif i == 'video.mp4' or i == 'video_audio.ogg':
|
||||
if 'additional_files' not in re:
|
||||
re['additional_files'] = []
|
||||
|
||||
if self.url_flag:
|
||||
re['additional_files'].append(
|
||||
{"checksum": x.hash, "url": x.url, 'file_name': i})
|
||||
else:
|
||||
re['additional_files'].append(
|
||||
{"checksum": x.hash, 'file_name': i})
|
||||
else:
|
||||
if 'chart' not in re:
|
||||
re['chart'] = {}
|
||||
|
||||
if self.url_flag:
|
||||
re['chart'][i[0]] = {"checksum": x.hash, "url": x.url}
|
||||
else:
|
||||
re['chart'][i[0]] = {"checksum": x.hash}
|
||||
|
||||
self.urls.update({song_id: re})
|
||||
|
||||
@staticmethod
|
||||
@lru_cache()
|
||||
def get_all_song_ids() -> list:
|
||||
'''获取全歌曲文件夹列表,有lru缓存'''
|
||||
return list(filter(lambda x: os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, x)), os.listdir(Constant.SONG_FILE_FOLDER_PATH)))
|
||||
|
||||
def add_songs(self, song_ids: list = None) -> None:
|
||||
'''添加一个或多个歌曲到下载列表,若`song_ids`为空,则添加所有歌曲'''
|
||||
if song_ids is not None:
|
||||
self.song_ids = song_ids
|
||||
|
||||
if not self.song_ids:
|
||||
self.song_ids = self.get_all_song_ids()
|
||||
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)):
|
||||
self.add_one_song(i)
|
||||
|
||||
if self.url_flag:
|
||||
self.clear_download_token()
|
||||
self.insert_download_tokens()
|
||||
111
latest version/core/error.py
Normal file
111
latest version/core/error.py
Normal file
@@ -0,0 +1,111 @@
|
||||
class ArcError(Exception):
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
self.message: str = message
|
||||
self.error_code: int = error_code
|
||||
self.api_error_code: int = api_error_code
|
||||
self.extra_data = extra_data
|
||||
self.status: int = status
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self.message)
|
||||
|
||||
|
||||
class InputError(ArcError):
|
||||
'''输入类型错误'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class DataExist(ArcError):
|
||||
'''数据存在'''
|
||||
pass
|
||||
|
||||
|
||||
class NoData(ArcError):
|
||||
'''数据不存在'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class PostError(ArcError):
|
||||
'''缺少输入'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class UserBan(ArcError):
|
||||
'''用户封禁'''
|
||||
|
||||
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class ItemNotEnough(ArcError):
|
||||
'''物品数量不足'''
|
||||
|
||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class ItemUnavailable(ArcError):
|
||||
'''物品不可用'''
|
||||
|
||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class RedeemUnavailable(ArcError):
|
||||
'''兑换码不可用'''
|
||||
|
||||
def __init__(self, message=None, error_code=505, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class MapLocked(ArcError):
|
||||
'''地图锁定'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class StaminaNotEnough(ArcError):
|
||||
'''体力不足'''
|
||||
|
||||
def __init__(self, message=None, error_code=107, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class TicketNotEnough(ArcError):
|
||||
'''记忆源点不足'''
|
||||
|
||||
def __init__(self, message=None, error_code=-6, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class FriendError(ArcError):
|
||||
'''好友系统出错'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class NoAccess(ArcError):
|
||||
'''无权限'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=403) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class Timeout(ArcError):
|
||||
'''超时'''
|
||||
pass
|
||||
|
||||
|
||||
class RateLimit(ArcError):
|
||||
'''频率达到限制'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data=None, status=429) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
248
latest version/core/init.py
Normal file
248
latest version/core/init.py
Normal file
@@ -0,0 +1,248 @@
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from json import load
|
||||
from shutil import copy, copy2
|
||||
from time import time
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.constant import ARCAEA_SERVER_VERSION
|
||||
from core.course import Course
|
||||
from core.download import SonglistParser
|
||||
from core.purchase import Purchase
|
||||
from core.sql import Connect, DatabaseMigrator, 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') 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:
|
||||
'''初始化搭档信息'''
|
||||
for i in range(0, len(self.init_data.char)):
|
||||
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]:
|
||||
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, 'r') as f:
|
||||
self.insert_purchase_item(load(f))
|
||||
|
||||
with open(self.single_path, 'r') as f:
|
||||
self.insert_purchase_item(load(f))
|
||||
|
||||
self.c.execute(
|
||||
'''select exists(select * from item where item_id='epilogue')''')
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute(
|
||||
'''insert into item values('epilogue','pack',1)''')
|
||||
|
||||
def course_init(self) -> None:
|
||||
'''初始化课题信息'''
|
||||
courses = []
|
||||
with open(self.course_path, 'r', encoding='utf-8') 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 FileChecker:
|
||||
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
|
||||
def check_file(self, file_path: str) -> bool:
|
||||
f = os.path.isfile(file_path)
|
||||
if not f:
|
||||
self.app.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.app.logger.warning('Folder `%s` is missing.' % folder_path)
|
||||
return f
|
||||
|
||||
def check_update_database(self) -> bool:
|
||||
if not self.check_file(Config.SQLITE_DATABASE_PATH):
|
||||
# 新建数据库
|
||||
try:
|
||||
self.app.logger.info(
|
||||
'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
DatabaseInit().init()
|
||||
self.app.logger.info(
|
||||
'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
except:
|
||||
self.app.logger.warning(
|
||||
'Fail 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.app.logger.warning(
|
||||
'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH)
|
||||
try:
|
||||
self.app.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.app.logger.info(
|
||||
'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH)
|
||||
|
||||
except ValueError:
|
||||
self.app.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)
|
||||
|
||||
def check_before_run(self) -> bool:
|
||||
'''运行前检查,返回布尔值'''
|
||||
# TODO: try
|
||||
MemoryDatabase() # 初始化内存数据库
|
||||
SonglistParser() # 解析songlist
|
||||
return self.check_folder(Config.SONG_FILE_FOLDER_PATH) & self.check_update_database()
|
||||
390
latest version/core/item.py
Normal file
390
latest version/core/item.py
Normal file
@@ -0,0 +1,390 @@
|
||||
from .config_manager import Config
|
||||
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData
|
||||
|
||||
|
||||
class Item:
|
||||
item_type = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.item_id = None
|
||||
self.__amount = None
|
||||
self.is_available = None
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
return self.__amount
|
||||
|
||||
@amount.setter
|
||||
def amount(self, value: int):
|
||||
self.__amount = int(value)
|
||||
|
||||
def to_dict(self, has_is_available: bool = False) -> dict:
|
||||
r = {
|
||||
'id': self.item_id,
|
||||
'amount': self.amount,
|
||||
'type': self.item_type
|
||||
}
|
||||
if has_is_available:
|
||||
r['is_available'] = self.is_available
|
||||
return r
|
||||
|
||||
def user_claim_item(self, user):
|
||||
# parameter: user - User类或子类的实例
|
||||
pass
|
||||
|
||||
|
||||
class UserItem(Item):
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.user = None
|
||||
|
||||
def select(self, user=None):
|
||||
'''
|
||||
查询用户item\
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
self.c.execute('''select amount from user_item where user_id=? and item_id=? and type=?''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.amount = x[0] if x[0] is not None else 1
|
||||
else:
|
||||
self.amount = 0
|
||||
|
||||
|
||||
class NormalItem(UserItem):
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def user_claim_item(self, user):
|
||||
self.user = user
|
||||
if not self.is_available:
|
||||
self.c.execute(
|
||||
'''select is_available from item where item_id=? and type=?''', (self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
if x[0] == 0:
|
||||
self.is_available = False
|
||||
raise ItemUnavailable('The item is unavailable.')
|
||||
else:
|
||||
self.is_available = True
|
||||
else:
|
||||
raise NoData('No item data.')
|
||||
|
||||
self.c.execute('''select exists(select * from user_item where user_id=? and item_id=? and type=?)''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute('''insert into user_item values(:a,:b,:c,1)''',
|
||||
{'a': self.user.user_id, 'b': self.item_id, 'c': self.item_type})
|
||||
|
||||
|
||||
class PositiveItem(UserItem):
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''添加或使用用户item,注意是+amount'''
|
||||
self.user = user
|
||||
self.c.execute('''select amount from user_item where user_id=? and item_id=? and type=?''',
|
||||
(self.user.user_id, self.item_id, self.item_type))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
if x[0] + self.amount < 0: # 数量不足
|
||||
raise ItemNotEnough(
|
||||
'The user does not have enough `%s`.' % self.item_id)
|
||||
self.c.execute('''update user_item set amount=? where user_id=? and item_id=? and type=?''',
|
||||
(x[0]+self.amount, self.user.user_id, self.item_id, self.item_type))
|
||||
else:
|
||||
if self.amount < 0: # 添加数量错误
|
||||
raise InputError(
|
||||
'The amount of `%s` is wrong.' % self.item_id)
|
||||
self.c.execute('''insert into user_item values(?,?,?,?)''',
|
||||
(self.user.user_id, self.item_id, self.item_type, self.amount))
|
||||
|
||||
|
||||
class ItemCore(PositiveItem):
|
||||
item_type = 'core'
|
||||
|
||||
def __init__(self, c, core=None, reverse=False) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
if core:
|
||||
self.item_id = core.item_id
|
||||
self.amount = - core.amount if reverse else core.amount
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.item_id + '_' + str(self.amount)
|
||||
|
||||
|
||||
class ItemCharacter(UserItem):
|
||||
item_type = 'character'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
|
||||
def set_id(self, character_id: str) -> None:
|
||||
# 将name: str转为character_id: int存到item_id里
|
||||
if character_id.isdigit():
|
||||
self.item_id = int(character_id)
|
||||
else:
|
||||
self.c.execute(
|
||||
'''select character_id from character where name=?''', (character_id,))
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.item_id = x[0]
|
||||
else:
|
||||
raise NoData('No character `%s`.' % character_id)
|
||||
|
||||
def user_claim_item(self, user):
|
||||
if not isinstance(self.item_id, int):
|
||||
self.set_id(self.item_id)
|
||||
|
||||
self.c.execute(
|
||||
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user.user_id, self.item_id))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute(
|
||||
'''insert into user_char values(?,?,1,0,0,0)''', (user.user_id, self.item_id))
|
||||
|
||||
|
||||
class Memory(UserItem):
|
||||
item_type = 'memory'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
|
||||
def user_claim_item(self, user):
|
||||
self.c.execute(
|
||||
'''select ticket from user where user_id=?''', (user.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
self.c.execute('''update user set ticket=? where user_id=?''',
|
||||
(x[0]+self.amount, user.user_id))
|
||||
else:
|
||||
raise NoData('The ticket of the user is null.')
|
||||
|
||||
|
||||
class Fragment(UserItem):
|
||||
item_type = 'fragment'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.is_available = True
|
||||
|
||||
def user_claim_item(self, user):
|
||||
pass
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'fragment' + str(self.amount)
|
||||
|
||||
|
||||
class Anni5tix(PositiveItem):
|
||||
item_type = 'anni5tix'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
|
||||
class WorldSong(NormalItem):
|
||||
item_type = 'world_song'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
|
||||
class WorldUnlock(NormalItem):
|
||||
item_type = 'world_unlock'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
|
||||
class CourseBanner(NormalItem):
|
||||
item_type = 'course_banner'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
self.is_available = True
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.item_id)
|
||||
|
||||
|
||||
class Single(NormalItem):
|
||||
item_type = 'single'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
|
||||
|
||||
class Pack(NormalItem):
|
||||
item_type = 'pack'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
|
||||
|
||||
class ProgBoost(UserItem):
|
||||
item_type = 'prog_boost_300'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''
|
||||
世界模式prog_boost\
|
||||
parameters: `user` - `UserOnline`类或子类的实例
|
||||
'''
|
||||
user.update_user_one_column('prog_boost', 300)
|
||||
|
||||
|
||||
class Stamina6(UserItem):
|
||||
item_type = 'stamina6'
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__(c)
|
||||
|
||||
def user_claim_item(self, user):
|
||||
'''
|
||||
世界模式记忆源点或残片买体力+6\
|
||||
顺手清一下世界模式过载状态
|
||||
'''
|
||||
user.select_user_about_stamina()
|
||||
user.stamina.stamina += 6
|
||||
user.stamina.update()
|
||||
user.update_user_one_column('world_mode_locked_end_ts', -1)
|
||||
|
||||
|
||||
class ItemFactory:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
|
||||
def get_item(self, item_type: str):
|
||||
'''
|
||||
根据item_type实例化对应的item类
|
||||
return: Item类或子类的实例
|
||||
'''
|
||||
if item_type == 'core':
|
||||
return ItemCore(self.c)
|
||||
elif item_type == 'character':
|
||||
return ItemCharacter(self.c)
|
||||
elif item_type == 'memory':
|
||||
return Memory(self.c)
|
||||
elif item_type == 'anni5tix':
|
||||
return Anni5tix(self.c)
|
||||
elif item_type == 'world_song':
|
||||
return WorldSong(self.c)
|
||||
elif item_type == 'world_unlock':
|
||||
return WorldUnlock(self.c)
|
||||
elif item_type == 'single':
|
||||
return Single(self.c)
|
||||
elif item_type == 'pack':
|
||||
return Pack(self.c)
|
||||
elif item_type == 'fragment':
|
||||
return Fragment(self.c)
|
||||
elif item_type == 'prog_boost_300':
|
||||
return ProgBoost(self.c)
|
||||
elif item_type == 'stamina6':
|
||||
return Stamina6(self.c)
|
||||
elif item_type == 'course_banner':
|
||||
return CourseBanner(self.c)
|
||||
else:
|
||||
raise InputError('The item type `%s` is wrong.' % item_type)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict, c=None):
|
||||
'''注意这里没有处理character_id的转化,是为了世界模式的map服务的'''
|
||||
if 'item_type' in d:
|
||||
item_type = d['item_type']
|
||||
elif 'type' in d:
|
||||
item_type = d['type']
|
||||
else:
|
||||
raise InputError('The dict of item is wrong.')
|
||||
i = cls().get_item(item_type)
|
||||
if c is not None:
|
||||
i.c = c
|
||||
if 'item_id' in d:
|
||||
i.item_id = d['item_id']
|
||||
elif 'id' in d:
|
||||
i.item_id = d['id']
|
||||
else:
|
||||
i.item_id = item_type
|
||||
i.amount = d.get('amount', 1)
|
||||
i.is_available = d.get('is_available', True)
|
||||
return i
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s: str, c=None):
|
||||
if s.startswith('fragment'):
|
||||
item_type = 'fragment'
|
||||
item_id = 'fragment'
|
||||
amount = int(s[8:])
|
||||
elif s.startswith('core'):
|
||||
item_type = 'core'
|
||||
x = s.split('_')
|
||||
item_id = x[0] + '_' + x[1]
|
||||
amount = int(x[-1])
|
||||
elif s.startswith('course_banner'):
|
||||
item_type = 'course_banner'
|
||||
item_id = s
|
||||
amount = 1
|
||||
else:
|
||||
raise InputError('The string of item is wrong.')
|
||||
i = cls().get_item(item_type)
|
||||
if c is not None:
|
||||
i.c = c
|
||||
i.item_id = item_id
|
||||
i.amount = amount
|
||||
i.is_available = True
|
||||
return i
|
||||
|
||||
|
||||
class UserItemList:
|
||||
'''
|
||||
用户的item列表\
|
||||
注意只能查在user_item里面的,character不行\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.items: list = []
|
||||
|
||||
def select_from_type(self, item_type: str) -> 'UserItemList':
|
||||
'''
|
||||
根据item_type搜索用户的item
|
||||
'''
|
||||
if Config.WORLD_SONG_FULL_UNLOCK and item_type == 'world_song' or Config.WORLD_SCENERY_FULL_UNLOCK and item_type == 'world_unlock':
|
||||
self.c.execute(
|
||||
'''select item_id from item where type=?''', (item_type,))
|
||||
else:
|
||||
self.c.execute('''select item_id, amount from user_item where type = :a and user_id = :b''', {
|
||||
'a': item_type, 'b': self.user.user_id})
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return self
|
||||
|
||||
self.items: list = []
|
||||
for i in x:
|
||||
if len(i) > 1:
|
||||
amount = i[1] if i[1] is not None else 1
|
||||
else:
|
||||
amount = 1
|
||||
self.items.append(ItemFactory.from_dict(
|
||||
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
|
||||
return self
|
||||
31
latest version/core/limiter.py
Normal file
31
latest version/core/limiter.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from limits import parse_many, strategies
|
||||
from limits.storage import storage_from_string
|
||||
|
||||
|
||||
class ArcLimiter:
|
||||
storage = storage_from_string("memory://")
|
||||
strategy = strategies.FixedWindowRateLimiter(storage)
|
||||
|
||||
def __init__(self, limit_str: str = None, namespace: str = None):
|
||||
self._limits: list = None
|
||||
self.limits = limit_str
|
||||
self.namespace = namespace
|
||||
|
||||
@property
|
||||
def limits(self) -> list:
|
||||
return self._limits
|
||||
|
||||
@limits.setter
|
||||
def limits(self, value: str):
|
||||
if value is None:
|
||||
return
|
||||
self._limits = parse_many(value)
|
||||
|
||||
def hit(self, key: str, cost: int = 1) -> bool:
|
||||
flag = True
|
||||
for limit in self.limits:
|
||||
flag &= self.strategy.hit(limit, self.namespace, key, cost)
|
||||
return flag
|
||||
|
||||
def test(self, key: str) -> bool:
|
||||
return all(self.strategy.test(limit, self.namespace, key) for limit in self.limits)
|
||||
164
latest version/core/linkplay.py
Normal file
164
latest version/core/linkplay.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import socket
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from core.error import ArcError, Timeout
|
||||
|
||||
from .constant import Constant
|
||||
from .user import UserInfo
|
||||
|
||||
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
|
||||
|
||||
|
||||
def get_song_unlock(client_song_map: dict) -> bytes:
|
||||
'''处理可用歌曲bit,返回bytes'''
|
||||
|
||||
user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH
|
||||
for i in range(0, Constant.LINKPLAY_UNLOCK_LENGTH*2, 2):
|
||||
x = 0
|
||||
y = 0
|
||||
if str(i) in client_song_map:
|
||||
if client_song_map[str(i)][0]:
|
||||
x += 1
|
||||
if client_song_map[str(i)][1]:
|
||||
x += 2
|
||||
if client_song_map[str(i)][2]:
|
||||
x += 4
|
||||
if client_song_map[str(i)][3]:
|
||||
x += 8
|
||||
if str(i+1) in client_song_map:
|
||||
if client_song_map[str(i+1)][0]:
|
||||
y += 1
|
||||
if client_song_map[str(i+1)][1]:
|
||||
y += 2
|
||||
if client_song_map[str(i+1)][2]:
|
||||
y += 4
|
||||
if client_song_map[str(i+1)][3]:
|
||||
y += 8
|
||||
|
||||
user_song_unlock[i // 2] = y*16 + x
|
||||
|
||||
return bytes(user_song_unlock)
|
||||
|
||||
|
||||
class Player(UserInfo):
|
||||
def __init__(self, c=None, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
self.player_id: int = 0
|
||||
self.token: int = 0
|
||||
self.key: bytes = None
|
||||
|
||||
self.__song_unlock: bytes = None
|
||||
self.client_song_map: dict = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'userId': self.user_id,
|
||||
'playerId': str(self.player_id),
|
||||
'token': str(self.token),
|
||||
'key': (b64encode(self.key)).decode()
|
||||
}
|
||||
|
||||
@property
|
||||
def song_unlock(self) -> bytes:
|
||||
if self.__song_unlock is None:
|
||||
self.get_song_unlock()
|
||||
return self.__song_unlock
|
||||
|
||||
def get_song_unlock(self, client_song_map: dict = None) -> bytes:
|
||||
if client_song_map is not None:
|
||||
self.client_song_map = client_song_map
|
||||
self.__song_unlock = get_song_unlock(self.client_song_map)
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self) -> None:
|
||||
self.room_id: int = 0
|
||||
self.room_code: str = 'AAAA00'
|
||||
|
||||
self.song_unlock: bytes = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'roomId': str(self.room_id),
|
||||
'roomCode': self.room_code,
|
||||
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode()
|
||||
}
|
||||
|
||||
|
||||
class RemoteMultiPlayer:
|
||||
def __init__(self) -> None:
|
||||
self.user: 'Player' = None
|
||||
self.room: 'Room' = None
|
||||
|
||||
self.data_recv: tuple = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return dict(self.room.to_dict(), **self.user.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def tcp(data: str) -> str:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.connect((Constant.LINKPLAY_HOST,
|
||||
Constant.LINKPLAY_TCP_PORT))
|
||||
sock.sendall(bytes(data + "\n", "utf-8"))
|
||||
try:
|
||||
received = str(sock.recv(1024), "utf-8").strip()
|
||||
except socket.timeout:
|
||||
raise Timeout(
|
||||
'Timeout when waiting for data from link play server.', status=400)
|
||||
# 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':
|
||||
raise ArcError('Link Play error.',
|
||||
int(self.data_recv[0]), status=400)
|
||||
|
||||
def create_room(self, user: 'Player' = None) -> None:
|
||||
'''创建房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
user.select_user_one_column('name')
|
||||
self.data_swap((1, self.user.name, b64encode(
|
||||
self.user.song_unlock).decode('utf-8')))
|
||||
self.room = Room()
|
||||
self.room.room_code = self.data_recv[1]
|
||||
self.room.room_id = int(self.data_recv[2])
|
||||
self.room.song_unlock = self.user.song_unlock
|
||||
self.user.token = int(self.data_recv[3])
|
||||
self.user.key = b64decode(self.data_recv[4])
|
||||
self.user.player_id = int(self.data_recv[5])
|
||||
|
||||
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
|
||||
'''加入房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
if room is not None:
|
||||
self.room = room
|
||||
|
||||
self.user.select_user_one_column('name')
|
||||
self.data_swap(
|
||||
(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_id = int(self.data_recv[2])
|
||||
self.room.song_unlock = b64decode(self.data_recv[6])
|
||||
self.user.token = int(self.data_recv[3])
|
||||
self.user.key = b64decode(self.data_recv[4])
|
||||
self.user.player_id = int(self.data_recv[5])
|
||||
|
||||
def update_room(self, user: 'Player' = None) -> None:
|
||||
'''更新房间'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
self.data_swap((3, self.user.token))
|
||||
self.room = Room()
|
||||
self.room.room_code = self.data_recv[1]
|
||||
self.room.room_id = int(self.data_recv[2])
|
||||
self.room.song_unlock = b64decode(self.data_recv[5])
|
||||
self.user.key = b64decode(self.data_recv[3])
|
||||
self.user.player_id = int(self.data_recv[4])
|
||||
137
latest version/core/present.py
Normal file
137
latest version/core/present.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from time import time
|
||||
|
||||
from core.item import ItemFactory
|
||||
|
||||
from .error import ArcError, NoData
|
||||
|
||||
|
||||
class Present:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.present_id: str = None
|
||||
self.expire_ts: int = None
|
||||
self.description: str = None
|
||||
|
||||
self.items: list = None
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return self.expire_ts < int(time() * 1000)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'present_id': self.present_id,
|
||||
'expire_ts': self.expire_ts,
|
||||
'description': self.description,
|
||||
'items': [x.to_dict() for x in self.items]
|
||||
}
|
||||
|
||||
def select(self, present_id: str = None) -> None:
|
||||
'''
|
||||
用present_id查询信息
|
||||
'''
|
||||
if present_id:
|
||||
self.present_id = present_id
|
||||
|
||||
self.c.execute(
|
||||
'''select * from present where present_id=:a''', {'a': self.present_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The present `%s` does not exist.' % self.present_id)
|
||||
|
||||
self.expire_ts = x[1] if x[1] else 0
|
||||
self.description = x[2] if x[2] else ''
|
||||
|
||||
def select_items(self) -> None:
|
||||
'''
|
||||
查询奖励的物品
|
||||
'''
|
||||
self.c.execute(
|
||||
'''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({
|
||||
'item_id': i[1],
|
||||
'type': i[2],
|
||||
'amount': i[3] if i[3] else 1
|
||||
}, self.c) for i in x]
|
||||
|
||||
|
||||
class UserPresent(Present):
|
||||
'''
|
||||
用户登录奖励类\
|
||||
忽视了description的多语言\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
def delete_user_present(self) -> None:
|
||||
'''
|
||||
删除用户奖励
|
||||
'''
|
||||
self.c.execute('''delete from user_present where user_id=:a and present_id=:b''',
|
||||
{'a': self.user.user_id, 'b': self.present_id})
|
||||
|
||||
def claim_user_present(self, present_id: str = None, user=None) -> None:
|
||||
'''
|
||||
确认并删除用户奖励
|
||||
'''
|
||||
if present_id:
|
||||
self.present_id = present_id
|
||||
if user:
|
||||
self.user = user
|
||||
if self.expire_ts is None:
|
||||
self.select()
|
||||
if self.items is None:
|
||||
self.select_items()
|
||||
|
||||
self.c.execute('''select exists(select * from user_present where user_id=:a and present_id=:b)''',
|
||||
{'a': self.user.user_id, 'b': self.present_id})
|
||||
|
||||
if self.c.fetchone() == (0,):
|
||||
raise NoData('The present `%s` for the user `%s` does not exist.' % (
|
||||
self.present_id, self.user.user_id))
|
||||
|
||||
self.delete_user_present()
|
||||
|
||||
if self.is_expired:
|
||||
raise ArcError('The present `%s` has expired.' % self.present_id)
|
||||
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
|
||||
|
||||
class UserPresentList:
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.presents: list = None
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.presents]
|
||||
|
||||
def select_user_presents(self) -> None:
|
||||
'''
|
||||
查询用户全部奖励
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select * from present where present_id in (select present_id from user_present where user_id=:a)''', {'a': self.user.user_id})
|
||||
x = self.c.fetchall()
|
||||
self.presents = []
|
||||
if not x:
|
||||
return None
|
||||
|
||||
for i in x:
|
||||
p = UserPresent(self.c, self.user)
|
||||
p.present_id = i[0]
|
||||
p.expire_ts = i[1]
|
||||
p.description = i[2]
|
||||
if not p.is_expired:
|
||||
p.select_items()
|
||||
self.presents.append(p)
|
||||
188
latest version/core/purchase.py
Normal file
188
latest version/core/purchase.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from time import time
|
||||
|
||||
from .error import NoData, TicketNotEnough
|
||||
from .item import ItemFactory
|
||||
|
||||
|
||||
class Purchase:
|
||||
'''
|
||||
购买类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.purchase_name: str = None
|
||||
self.price: int = None
|
||||
self.orig_price: int = None
|
||||
self.discount_from: int = None
|
||||
self.discount_to: int = None
|
||||
self.discount_reason: str = None
|
||||
|
||||
self.items: list = []
|
||||
|
||||
@property
|
||||
def price_displayed(self) -> int:
|
||||
'''
|
||||
返回显示的价格
|
||||
'''
|
||||
if self.discount_from > 0 and self.discount_to > 0:
|
||||
if self.discount_from <= int(time() * 1000) <= self.discount_to:
|
||||
if self.discount_reason == 'anni5tix':
|
||||
x = ItemFactory(self.c).get_item('anni5tix')
|
||||
x.item_id = 'anni5tix'
|
||||
x.select(self.user)
|
||||
if x.amount >= 1:
|
||||
return 0
|
||||
return self.price
|
||||
return self.orig_price
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
price = self.price_displayed
|
||||
r = {
|
||||
'name': self.purchase_name,
|
||||
'price': price,
|
||||
'orig_price': self.orig_price,
|
||||
'items': [x.to_dict(has_is_available=True) for x in self.items]
|
||||
}
|
||||
if self.discount_from > 0 and self.discount_to > 0:
|
||||
r['discount_from'] = self.discount_from
|
||||
r['discount_to'] = self.discount_to
|
||||
if self.discount_reason == 'anni5tix' and price == 0:
|
||||
r['discount_reason'] = self.discount_reason
|
||||
return r
|
||||
|
||||
def from_dict(self, d: dict) -> 'Purchase':
|
||||
self.purchase_name = d['name']
|
||||
self.price = d['price']
|
||||
self.orig_price = d['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 insert_all(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))
|
||||
self.insert_items()
|
||||
|
||||
def insert_items(self) -> None:
|
||||
'''向数据库插入物品,注意已存在的物品不会变更'''
|
||||
for i in self.items:
|
||||
self.c.execute(
|
||||
'''select exists(select * from item where item_id=?)''', (i.item_id,))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute('''insert into item values(?,?,?)''',
|
||||
(i.item_id, i.item_type, i.is_available))
|
||||
|
||||
self.c.execute('''insert into purchase_item values(?,?,?,?)''',
|
||||
(self.purchase_name, i.item_id, i.item_type, i.amount))
|
||||
|
||||
def select(self, purchase_name: str = None) -> 'Purchase':
|
||||
'''
|
||||
用purchase_name查询信息
|
||||
'''
|
||||
if purchase_name:
|
||||
self.purchase_name = purchase_name
|
||||
|
||||
self.c.execute(
|
||||
'''select * from purchase where purchase_name=:name''', {'name': purchase_name})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('The purchase `%s` does not exist.' %
|
||||
purchase_name, 501)
|
||||
|
||||
self.price = x[1]
|
||||
self.orig_price = x[2]
|
||||
self.discount_from = x[3] if x[3] else -1
|
||||
self.discount_to = x[4] if x[4] else -1
|
||||
self.discount_reason = x[5] if x[5] else ''
|
||||
self.select_items()
|
||||
return self
|
||||
|
||||
def select_items(self) -> None:
|
||||
'''从数据库拉取purchase_item数据'''
|
||||
self.c.execute(
|
||||
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': self.purchase_name})
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
raise NoData('The items of the purchase `%s` does not exist.' %
|
||||
self.purchase_name, 501)
|
||||
|
||||
self.items = []
|
||||
t = None
|
||||
|
||||
for i in x:
|
||||
if i[0] == self.purchase_name:
|
||||
# 物品排序,否则客户端报错
|
||||
t = ItemFactory.from_dict({
|
||||
'item_id': i[0],
|
||||
'type': i[1],
|
||||
'amount': i[2] if i[2] else 1
|
||||
}, self.c)
|
||||
else:
|
||||
self.items.append(ItemFactory.from_dict({
|
||||
'item_id': i[0],
|
||||
'type': i[1],
|
||||
'amount': i[2] if i[2] else 1
|
||||
}, self.c))
|
||||
if t is not None:
|
||||
self.items = [t] + self.items
|
||||
|
||||
def buy(self) -> None:
|
||||
'''进行购买'''
|
||||
if self.price is None or self.orig_price is None:
|
||||
self.select()
|
||||
if not self.items:
|
||||
self.select_items()
|
||||
self.user.select_user_one_column('ticket', 0)
|
||||
price_used = self.price_displayed
|
||||
|
||||
if self.user.ticket < price_used:
|
||||
raise TicketNotEnough(
|
||||
'The user does not have enough memories.', -6)
|
||||
|
||||
if not(self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to):
|
||||
if price_used == 0:
|
||||
x = ItemFactory(self.c).get_item('anni5tix')
|
||||
x.item_id = 'anni5tix'
|
||||
x.amount = -1
|
||||
x.user_claim_item(self.user)
|
||||
else:
|
||||
self.user.ticket -= price_used
|
||||
self.user.update_user_one_column('ticket')
|
||||
|
||||
for i in self.items:
|
||||
i.user_claim_item(self.user)
|
||||
|
||||
|
||||
class PurchaseList:
|
||||
'''
|
||||
购买列表类\
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.purchases: list = []
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.purchases]
|
||||
|
||||
def select_from_type(self, item_type: str) -> 'PurchaseList':
|
||||
self.c.execute('''select purchase_name from purchase_item where type = :a''', {
|
||||
'a': item_type})
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return self
|
||||
|
||||
self.purchases: list = []
|
||||
for i in x:
|
||||
self.purchases.append(Purchase(self.c, self.user).select(i[0]))
|
||||
return self
|
||||
157
latest version/core/rank.py
Normal file
157
latest version/core/rank.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from .constant import Constant
|
||||
from .score import UserScore
|
||||
from .song import Chart
|
||||
from .sql import Query, Sql
|
||||
from .user import UserInfo
|
||||
|
||||
|
||||
class RankList:
|
||||
'''
|
||||
排行榜类\
|
||||
默认limit=20,limit<0认为是all\
|
||||
property: `user` - `User`类或者子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.list: list = []
|
||||
self.song = Chart()
|
||||
self.limit: int = 20
|
||||
self.user = None
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict() for x in self.list]
|
||||
|
||||
def select_top(self) -> None:
|
||||
'''
|
||||
得到top分数表
|
||||
'''
|
||||
if self.limit >= 0:
|
||||
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': self.limit})
|
||||
else:
|
||||
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', {
|
||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
|
||||
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return None
|
||||
|
||||
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||
|
||||
rank = 0
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
def select_friend(self, user=None, limit=Constant.MAX_FRIEND_COUNT) -> None:
|
||||
'''
|
||||
得到用户好友分数表
|
||||
'''
|
||||
self.limit = limit
|
||||
if user:
|
||||
self.user = user
|
||||
|
||||
user_ids = [self.user.user_id] + [x[0] for x in self.user.friend_ids]
|
||||
|
||||
self.c.execute(f'''select * from best_score where user_id in ({','.join(['?'] * len(user_ids))}) and song_id = ? and difficulty = ? order by score DESC, time_played DESC limit ?''', user_ids + [
|
||||
self.song.song_id, self.song.difficulty, self.limit])
|
||||
x = self.c.fetchall()
|
||||
if not x:
|
||||
return None
|
||||
|
||||
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||
rank = 0
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
@staticmethod
|
||||
def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION):
|
||||
'''
|
||||
计算我的排名中的查询参数\
|
||||
returns:
|
||||
`sql_limit`: int - 查询limit参数
|
||||
`sql_offset`: int - 查询offset参数
|
||||
`need_myself`: bool - 是否需要在排名结尾添加自己
|
||||
'''
|
||||
sql_limit = all_limit
|
||||
sql_offset = 0
|
||||
need_myself = False
|
||||
|
||||
if my_rank <= max_local_position: # 排名在前面,前方人数不足
|
||||
pass
|
||||
elif my_rank > max_global_position: # 排名太后了,不显示排名
|
||||
sql_limit -= 1
|
||||
sql_offset = max_global_position - all_limit + 1
|
||||
need_myself = True
|
||||
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
|
||||
sql_offset = amount - all_limit
|
||||
elif my_rank >= max_local_position and my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
|
||||
sql_offset = my_rank - max_local_position
|
||||
else: # 我已经忘了这是什么了
|
||||
sql_offset = max_global_position - all_limit
|
||||
|
||||
return sql_limit, sql_offset, need_myself
|
||||
|
||||
def select_me(self, user=None) -> None:
|
||||
'''
|
||||
得到我的排名分数表
|
||||
'''
|
||||
if user:
|
||||
self.user = user
|
||||
self.c.execute('''select score, time_played from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty''', {
|
||||
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
return None
|
||||
|
||||
self.c.execute('''select count(*) from best_score where song_id = :song_id and difficulty = :difficulty and ( score > :score or (score = :score and time_played > :time_played) )''', {
|
||||
'user_id': self.user.user_id, 'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'score': x[0], 'time_played': x[1]})
|
||||
my_rank = int(self.c.fetchone()[0]) + 1
|
||||
self.c.execute('''select count(*) from best_score where song_id=:a and difficulty=:b''',
|
||||
{'a': self.song.song_id, 'b': self.song.difficulty})
|
||||
|
||||
sql_limit, sql_offset, need_myself = self.get_my_rank_parameter(
|
||||
my_rank, int(self.c.fetchone()[0]), self.limit)
|
||||
self.c.execute('''select * from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||
'song_id': self.song.song_id, 'difficulty': self.song.difficulty, 'limit': sql_limit, 'offset': sql_offset})
|
||||
x = self.c.fetchall()
|
||||
|
||||
if x:
|
||||
user_info_list = Sql(self.c).select('user', ['user_id', 'name', 'character_id', 'is_skill_sealed', 'is_char_uncapped',
|
||||
'is_char_uncapped_override', 'favorite_character'], Query().from_args({'user_id': [i[0] for i in x]}))
|
||||
user_info_dict = {i[0]: i[1:] for i in user_info_list}
|
||||
rank = sql_offset if sql_offset > 0 else 0
|
||||
self.list = []
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = UserScore(self.c, UserInfo(self.c, i[0])).from_list(i)
|
||||
y.song = self.song
|
||||
y.rank = rank
|
||||
y.user.from_list_about_character(user_info_dict[i[0]])
|
||||
|
||||
self.list.append(y)
|
||||
|
||||
if need_myself:
|
||||
y = UserScore(self.c, UserInfo(self.c, self.user.user_id))
|
||||
y.song = self.song
|
||||
y.select_score()
|
||||
y.rank = -1
|
||||
self.list.append(y)
|
||||
94
latest version/core/redeem.py
Normal file
94
latest version/core/redeem.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from .error import NoData, RedeemUnavailable
|
||||
from .item import ItemFactory
|
||||
|
||||
|
||||
class Redeem:
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.code: str = None
|
||||
self.redeem_type: int = None
|
||||
|
||||
self.items: list = []
|
||||
self.fragment: int = None
|
||||
|
||||
def select(self, code: str = None) -> None:
|
||||
if code:
|
||||
self.code = code
|
||||
self.c.execute('''select * from redeem where code=:a''',
|
||||
{'a': self.code})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The redeem `%s` does not exist.' % self.code, 504)
|
||||
|
||||
self.redeem_type = x[1]
|
||||
|
||||
def select_items(self) -> None:
|
||||
self.c.execute('''select * from redeem_item where code=:a''',
|
||||
{'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({
|
||||
'item_id': i[1],
|
||||
'type': i[2],
|
||||
'amount': i[3] if i[3] else 1
|
||||
}, self.c) for i in x]
|
||||
|
||||
|
||||
class UserRedeem(Redeem):
|
||||
'''
|
||||
用户兑换码类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c)
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
if self.redeem_type is None:
|
||||
self.select()
|
||||
|
||||
if self.redeem_type == 0:
|
||||
# 一次性
|
||||
self.c.execute(
|
||||
'''select exists(select * from user_redeem where code=:a)''', {'a': self.code})
|
||||
if self.c.fetchone() == (1,):
|
||||
return False
|
||||
elif self.redeem_type == 1:
|
||||
# 每个玩家一次
|
||||
self.c.execute('''select exists(select * from user_redeem where code=:a and user_id=:b)''',
|
||||
{'a': self.code, 'b': self.user.user_id})
|
||||
if self.c.fetchone() == (1,):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def insert_user_redeem(self) -> None:
|
||||
self.c.execute('''insert into user_redeem values(:b,:a)''',
|
||||
{'a': self.code, 'b': self.user.user_id})
|
||||
|
||||
def claim_user_redeem(self, code: str = None) -> None:
|
||||
if code:
|
||||
self.code = code
|
||||
if not self.is_available:
|
||||
if self.redeem_type == 0:
|
||||
raise RedeemUnavailable(
|
||||
'The redeem `%s` is unavailable.' % self.code)
|
||||
elif self.redeem_type == 1:
|
||||
raise RedeemUnavailable(
|
||||
'The redeem `%s` is unavailable.' % self.code, 506)
|
||||
|
||||
if not self.items:
|
||||
self.select_items()
|
||||
|
||||
self.insert_user_redeem()
|
||||
|
||||
self.fragment = 0
|
||||
for i in self.items:
|
||||
if i.item_type == 'fragment':
|
||||
self.fragment += i.amount
|
||||
else:
|
||||
i.user_claim_item(self.user)
|
||||
121
latest version/core/save.py
Normal file
121
latest version/core/save.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import json
|
||||
from time import time
|
||||
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import InputError
|
||||
from .util import md5
|
||||
|
||||
|
||||
class SaveData:
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
self.scores_data = []
|
||||
self.clearlamps_data = []
|
||||
self.clearedsongs_data = []
|
||||
self.unlocklist_data = []
|
||||
self.installid_data: str = ''
|
||||
self.devicemodelname_data: str = ''
|
||||
self.story_data = []
|
||||
self.createdAt: int = 0
|
||||
self.finalestate_data: str = ''
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"user_id": self.user.user_id,
|
||||
"story": {
|
||||
"": self.story_data
|
||||
},
|
||||
"devicemodelname": {
|
||||
"val": self.devicemodelname_data
|
||||
},
|
||||
"installid": {
|
||||
"val": self.installid_data
|
||||
},
|
||||
"unlocklist": {
|
||||
"": self.unlocklist_data
|
||||
},
|
||||
"clearedsongs": {
|
||||
"": self.clearedsongs_data
|
||||
},
|
||||
"clearlamps": {
|
||||
"": self.clearlamps_data
|
||||
},
|
||||
"scores": {
|
||||
"": self.scores_data
|
||||
},
|
||||
"version": {
|
||||
"val": 1
|
||||
},
|
||||
"createdAt": self.createdAt,
|
||||
'finalestate': {
|
||||
'val': self.finalestate_data
|
||||
}
|
||||
}
|
||||
|
||||
def select_all(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.user = user
|
||||
self.c.execute('''select * from user_save where user_id=:a''',
|
||||
{'a': user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.scores_data = json.loads(x[1])[""]
|
||||
self.clearlamps_data = json.loads(x[2])[""]
|
||||
self.clearedsongs_data = json.loads(x[3])[""]
|
||||
self.unlocklist_data = json.loads(x[4])[""]
|
||||
self.installid_data = json.loads(x[5])["val"]
|
||||
self.devicemodelname_data = json.loads(x[6])["val"]
|
||||
self.story_data = json.loads(x[7])[""]
|
||||
if x[8] is not None:
|
||||
self.createdAt = int(x[8])
|
||||
self.finalestate_data = x[9] if x[9] is not None else ''
|
||||
|
||||
if Config.SAVE_FULL_UNLOCK:
|
||||
self.installid_data = "0fcec8ed-7b62-48e2-9d61-55041a22b123" # 使得可以进入存档选择上传或下载界面
|
||||
for i in self.story_data:
|
||||
i['c'] = True
|
||||
i['r'] = True
|
||||
for i in self.unlocklist_data:
|
||||
if i['unlock_key'][-3:] == '101':
|
||||
i['complete'] = 100
|
||||
elif i['unlock_key'][:16] == 'aegleseeker|2|3|':
|
||||
i['complete'] = 10
|
||||
elif i['unlock_key'] == 'saikyostronger|2|3|einherjar|2':
|
||||
i['complete'] = 6
|
||||
elif i['unlock_key'] == 'saikyostronger|2|3|laqryma|2':
|
||||
i['complete'] = 3
|
||||
else:
|
||||
i['complete'] = 1
|
||||
|
||||
self.finalestate_data = '|'.join(
|
||||
['0', '100'] + [str(x[0]) if i in [64, 65, 66, 71] else str(x[1]) for i, x in enumerate(Constant.FINALE_SWITCH)] + ['1337'])
|
||||
|
||||
def update_all(self, user) -> None:
|
||||
'''
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
self.createdAt = int(time() * 1000)
|
||||
self.c.execute('''insert or replace into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j)''', {
|
||||
'a': user.user_id, 'b': json.dumps({'': self.scores_data}), 'c': json.dumps({'': self.clearlamps_data}), 'd': json.dumps({'': self.clearedsongs_data}), 'e': json.dumps({'': self.unlocklist_data}), 'f': json.dumps({'val': self.installid_data}), 'g': json.dumps({'val': self.devicemodelname_data}), 'h': json.dumps({'': self.story_data}), 'i': self.createdAt, 'j': self.finalestate_data})
|
||||
|
||||
def set_value(self, key: str, value: str, checksum: str) -> None:
|
||||
'''
|
||||
从Arcaea客户端给的奇怪字符串中获取存档数据,并进行数据校验
|
||||
'''
|
||||
if not value:
|
||||
return None
|
||||
if key not in self.__dict__:
|
||||
raise KeyError(
|
||||
'Property `%s` is not found in the instance of `SaveData` class.' % key)
|
||||
|
||||
if md5(value) == checksum:
|
||||
if key == 'installid_data' or key == 'devicemodelname_data' or key == 'finalestate_data':
|
||||
self.__dict__[key] = json.loads(value)['val']
|
||||
else:
|
||||
self.__dict__[key] = json.loads(value)['']
|
||||
else:
|
||||
raise InputError('Hash value of cloud save data mismatches.')
|
||||
599
latest version/core/score.py
Normal file
599
latest version/core/score.py
Normal file
@@ -0,0 +1,599 @@
|
||||
from base64 import b64encode
|
||||
from os import urandom
|
||||
from time import time
|
||||
|
||||
from .constant import Constant
|
||||
from .course import CoursePlay
|
||||
from .error import NoData, StaminaNotEnough
|
||||
from .item import ItemCore
|
||||
from .song import Chart
|
||||
from .sql import Query, Sql
|
||||
from .util import md5
|
||||
from .world import WorldPlay
|
||||
|
||||
|
||||
class Score:
|
||||
def __init__(self) -> None:
|
||||
self.song: 'Chart' = Chart()
|
||||
self.score: int = None
|
||||
self.shiny_perfect_count: int = None
|
||||
self.perfect_count: int = None
|
||||
self.near_count: int = None
|
||||
self.miss_count: int = None
|
||||
self.health: int = None
|
||||
self.modifier: int = None
|
||||
self.time_played: int = None
|
||||
self.best_clear_type: int = None
|
||||
self.clear_type: int = None
|
||||
self.rating: float = None
|
||||
|
||||
def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int):
|
||||
self.score = int(score) if score is not None else 0
|
||||
self.shiny_perfect_count = int(
|
||||
shiny_perfect_count) if shiny_perfect_count is not None else 0
|
||||
self.perfect_count = int(
|
||||
perfect_count) if perfect_count is not None else 0
|
||||
self.near_count = int(near_count) if near_count is not None else 0
|
||||
self.miss_count = int(miss_count) if miss_count is not None else 0
|
||||
self.health = int(health) if health is not None else 0
|
||||
self.modifier = int(modifier) if modifier is not None else 0
|
||||
self.time_played = int(time_played) if time_played is not None else 0
|
||||
self.clear_type = int(clear_type) if clear_type is not None else 0
|
||||
|
||||
@staticmethod
|
||||
def get_song_grade(score: int) -> int:
|
||||
'''分数转换为评级'''
|
||||
if score >= 9900000: # EX+
|
||||
return 6
|
||||
elif 9800000 <= score < 9900000: # EX
|
||||
return 5
|
||||
elif 9500000 <= score < 9800000: # AA
|
||||
return 4
|
||||
elif 9200000 <= score < 9500000: # A
|
||||
return 3
|
||||
elif 8900000 <= score < 9200000: # B
|
||||
return 2
|
||||
elif 8600000 <= score < 8900000: # C
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def song_grade(self) -> int:
|
||||
return self.get_song_grade(self.score)
|
||||
|
||||
@staticmethod
|
||||
def get_song_state(clear_type: int) -> int:
|
||||
'''clear_type转换为成绩状态,用数字大小标识便于比较'''
|
||||
if clear_type == 3: # PM
|
||||
return 5
|
||||
elif clear_type == 2: # FC
|
||||
return 4
|
||||
elif clear_type == 5: # Hard Clear
|
||||
return 3
|
||||
elif clear_type == 1: # Clear
|
||||
return 2
|
||||
elif clear_type == 4: # Easy Clear
|
||||
return 1
|
||||
else: # Track Lost
|
||||
return 0
|
||||
|
||||
@property
|
||||
def song_state(self) -> int:
|
||||
return self.get_song_state(self.clear_type)
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
'''分数有效性检查'''
|
||||
if self.shiny_perfect_count < 0 or self.perfect_count < 0 or self.near_count < 0 or self.miss_count < 0 or self.score < 0 or self.time_played <= 0:
|
||||
return False
|
||||
if self.song.difficulty not in (0, 1, 2, 3):
|
||||
return False
|
||||
|
||||
all_note = self.perfect_count + self.near_count + self.miss_count
|
||||
if all_note == 0:
|
||||
return False
|
||||
|
||||
calc_score = 10000000 / all_note * \
|
||||
(self.perfect_count + self.near_count/2) + self.shiny_perfect_count
|
||||
if abs(calc_score - self.score) >= 5:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def calculate_rating(defnum: int, score: int) -> float:
|
||||
'''计算rating,谱面定数小于等于0视为Unrank,这里的defnum = Chart const'''
|
||||
if not defnum or defnum <= 0:
|
||||
# 谱面没定数或者定数小于等于0被视作Unrank
|
||||
return -1
|
||||
|
||||
if score >= 10000000:
|
||||
ptt = defnum + 2
|
||||
elif score < 9800000:
|
||||
ptt = defnum + (score-9500000) / 300000
|
||||
if ptt < 0:
|
||||
ptt = 0
|
||||
else:
|
||||
ptt = defnum + 1 + (score-9800000) / 200000
|
||||
|
||||
return ptt
|
||||
|
||||
def get_rating_by_calc(self) -> float:
|
||||
# 通过计算得到本成绩的rating
|
||||
if not self.song.defnum:
|
||||
self.song.c = self.c
|
||||
self.song.select()
|
||||
self.rating = self.calculate_rating(self.song.chart_const, self.score)
|
||||
return self.rating
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = {
|
||||
"rating": self.rating,
|
||||
"modifier": self.modifier,
|
||||
"time_played": self.time_played,
|
||||
"health": self.health,
|
||||
"clear_type": self.clear_type,
|
||||
"miss_count": self.miss_count,
|
||||
"near_count": self.near_count,
|
||||
"perfect_count": self.perfect_count,
|
||||
"shiny_perfect_count": self.shiny_perfect_count,
|
||||
"score": self.score,
|
||||
"difficulty": self.song.difficulty,
|
||||
"song_id": self.song.song_id
|
||||
}
|
||||
if self.song.song_name is not None:
|
||||
r["song_name"] = self.song.song_name
|
||||
return r
|
||||
|
||||
|
||||
class UserScore(Score):
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
'''
|
||||
parameter: `user` - `UserInfo`类或子类的实例
|
||||
'''
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.rank = None # 成绩排名,给Ranklist用的
|
||||
|
||||
def select_score(self) -> None:
|
||||
'''查询成绩以及用户搭档信息,单次查询可用,不要集体循环查询'''
|
||||
self.c.execute('''select * from best_score where user_id = :a and song_id = :b and difficulty = :c''',
|
||||
{'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('No score data.')
|
||||
self.user.select_user_about_character()
|
||||
|
||||
self.from_list(x)
|
||||
|
||||
def from_list(self, x: list) -> 'UserScore':
|
||||
if self.song.song_id is None:
|
||||
self.song.song_id = x[1]
|
||||
if self.song.difficulty is None:
|
||||
self.song.difficulty = x[2]
|
||||
self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12])
|
||||
self.best_clear_type = int(x[11])
|
||||
self.rating = float(x[13])
|
||||
|
||||
return self
|
||||
|
||||
def to_dict(self, has_user_info: bool = True) -> dict:
|
||||
r = super().to_dict()
|
||||
r['best_clear_type'] = self.best_clear_type
|
||||
if has_user_info:
|
||||
r['user_id'] = self.user.user_id
|
||||
r['name'] = self.user.name
|
||||
r['is_skill_sealed'] = self.user.is_skill_sealed
|
||||
character = self.user.character_displayed
|
||||
r['is_char_uncapped'] = character.is_uncapped_displayed
|
||||
r['character'] = character.character_id
|
||||
if self.rank:
|
||||
r['rank'] = self.rank
|
||||
return r
|
||||
|
||||
|
||||
class UserPlay(UserScore):
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__(c, user)
|
||||
self.song_token: str = None
|
||||
self.song_hash: str = None
|
||||
self.submission_hash: str = None
|
||||
self.beyond_gauge: int = None
|
||||
self.unrank_flag: bool = None
|
||||
self.first_protect_flag: bool = None
|
||||
self.ptt: 'Potential' = None
|
||||
|
||||
self.is_world_mode: bool = None
|
||||
self.stamina_multiply: int = None
|
||||
self.fragment_multiply: int = None
|
||||
self.prog_boost_multiply: int = None
|
||||
|
||||
self.ptt: Potential = None # 临时用来计算用户ptt的
|
||||
self.world_play: 'WorldPlay' = None
|
||||
|
||||
self.course_play_state: int = None
|
||||
self.course_play: 'CoursePlay' = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.is_world_mode is None or self.course_play_state is None:
|
||||
return {}
|
||||
if self.course_play_state == 4:
|
||||
r = self.course_play.to_dict()
|
||||
elif self.is_world_mode:
|
||||
r = self.world_play.to_dict()
|
||||
else:
|
||||
r = {}
|
||||
r['user_rating'] = self.user.rating_ptt
|
||||
r['finale_challenge_higher'] = self.rating > self.ptt.value
|
||||
r['global_rank'] = self.user.global_rank
|
||||
r['finale_play_value'] = self.rating * 5 # emmmm
|
||||
return r
|
||||
|
||||
@property
|
||||
def is_protected(self) -> bool:
|
||||
return self.health == -1 or int(self.score) >= 9800000 or self.first_protect_flag
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
'''分数有效性检查,带hash校验'''
|
||||
if not super().is_valid:
|
||||
return False
|
||||
|
||||
# 歌曲谱面MD5检查,服务器没有谱面就不管了
|
||||
from .download import get_song_file_md5
|
||||
songfile_hash = get_song_file_md5(
|
||||
self.song.song_id, str(self.song.difficulty) + '.aff')
|
||||
if songfile_hash and songfile_hash != self.song_hash:
|
||||
return False
|
||||
|
||||
x = self.song_token + self.song_hash + self.song.song_id + str(self.song.difficulty) + str(self.score) + str(self.shiny_perfect_count) + str(
|
||||
self.perfect_count) + str(self.near_count) + str(self.miss_count) + str(self.health) + str(self.modifier) + str(self.clear_type)
|
||||
y = str(self.user.user_id) + self.song_hash
|
||||
checksum = md5(x+md5(y))
|
||||
if checksum != self.submission_hash:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_play_state(self) -> None:
|
||||
'''检查token,当然这里不管有没有,是用来判断世界模式和课题模式的'''
|
||||
if self.song_token == '1145141919810':
|
||||
# 硬编码检查,绕过数据库
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = -1
|
||||
return None
|
||||
|
||||
self.c.execute(
|
||||
'''select * from songplay_token where token=:a ''', {'a': self.song_token})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = -1
|
||||
return None
|
||||
# raise NoData('No token data.')
|
||||
# self.song.set_chart(x[2], x[3])
|
||||
if x[4]:
|
||||
self.course_play = CoursePlay(self.c, self.user, self)
|
||||
self.course_play.course_id = x[4]
|
||||
self.course_play.score = x[6]
|
||||
self.course_play.clear_type = x[7]
|
||||
self.is_world_mode = False
|
||||
self.course_play_state = x[5]
|
||||
else:
|
||||
self.stamina_multiply = int(x[8])
|
||||
self.fragment_multiply = int(x[9])
|
||||
self.prog_boost_multiply = int(x[10])
|
||||
self.is_world_mode = True
|
||||
self.course_play_state = -1
|
||||
|
||||
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0) -> None:
|
||||
self.song_token = b64encode(urandom(64)).decode()
|
||||
self.stamina_multiply = int(stamina_multiply)
|
||||
self.fragment_multiply = int(fragment_multiply)
|
||||
self.prog_boost_multiply = int(prog_boost_multiply)
|
||||
if self.prog_boost_multiply != 0:
|
||||
self.c.execute('''select prog_boost from user where user_id=:a''', {
|
||||
'a': self.user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x and x[0] == 300:
|
||||
self.prog_boost_multiply = 300
|
||||
|
||||
self.clear_play_state()
|
||||
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f)''', {
|
||||
'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})
|
||||
|
||||
self.user.select_user_about_current_map()
|
||||
self.user.current_map.select_map_info()
|
||||
|
||||
self.user.select_user_about_stamina()
|
||||
if self.user.stamina.stamina < self.user.current_map.stamina_cost * self.stamina_multiply:
|
||||
raise StaminaNotEnough('Stamina is not enough.')
|
||||
|
||||
self.user.select_user_about_character()
|
||||
if not self.user.is_skill_sealed:
|
||||
self.user.character.select_character_info()
|
||||
if self.user.character.skill_id_displayed == 'skill_fatalis':
|
||||
# 特殊判断hikari fatalis的双倍体力消耗
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * \
|
||||
self.stamina_multiply * 2
|
||||
self.user.stamina.update()
|
||||
return None
|
||||
|
||||
self.user.stamina.stamina -= self.user.current_map.stamina_cost * self.stamina_multiply
|
||||
self.user.stamina.update()
|
||||
|
||||
def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: str = None) -> None:
|
||||
'''课题模式打歌初始化'''
|
||||
self.song_token = 'c_' + b64encode(urandom(64)).decode()
|
||||
if course_id is not None:
|
||||
self.course_play.course_id = course_id
|
||||
|
||||
self.course_play_state = 0
|
||||
self.course_play.score = 0
|
||||
self.course_play.clear_type = 3 # 设置为PM,即最大值
|
||||
|
||||
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0)''', (self.song_token, self.user.user_id, self.song.song_id,
|
||||
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
|
||||
self.user.select_user_about_stamina()
|
||||
if use_course_skip_purchase:
|
||||
x = ItemCore(self.c)
|
||||
x.item_id = 'core_course_skip_purchase'
|
||||
x.amount = -1
|
||||
x.user_claim_item(self.user)
|
||||
else:
|
||||
if self.user.stamina.stamina < Constant.COURSE_STAMINA_COST:
|
||||
raise StaminaNotEnough('Stamina is not enough.')
|
||||
self.user.stamina.stamina -= Constant.COURSE_STAMINA_COST
|
||||
self.user.stamina.update()
|
||||
|
||||
def update_token_for_course(self) -> None:
|
||||
'''课题模式更新token,并查用户体力'''
|
||||
previous_token = self.song_token
|
||||
self.song_token = 'c_' + b64encode(urandom(64)).decode()
|
||||
self.c.execute('''update songplay_token set token=? where token=?''',
|
||||
(self.song_token, previous_token))
|
||||
self.user.select_user_about_stamina()
|
||||
|
||||
def update_play_state_for_course(self) -> None:
|
||||
self.c.execute('''update songplay_token set course_state=?, course_score=?, course_clear_type=? where token=?''',
|
||||
(self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token))
|
||||
|
||||
def clear_play_state(self) -> None:
|
||||
self.c.execute('''delete from songplay_token where user_id=:a ''', {
|
||||
'a': self.user.user_id})
|
||||
|
||||
def update_recent30(self) -> None:
|
||||
'''更新此分数对应用户的recent30'''
|
||||
old_recent_10 = self.ptt.recent_10
|
||||
if self.is_protected:
|
||||
old_r30 = [x for x in self.ptt.r30]
|
||||
old_s30 = [x for x in self.ptt.s30]
|
||||
|
||||
# 寻找pop位置
|
||||
songs = list(set(self.ptt.s30))
|
||||
if '' in self.ptt.s30:
|
||||
r30_id = 29
|
||||
else:
|
||||
n = len(songs)
|
||||
if n >= 11:
|
||||
r30_id = 29
|
||||
elif self.song.song_id_difficulty not in songs and n == 10:
|
||||
r30_id = 29
|
||||
elif self.song.song_id_difficulty in songs and n == 10:
|
||||
i = 29
|
||||
while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0:
|
||||
i -= 1
|
||||
r30_id = i
|
||||
elif self.song.song_id_difficulty not in songs and n == 9:
|
||||
i = 29
|
||||
while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0:
|
||||
i -= 1
|
||||
r30_id = i
|
||||
else:
|
||||
r30_id = 29
|
||||
|
||||
self.ptt.recent_30_update(
|
||||
r30_id, self.rating, self.song.song_id_difficulty)
|
||||
if self.is_protected and old_recent_10 > self.ptt.recent_10:
|
||||
if self.song.song_id_difficulty in old_s30:
|
||||
# 发现重复歌曲,更新到最高rating
|
||||
index = old_s30.index(self.song.song_id_difficulty)
|
||||
if old_r30[index] < self.rating:
|
||||
old_r30[index] = self.rating
|
||||
|
||||
self.ptt.r30 = old_r30
|
||||
self.ptt.s30 = old_s30
|
||||
|
||||
self.ptt.insert_recent_30()
|
||||
|
||||
def upload_score(self) -> None:
|
||||
'''上传分数,包括user的recent更新,best更新,recent30更新,世界模式计算'''
|
||||
self.get_play_state()
|
||||
self.get_rating_by_calc()
|
||||
if self.rating < 0:
|
||||
self.unrank_flag = True
|
||||
self.rating = 0
|
||||
else:
|
||||
self.unrank_flag = False
|
||||
|
||||
self.time_played = int(time())
|
||||
|
||||
# recent更新
|
||||
self.c.execute('''update user set song_id = :b, difficulty = :c, score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played * 1000})
|
||||
|
||||
# 成绩录入
|
||||
self.c.execute('''select score, best_clear_type from best_score where user_id = :a and song_id = :b and difficulty = :c''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
self.first_protect_flag = True # 初见保护
|
||||
self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating})
|
||||
self.user.update_global_rank()
|
||||
else:
|
||||
self.first_protect_flag = False
|
||||
if self.song_state > self.get_song_state(int(x[1])): # best状态更新
|
||||
self.c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', {
|
||||
'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty})
|
||||
if self.score >= int(x[0]): # best成绩更新
|
||||
self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', {
|
||||
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played})
|
||||
self.user.update_global_rank()
|
||||
|
||||
self.ptt = Potential(self.c, self.user)
|
||||
if not self.unrank_flag:
|
||||
self.update_recent30()
|
||||
|
||||
# 总PTT更新
|
||||
self.user.rating_ptt = int(self.ptt.value * 100)
|
||||
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
||||
'a': self.user.rating_ptt, 'b': self.user.user_id})
|
||||
|
||||
# 世界模式判断
|
||||
if self.is_world_mode:
|
||||
self.world_play = WorldPlay(self.c, self.user, self)
|
||||
self.world_play.update()
|
||||
|
||||
# 课题模式判断
|
||||
if self.course_play_state >= 0:
|
||||
self.course_play.update()
|
||||
|
||||
|
||||
class Potential:
|
||||
'''
|
||||
用户潜力值计算处理类\
|
||||
property: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
self.r30: list = None
|
||||
self.s30: list = None
|
||||
self.songs_selected: list = None
|
||||
|
||||
self.b30: list = None
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
'''计算用户潜力值'''
|
||||
return self.best_30 * Constant.BEST30_WEIGHT + self.recent_10 * Constant.RECENT10_WEIGHT
|
||||
|
||||
@property
|
||||
def best_30(self) -> float:
|
||||
'''获取用户best30的总潜力值'''
|
||||
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
|
||||
'a': self.user.user_id})
|
||||
return sum([x[0] for x in self.c.fetchall()])
|
||||
|
||||
def select_recent_30(self) -> None:
|
||||
'''获取用户recent30数据'''
|
||||
self.c.execute(
|
||||
'''select * from recent30 where user_id = :a''', {'a': self.user.user_id})
|
||||
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.s30 = []
|
||||
if not x:
|
||||
return None
|
||||
for i in range(1, 61, 2):
|
||||
if x[i] is not None:
|
||||
self.r30.append(float(x[i]))
|
||||
self.s30.append(x[i+1])
|
||||
else:
|
||||
self.r30.append(0)
|
||||
self.s30.append('')
|
||||
|
||||
@property
|
||||
def recent_10(self) -> float:
|
||||
'''获取用户recent10的总潜力值'''
|
||||
if self.r30 is None:
|
||||
self.select_recent_30()
|
||||
|
||||
rating_sum = 0
|
||||
r30, s30 = (list(t) for t in zip(
|
||||
*sorted(zip(self.r30, self.s30), reverse=True)))
|
||||
|
||||
self.songs_selected = []
|
||||
i = 0
|
||||
while len(self.songs_selected) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None:
|
||||
if s30[i] not in self.songs_selected:
|
||||
rating_sum += r30[i]
|
||||
self.songs_selected.append(s30[i])
|
||||
i += 1
|
||||
return rating_sum
|
||||
|
||||
def recent_30_to_dict_list(self) -> list:
|
||||
if self.r30 is None:
|
||||
self.select_recent_30()
|
||||
r = []
|
||||
for x, y in zip(self.s30, self.r30):
|
||||
if x:
|
||||
r.append({
|
||||
'song_id': x[:-1],
|
||||
'difficulty': int(x[-1]),
|
||||
'rating': y
|
||||
})
|
||||
return r
|
||||
|
||||
def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None:
|
||||
self.r30.pop(pop_index)
|
||||
self.s30.pop(pop_index)
|
||||
self.r30.insert(0, rating)
|
||||
self.s30.insert(0, song_id_difficulty)
|
||||
|
||||
def insert_recent_30(self) -> None:
|
||||
'''更新r30表数据'''
|
||||
sql = '''update recent30 set r0=?,song_id0=?,r1=?,song_id1=?,r2=?,song_id2=?,r3=?,song_id3=?,r4=?,song_id4=?,r5=?,song_id5=?,r6=?,song_id6=?,r7=?,song_id7=?,r8=?,song_id8=?,r9=?,song_id9=?,r10=?,song_id10=?,r11=?,song_id11=?,r12=?,song_id12=?,r13=?,song_id13=?,r14=?,song_id14=?,r15=?,song_id15=?,r16=?,song_id16=?,r17=?,song_id17=?,r18=?,song_id18=?,r19=?,song_id19=?,r20=?,song_id20=?,r21=?,song_id21=?,r22=?,song_id22=?,r23=?,song_id23=?,r24=?,song_id24=?,r25=?,song_id25=?,r26=?,song_id26=?,r27=?,song_id27=?,r28=?,song_id28=?,r29=?,song_id29=? where user_id=?'''
|
||||
sql_list = []
|
||||
for i in range(30):
|
||||
sql_list.append(self.r30[i])
|
||||
sql_list.append(self.s30[i])
|
||||
|
||||
sql_list.append(self.user.user_id)
|
||||
|
||||
self.c.execute(sql, sql_list)
|
||||
|
||||
|
||||
class UserScoreList:
|
||||
'''
|
||||
用户分数查询类\
|
||||
properties: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None):
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.scores: list = None
|
||||
self.query: 'Query' = Query(['user_id', 'song_id', 'difficulty'], ['song_id'], [
|
||||
'rating', 'difficulty', 'song_id', 'score', 'time_played'])
|
||||
|
||||
def to_dict_list(self) -> list:
|
||||
return [x.to_dict(has_user_info=False) for x in self.scores]
|
||||
|
||||
def select_from_user(self, user=None) -> None:
|
||||
'''获取用户的best_score数据'''
|
||||
if user is not None:
|
||||
self.user = user
|
||||
|
||||
self.query.query_append({'user_id': self.user.user_id})
|
||||
self.query.sort += [{'column': 'rating', 'order': 'DESC'}]
|
||||
x = Sql(self.c).select('best_score', query=self.query)
|
||||
|
||||
self.scores = [UserScore(self.c, self.user).from_list(i) for i in x]
|
||||
|
||||
def select_song_name(self) -> None:
|
||||
'''为所有成绩中的song_id查询song_name'''
|
||||
if self.scores is None:
|
||||
return
|
||||
for score in self.scores:
|
||||
self.c.execute(
|
||||
'''select name from chart where song_id = ?''', (score.song.song_id,))
|
||||
x = self.c.fetchone()
|
||||
score.song.song_name = x[0] if x else ''
|
||||
82
latest version/core/song.py
Normal file
82
latest version/core/song.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from .error import NoData
|
||||
|
||||
|
||||
class Chart:
|
||||
# defnum: chart const * 10
|
||||
|
||||
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
|
||||
self.c = c
|
||||
self.set_chart(song_id, difficulty)
|
||||
self.defnum: int = None
|
||||
self.song_name: str = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'difficulty': self.difficulty,
|
||||
'chart_const': self.chart_const
|
||||
}
|
||||
|
||||
@property
|
||||
def chart_const(self) -> float:
|
||||
return self.defnum / 10 if self.defnum else -1
|
||||
|
||||
@property
|
||||
def song_id_difficulty(self) -> str:
|
||||
return '%s%d' % (self.song_id, self.difficulty)
|
||||
|
||||
def set_chart(self, song_id: str = None, difficulty: int = None) -> None:
|
||||
self.song_id = song_id
|
||||
self.difficulty = int(difficulty) if difficulty is not None else None
|
||||
|
||||
def select(self) -> None:
|
||||
self.c.execute(
|
||||
'''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
self.defnum = -10
|
||||
# raise NoData('The song `%s` does not exist.' % self.song_id)
|
||||
else:
|
||||
self.defnum = x[self.difficulty]
|
||||
|
||||
|
||||
class Song:
|
||||
def __init__(self, c=None, song_id=None) -> None:
|
||||
self.c = c
|
||||
self.song_id: str = song_id
|
||||
self.name: str = None
|
||||
self.charts: dict = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'song_id': self.song_id,
|
||||
'name': self.name,
|
||||
'charts': [chart.to_dict() for chart in self.charts]
|
||||
}
|
||||
|
||||
def from_list(self, x: list) -> 'Song':
|
||||
if self.song_id is None:
|
||||
self.song_id = x[0]
|
||||
self.name = x[1]
|
||||
self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart(
|
||||
self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)]
|
||||
self.charts[0].defnum = x[2]
|
||||
self.charts[1].defnum = x[3]
|
||||
self.charts[2].defnum = x[4]
|
||||
self.charts[3].defnum = x[5]
|
||||
return self
|
||||
|
||||
def insert(self) -> None:
|
||||
self.c.execute(
|
||||
'''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum))
|
||||
|
||||
def select(self, song_id: str = None) -> 'Song':
|
||||
if song_id is not None:
|
||||
self.song_id = song_id
|
||||
|
||||
self.c.execute('''select * from chart where song_id=:a''', {
|
||||
'a': self.song_id})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('The song `%s` does not exist.' % self.song_id)
|
||||
|
||||
return self.from_list(x)
|
||||
409
latest version/core/sql.py
Normal file
409
latest version/core/sql.py
Normal file
@@ -0,0 +1,409 @@
|
||||
import sqlite3
|
||||
import traceback
|
||||
from atexit import register
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError
|
||||
|
||||
|
||||
class Connect:
|
||||
# 数据库连接类,上下文管理
|
||||
|
||||
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False):
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db\
|
||||
接受:文件路径\
|
||||
返回:sqlite3连接操作对象
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.in_memory: bool = in_memory
|
||||
|
||||
def __enter__(self) -> sqlite3.Cursor:
|
||||
if self.in_memory:
|
||||
self.conn = sqlite3.connect(
|
||||
'file:arc_tmp?mode=memory&cache=shared', uri=True, timeout=10)
|
||||
else:
|
||||
self.conn = sqlite3.connect(self.file_path, timeout=10)
|
||||
self.c = self.conn.cursor()
|
||||
return self.c
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||
flag = True
|
||||
if exc_type is not None:
|
||||
if issubclass(exc_type, ArcError):
|
||||
flag = False
|
||||
else:
|
||||
self.conn.rollback()
|
||||
|
||||
current_app.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
class Query:
|
||||
'''查询参数类'''
|
||||
|
||||
def __init__(self, query_able: list = None, quzzy_query_able: list = None, sort_able: list = None) -> None:
|
||||
self.query_able: list = query_able # None表示不限制
|
||||
self.quzzy_query_able: list = quzzy_query_able # None表示不限制
|
||||
self.sort_able: list = sort_able
|
||||
|
||||
self.__limit: int = -1
|
||||
self.__offset: int = 0
|
||||
|
||||
# {'name': 'admin'} or {'name': ['admin', 'user']}
|
||||
self.__query: dict = {}
|
||||
self.__fuzzy_query: dict = {} # {'name': 'dmi'}
|
||||
|
||||
# [{'column': 'user_id', 'order': 'ASC'}, ...]
|
||||
self.__sort: list = []
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
return self.__limit
|
||||
|
||||
@limit.setter
|
||||
def limit(self, limit: int) -> None:
|
||||
if not isinstance(limit, int):
|
||||
raise InputError(api_error_code=-101)
|
||||
self.__limit = limit
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
return self.__offset
|
||||
|
||||
@offset.setter
|
||||
def offset(self, offset: int) -> None:
|
||||
if not isinstance(offset, int):
|
||||
raise InputError(api_error_code=-101)
|
||||
self.__offset = offset
|
||||
|
||||
@property
|
||||
def query(self) -> dict:
|
||||
return self.__query
|
||||
|
||||
@query.setter
|
||||
def query(self, query: dict) -> None:
|
||||
self.__query = {}
|
||||
self.query_append(query)
|
||||
|
||||
def query_append(self, query: dict) -> None:
|
||||
if not isinstance(query, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.query_able is not None and query and not set(query).issubset(set(self.query_able)):
|
||||
raise InputError(api_error_code=-102)
|
||||
if not self.__query:
|
||||
self.__query = query
|
||||
else:
|
||||
self.__query.update(query)
|
||||
|
||||
@property
|
||||
def fuzzy_query(self) -> dict:
|
||||
return self.__fuzzy_query
|
||||
|
||||
@fuzzy_query.setter
|
||||
def fuzzy_query(self, fuzzy_query: dict) -> None:
|
||||
self.__fuzzy_query = {}
|
||||
self.fuzzy_query_append(fuzzy_query)
|
||||
|
||||
def fuzzy_query_append(self, fuzzy_query: dict) -> None:
|
||||
if not isinstance(fuzzy_query, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.quzzy_query_able is not None and fuzzy_query and not set(fuzzy_query).issubset(set(self.quzzy_query_able)):
|
||||
raise InputError(api_error_code=-102)
|
||||
if not self.__fuzzy_query:
|
||||
self.__fuzzy_query = fuzzy_query
|
||||
else:
|
||||
self.__fuzzy_query.update(fuzzy_query)
|
||||
|
||||
@property
|
||||
def sort(self) -> list:
|
||||
return self.__sort
|
||||
|
||||
@sort.setter
|
||||
def sort(self, sort: list) -> None:
|
||||
if not isinstance(sort, list):
|
||||
raise InputError(api_error_code=-101)
|
||||
if self.sort_able is not None and sort:
|
||||
for x in sort:
|
||||
if not isinstance(x, dict):
|
||||
raise InputError(api_error_code=-101)
|
||||
if 'column' not in x or x['column'] not in self.sort_able:
|
||||
raise InputError(api_error_code=-103)
|
||||
if 'order' not in x:
|
||||
x['order'] = 'ASC'
|
||||
else:
|
||||
if x['order'] not in ['ASC', 'DESC']:
|
||||
raise InputError(api_error_code=-104)
|
||||
self.__sort = sort
|
||||
|
||||
def set_value(self, limit=-1, offset=0, query={}, fuzzy_query={}, sort=[]) -> None:
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.query = query
|
||||
self.fuzzy_query = fuzzy_query
|
||||
self.sort = sort
|
||||
|
||||
def from_dict(self, d: dict) -> 'Query':
|
||||
self.set_value(d.get('limit', -1), d.get('offset', 0),
|
||||
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
|
||||
return self
|
||||
|
||||
def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = [], fuzzy_query: dict = {}) -> 'Query':
|
||||
self.set_value(limit, offset, query, fuzzy_query, sort)
|
||||
return self
|
||||
|
||||
|
||||
class Sql:
|
||||
'''
|
||||
数据库增查删改类
|
||||
'''
|
||||
|
||||
def __init__(self, c=None) -> None:
|
||||
self.c = c
|
||||
|
||||
@staticmethod
|
||||
def get_select_sql(table_name: str, target_column: list = [], query: 'Query' = None):
|
||||
'''拼接单表内行查询单句sql语句,返回语句和参数列表'''
|
||||
sql_list = []
|
||||
if not target_column:
|
||||
sql = f'select * from {table_name}'
|
||||
else:
|
||||
sql = f"select {', '.join(target_column)} from {table_name}"
|
||||
|
||||
if query is None:
|
||||
return sql, sql_list
|
||||
|
||||
where_key = []
|
||||
for k, v in query.query.items():
|
||||
if isinstance(v, list):
|
||||
where_key.append(f"{k} in ({','.join(['?'] * len(v))})")
|
||||
sql_list.extend(v)
|
||||
else:
|
||||
where_key.append(f'{k}=?')
|
||||
sql_list.append(v)
|
||||
|
||||
for k, v in query.fuzzy_query.items():
|
||||
where_key.append(f'{k} like ?')
|
||||
sql_list.append(f'%{v}%')
|
||||
|
||||
if where_key:
|
||||
sql += ' where '
|
||||
sql += ' and '.join(where_key)
|
||||
|
||||
if query.sort:
|
||||
sql += ' order by ' + \
|
||||
', '.join([x['column'] + ' ' + x['order'] for x in query.sort])
|
||||
|
||||
if query.limit >= 0:
|
||||
sql += ' limit ? offset ?'
|
||||
sql_list.append(query.limit)
|
||||
sql_list.append(query.offset)
|
||||
|
||||
return sql, sql_list
|
||||
|
||||
@staticmethod
|
||||
def get_insert_sql(table_name: str, key: list = [], value_len: int = None, insert_type: str = None) -> str:
|
||||
'''拼接insert语句,请注意只返回sql语句,insert_type为replace或ignore'''
|
||||
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 = {}, query: 'Query' = None) -> str:
|
||||
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_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 = [], query: 'Query' = None) -> list:
|
||||
'''单表内行select单句sql语句,返回fetchall数据'''
|
||||
sql, sql_list = self.get_select_sql(table_name, target_column, query)
|
||||
self.c.execute(sql, sql_list)
|
||||
return self.c.fetchall()
|
||||
|
||||
def select_exists(self, table_name: str, target_column: list = [], 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 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('''pragma table_info ("%s")''' % 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
|
||||
|
||||
sql2.insert_many(table_name, [], sql1.select(
|
||||
table_name, list(filter(lambda x: x in db2_name, db1_name))), 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])
|
||||
|
||||
@staticmethod
|
||||
def update_user_epilogue(c) -> None:
|
||||
'''给用户添加epilogue包'''
|
||||
c.execute('''select user_id from user''')
|
||||
Sql(c).insert_many('user_item', [], [(i[0], 'epilogue', 'pack', 1)
|
||||
for i in c.fetchall()], insert_type='ignore')
|
||||
|
||||
def update_database(self) -> None:
|
||||
'''
|
||||
将c1数据库不存在数据加入或覆盖到c2数据库上
|
||||
对于c2,更新一些表,并用character数据更新user_char_full
|
||||
'''
|
||||
with Connect(self.c2_path) as c2:
|
||||
with Connect(self.c1_path) as c1:
|
||||
[self.update_one_table(c1, c2, i)
|
||||
for i in Constant.DATABASE_MIGRATE_TABLES]
|
||||
|
||||
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||
self.update_one_table(c1, c2, 'character')
|
||||
|
||||
self.update_user_char_full(c2) # 更新user_char_full
|
||||
self.update_user_epilogue(c2) # 更新user的epilogue
|
||||
|
||||
|
||||
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()
|
||||
19
latest version/core/system.py
Normal file
19
latest version/core/system.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from time import time
|
||||
|
||||
from .constant import Constant
|
||||
|
||||
|
||||
class GameInfo:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"max_stamina": Constant.MAX_STAMINA,
|
||||
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
||||
"core_exp": Constant.CORE_EXP,
|
||||
"curr_ts": int(time()*1000),
|
||||
"level_steps": [{'level': i, 'level_exp': Constant.LEVEL_STEPS[i]} for i in Constant.LEVEL_STEPS],
|
||||
"world_ranking_enabled": True,
|
||||
"is_byd_chapter_unlocked": True
|
||||
}
|
||||
765
latest version/core/user.py
Normal file
765
latest version/core/user.py
Normal file
@@ -0,0 +1,765 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
from os import urandom
|
||||
|
||||
from .character import UserCharacter, UserCharacterList
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .error import (ArcError, DataExist, FriendError, InputError, NoAccess,
|
||||
NoData, RateLimit, UserBan)
|
||||
from .item import UserItemList
|
||||
from .limiter import ArcLimiter
|
||||
from .score import Score
|
||||
from .sql import Query, Sql
|
||||
from .world import Map, UserMap, UserStamina
|
||||
|
||||
|
||||
def code_get_id(c, user_code: str) -> int:
|
||||
# 用user_code获取user_id
|
||||
|
||||
c.execute('''select user_id from user where user_code = :a''',
|
||||
{'a': user_code})
|
||||
x = c.fetchone()
|
||||
|
||||
if x is not None:
|
||||
user_id = int(x[0])
|
||||
else:
|
||||
raise NoData('No user.', 401, -3)
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self) -> None:
|
||||
self.name: str = None
|
||||
self.email: str = None
|
||||
self.password: str = None
|
||||
self.user_id: int = None
|
||||
self.user_code: str = None
|
||||
|
||||
self.join_date = None
|
||||
self.rating_ptt: int = None # 100 times
|
||||
|
||||
self.ticket: int = None
|
||||
self.world_rank_score: int = None
|
||||
self.ban_flag = None
|
||||
|
||||
@property
|
||||
def hash_pwd(self) -> str:
|
||||
'''`password`的SHA-256值'''
|
||||
return hashlib.sha256(self.password.encode("utf8")).hexdigest()
|
||||
|
||||
|
||||
class UserRegister(User):
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
|
||||
def set_name(self, name: str):
|
||||
if 3 <= len(name) <= 16:
|
||||
self.c.execute(
|
||||
'''select exists(select * from user where name = :name)''', {'name': name})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.name = name
|
||||
else:
|
||||
raise DataExist('Username exists.', 101, -210)
|
||||
|
||||
else:
|
||||
raise InputError('Username is invalid.')
|
||||
|
||||
def set_password(self, password: str):
|
||||
if 8 <= len(password) <= 32:
|
||||
self.password = password
|
||||
else:
|
||||
raise InputError('Password is invalid.')
|
||||
|
||||
def set_email(self, email: str):
|
||||
# 邮箱格式懒得多判断
|
||||
if 4 <= len(email) <= 64 and '@' in email and '.' in email:
|
||||
self.c.execute(
|
||||
'''select exists(select * from user where email = :email)''', {'email': email})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.email = email
|
||||
else:
|
||||
raise DataExist('Email address exists.', 102, -211)
|
||||
else:
|
||||
raise InputError('Email address is invalid.')
|
||||
|
||||
def set_user_code(self, user_code: str) -> None:
|
||||
'''设置用户的user_code'''
|
||||
if len(user_code) == 9 and user_code.isdigit():
|
||||
self.c.execute(
|
||||
'''select exists(select * from user where user_code = ?)''', (user_code, ))
|
||||
if self.c.fetchone() == (0,):
|
||||
self.user_code = user_code
|
||||
else:
|
||||
raise DataExist('User code exists.', 103, -212)
|
||||
else:
|
||||
raise InputError('User code is invalid.')
|
||||
|
||||
def _build_user_code(self):
|
||||
# 生成9位的user_code,用的自然是随机
|
||||
from random import randint
|
||||
random_times = 0
|
||||
|
||||
while random_times <= 1000:
|
||||
random_times += 1
|
||||
user_code = ''.join([str(randint(0, 9)) for _ in range(9)])
|
||||
self.c.execute('''select exists(select * from user where user_code = :user_code)''',
|
||||
{'user_code': user_code})
|
||||
if self.c.fetchone() == (0,):
|
||||
break
|
||||
|
||||
if random_times <= 1000:
|
||||
self.user_code = user_code
|
||||
else:
|
||||
raise ArcError('No available user code.')
|
||||
|
||||
def _build_user_id(self):
|
||||
# 生成user_id,往后加1
|
||||
self.c.execute('''select max(user_id) from user''')
|
||||
x = self.c.fetchone()
|
||||
if x[0] is not None:
|
||||
self.user_id = x[0] + 1
|
||||
else:
|
||||
self.user_id = 2000001
|
||||
|
||||
def _insert_user_char(self):
|
||||
# 为用户添加初始角色
|
||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(self.user_id, 0, 1, 0, 0, 0))
|
||||
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(self.user_id, 1, 1, 0, 0, 0))
|
||||
self.c.execute(
|
||||
'''select character_id, max_level, is_uncapped from character''')
|
||||
x = self.c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
exp = 25000 if i[1] == 30 else 10000
|
||||
self.c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
|
||||
(self.user_id, i[0], i[1], exp, i[2], 0))
|
||||
|
||||
def register(self):
|
||||
now = int(time.time() * 1000)
|
||||
if self.user_code is None:
|
||||
self._build_user_code()
|
||||
if self.user_id is None:
|
||||
self._build_user_id()
|
||||
self._insert_user_char()
|
||||
|
||||
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
|
||||
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email)
|
||||
values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
|
||||
''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email})
|
||||
self.c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
||||
'user_id': self.user_id})
|
||||
|
||||
|
||||
class UserLogin(User):
|
||||
# 密码和token的加密方式为 SHA-256
|
||||
limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login')
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.device_id = None
|
||||
self.ip = None
|
||||
self.token = None
|
||||
self.now = 0
|
||||
|
||||
def set_name(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def set_password(self, password: str):
|
||||
self.password = password
|
||||
|
||||
def set_device_id(self, device_id: str):
|
||||
self.device_id = device_id
|
||||
|
||||
def set_ip(self, ip: str):
|
||||
self.ip = ip
|
||||
|
||||
def _arc_auto_ban(self):
|
||||
# 多设备自动封号机制,返回封号时长
|
||||
self.c.execute('''delete from login where user_id=?''',
|
||||
(self.user_id, ))
|
||||
self.c.execute(
|
||||
'''select ban_flag from user where user_id=?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if x and x[0] != '' and x[0] is not None:
|
||||
last_ban_time = int(x[0].split(':', 1)[0])
|
||||
i = 0
|
||||
while i < len(Constant.BAN_TIME) - 1 and Constant.BAN_TIME[i] <= last_ban_time:
|
||||
i += 1
|
||||
ban_time = Constant.BAN_TIME[i]
|
||||
else:
|
||||
ban_time = Constant.BAN_TIME[0]
|
||||
|
||||
ban_flag = ':'.join(
|
||||
(str(ban_time), str(self.now + ban_time * 86400000)))
|
||||
self.c.execute('''update user set ban_flag=? where user_id=?''',
|
||||
(ban_flag, self.user_id))
|
||||
|
||||
return ban_time * 86400000
|
||||
|
||||
def _check_device(self, device_list):
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if not Config.ALLOW_LOGIN_SAME_DEVICE:
|
||||
if self.device_id in device_list: # 对相同设备进行删除
|
||||
self.c.execute('''delete from login where login_device=:a and user_id=:b''', {
|
||||
'a': self.device_id, 'b': self.user_id})
|
||||
should_delete_num = len(
|
||||
device_list) + 1 - device_list.count(self.device_id) - Config.LOGIN_DEVICE_NUMBER_LIMIT
|
||||
|
||||
if should_delete_num >= 1: # 删掉多余token
|
||||
if not Config.ALLOW_LOGIN_SAME_DEVICE and Config.ALLOW_BAN_MULTIDEVICE_USER_AUTO: # 自动封号检查
|
||||
self.c.execute(
|
||||
'''select count(*) from login where user_id=? and login_time>?''', (self.user_id, self.now-86400000))
|
||||
if self.c.fetchone()[0] >= Config.LOGIN_DEVICE_NUMBER_LIMIT:
|
||||
remaining_ts = self._arc_auto_ban()
|
||||
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
|
||||
'remaining_ts': remaining_ts})
|
||||
|
||||
self.c.execute('''delete from login where rowid in (select rowid from login where user_id=:user_id limit :a);''',
|
||||
{'user_id': self.user_id, 'a': int(should_delete_num)})
|
||||
|
||||
def login(self, name: str = '', password: str = '', device_id: str = '', ip: str = ''):
|
||||
if name:
|
||||
self.set_name(name)
|
||||
if password:
|
||||
self.set_password(password)
|
||||
if device_id:
|
||||
self.set_device_id(device_id)
|
||||
if ip:
|
||||
self.set_ip(ip)
|
||||
|
||||
if not self.limiter.hit(name):
|
||||
raise RateLimit('Too many login attempts.', 123, -203)
|
||||
|
||||
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
|
||||
'name': self.name})
|
||||
x = self.c.fetchone()
|
||||
if x is None:
|
||||
raise NoData('Username does not exist.', 104)
|
||||
|
||||
self.now = int(time.time() * 1000)
|
||||
if x[2] is not None and x[2] != '':
|
||||
# 自动封号检查
|
||||
ban_timestamp = int(x[2].split(':', 1)[1])
|
||||
if ban_timestamp > self.now:
|
||||
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
|
||||
'remaining_ts': ban_timestamp-self.now})
|
||||
|
||||
if x[1] == '':
|
||||
# 账号封禁
|
||||
raise UserBan('The account has been banned.', 106)
|
||||
|
||||
if x[1] != self.hash_pwd:
|
||||
raise NoAccess('Wrong password.', 104)
|
||||
|
||||
self.user_id = str(x[0])
|
||||
self.token = base64.b64encode(hashlib.sha256(
|
||||
(self.user_id + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
|
||||
|
||||
self.c.execute(
|
||||
'''select login_device from login where user_id = :user_id''', {"user_id": self.user_id})
|
||||
y = self.c.fetchall()
|
||||
if y:
|
||||
self._check_device([i[0] if i[0] else '' for i in y])
|
||||
|
||||
self.c.execute('''insert into login values(:access_token, :user_id, :time, :ip, :device_id)''', {
|
||||
'user_id': self.user_id, 'access_token': self.token, 'device_id': self.device_id, 'time': self.now, 'ip': self.ip})
|
||||
|
||||
|
||||
class UserAuth(User):
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.token = None
|
||||
|
||||
def token_get_id(self):
|
||||
# 用token获取id,没有考虑不同用户token相同情况,说不定会有bug
|
||||
self.c.execute('''select user_id from login where access_token = :token''', {
|
||||
'token': self.token})
|
||||
x = self.c.fetchone()
|
||||
if x is not None:
|
||||
self.user_id = x[0]
|
||||
else:
|
||||
raise NoAccess('Wrong token.', -4)
|
||||
|
||||
return self.user_id
|
||||
|
||||
|
||||
class UserInfo(User):
|
||||
def __init__(self, c, user_id=None) -> None:
|
||||
User.__init__(self)
|
||||
self.c = c
|
||||
self.user_id = user_id
|
||||
self.character = None
|
||||
self.is_skill_sealed = False
|
||||
self.is_hide_rating = False
|
||||
self.recent_score = Score()
|
||||
self.favorite_character = None
|
||||
self.max_stamina_notification_enabled = False
|
||||
self.prog_boost = 0
|
||||
self.next_fragstam_ts: int = None
|
||||
self.world_mode_locked_end_ts: int = None
|
||||
|
||||
self.__cores: list = None
|
||||
self.__packs: list = None
|
||||
self.__singles: list = None
|
||||
self.characters: 'UserCharacterList' = None
|
||||
self.__friends: list = None
|
||||
self.__world_unlocks: list = None
|
||||
self.__world_songs: list = None
|
||||
self.curr_available_maps: list = None
|
||||
self.__course_banners: list = None
|
||||
|
||||
@property
|
||||
def cores(self) -> list:
|
||||
if self.__cores is None:
|
||||
x = UserItemList(self.c, self).select_from_type('core')
|
||||
self.__cores = [{'core_type': i.item_id,
|
||||
'amount': i.amount} for i in x.items]
|
||||
|
||||
return self.__cores
|
||||
|
||||
@property
|
||||
def singles(self) -> list:
|
||||
if self.__singles is None:
|
||||
x = UserItemList(self.c, self).select_from_type('single')
|
||||
self.__singles = [i.item_id for i in x.items]
|
||||
|
||||
return self.__singles
|
||||
|
||||
@property
|
||||
def packs(self) -> list:
|
||||
if self.__packs is None:
|
||||
x = UserItemList(self.c, self).select_from_type('pack')
|
||||
self.__packs = [i.item_id for i in x.items]
|
||||
|
||||
return self.__packs
|
||||
|
||||
@property
|
||||
def world_unlocks(self) -> list:
|
||||
if self.__world_unlocks is None:
|
||||
x = UserItemList(self.c, self).select_from_type(
|
||||
'world_unlock')
|
||||
self.__world_unlocks = [i.item_id for i in x.items]
|
||||
|
||||
return self.__world_unlocks
|
||||
|
||||
@property
|
||||
def world_songs(self) -> list:
|
||||
if self.__world_songs is None:
|
||||
x = UserItemList(
|
||||
self.c, self).select_from_type('world_song')
|
||||
self.__world_songs = [i.item_id for i in x.items]
|
||||
|
||||
return self.__world_songs
|
||||
|
||||
@property
|
||||
def course_banners(self) -> list:
|
||||
if self.__course_banners is None:
|
||||
x = UserItemList(
|
||||
self.c, self).select_from_type('course_banner')
|
||||
self.__course_banners = [i.item_id for i in x.items]
|
||||
|
||||
return self.__course_banners
|
||||
|
||||
def select_characters(self) -> None:
|
||||
self.characters = UserCharacterList(self.c, self)
|
||||
self.characters.select_user_characters()
|
||||
|
||||
@property
|
||||
def characters_list(self) -> list:
|
||||
if self.characters is None:
|
||||
self.select_characters()
|
||||
return [x.character_id for x in self.characters.characters]
|
||||
|
||||
@property
|
||||
def character_displayed(self) -> 'UserCharacter':
|
||||
'''对外显示的角色'''
|
||||
if self.favorite_character is None:
|
||||
return self.character
|
||||
|
||||
self.favorite_character.select_character_uncap_condition(self)
|
||||
return self.favorite_character
|
||||
|
||||
@property
|
||||
def friend_ids(self) -> list:
|
||||
self.c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
|
||||
'user_id': self.user_id})
|
||||
return self.c.fetchall()
|
||||
|
||||
@property
|
||||
def friends(self) -> list:
|
||||
# 得到用户的朋友列表
|
||||
if self.__friends is None:
|
||||
s = []
|
||||
for i in self.friend_ids:
|
||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': i[0], 'y': self.user_id})
|
||||
|
||||
is_mutual = True if self.c.fetchone() == (1,) else False
|
||||
|
||||
you = UserOnline(self.c, i[0])
|
||||
you.select_user()
|
||||
character = you.character if you.favorite_character is None else you.favorite_character
|
||||
character.select_character_uncap_condition(you)
|
||||
|
||||
rating = you.rating_ptt if not you.is_hide_rating else -1
|
||||
|
||||
s.append({
|
||||
"is_mutual": is_mutual,
|
||||
"is_char_uncapped_override": character.is_uncapped_override,
|
||||
"is_char_uncapped": character.is_uncapped,
|
||||
"is_skill_sealed": you.is_skill_sealed,
|
||||
"rating": rating,
|
||||
"join_date": you.join_date,
|
||||
"character": character.character_id,
|
||||
"recent_score": you.recent_score_list,
|
||||
"name": you.name,
|
||||
"user_id": you.user_id
|
||||
})
|
||||
s.sort(key=lambda item: item["recent_score"][0]["time_played"] if len(
|
||||
item["recent_score"]) > 0 else 0, reverse=True)
|
||||
self.__friends = s
|
||||
|
||||
return self.__friends
|
||||
|
||||
@property
|
||||
def recent_score_list(self) -> list:
|
||||
# 用户最近一次成绩,是列表
|
||||
if self.name is None:
|
||||
self.select_user()
|
||||
|
||||
if self.recent_score.song.song_id is None:
|
||||
return []
|
||||
|
||||
self.c.execute('''select best_clear_type from best_score where user_id=:u and song_id=:s and difficulty=:d''', {
|
||||
'u': self.user_id, 's': self.recent_score.song.song_id, 'd': self.recent_score.song.difficulty})
|
||||
y = self.c.fetchone()
|
||||
best_clear_type = y[0] if y is not None else self.recent_score.clear_type
|
||||
|
||||
r = self.recent_score.to_dict()
|
||||
r["best_clear_type"] = best_clear_type
|
||||
return [r]
|
||||
|
||||
def select_curr_available_maps(self) -> None:
|
||||
self.curr_available_maps: list = []
|
||||
for i in Config.AVAILABLE_MAP:
|
||||
self.curr_available_maps.append(Map(i))
|
||||
|
||||
@property
|
||||
def curr_available_maps_list(self) -> list:
|
||||
if self.curr_available_maps is None:
|
||||
self.select_curr_available_maps()
|
||||
return [x.to_dict() for x in self.curr_available_maps]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
'''返回用户信息的字典,其实就是/user/me'''
|
||||
if self.name is None:
|
||||
self.select_user()
|
||||
|
||||
# 这是考虑有可能favourite_character设置了用户未拥有的角色,同时提前计算角色列表
|
||||
character_list = self.characters_list
|
||||
if self.favorite_character and self.favorite_character.character_id in character_list:
|
||||
favorite_character_id = self.favorite_character.character_id
|
||||
else:
|
||||
favorite_character_id = -1
|
||||
|
||||
if self.character.character_id not in character_list:
|
||||
self.character.character_id = 0
|
||||
|
||||
return {
|
||||
"is_aprilfools": Config.IS_APRILFOOLS,
|
||||
"curr_available_maps": self.curr_available_maps_list,
|
||||
"character_stats": [x.to_dict() for x in self.characters.characters],
|
||||
"friends": self.friends,
|
||||
"settings": {
|
||||
"favorite_character": favorite_character_id,
|
||||
"is_hide_rating": self.is_hide_rating,
|
||||
"max_stamina_notification_enabled": self.max_stamina_notification_enabled
|
||||
},
|
||||
"user_id": self.user_id,
|
||||
"name": self.name,
|
||||
"user_code": self.user_code,
|
||||
"display_name": self.name,
|
||||
"ticket": self.ticket,
|
||||
"character": self.character.character_id,
|
||||
"is_locked_name_duplicate": False,
|
||||
"is_skill_sealed": self.is_skill_sealed,
|
||||
"current_map": self.current_map.map_id,
|
||||
"prog_boost": self.prog_boost,
|
||||
"next_fragstam_ts": self.next_fragstam_ts,
|
||||
"max_stamina_ts": self.stamina.max_stamina_ts,
|
||||
"stamina": self.stamina.stamina,
|
||||
"world_unlocks": self.world_unlocks,
|
||||
"world_songs": self.world_songs,
|
||||
"singles": self.singles,
|
||||
"packs": self.packs,
|
||||
"characters": character_list,
|
||||
"cores": self.cores,
|
||||
"recent_score": self.recent_score_list,
|
||||
"max_friend": Constant.MAX_FRIEND_COUNT,
|
||||
"rating": self.rating_ptt,
|
||||
"join_date": self.join_date,
|
||||
"global_rank": self.global_rank,
|
||||
'country': '',
|
||||
'course_banners': self.course_banners,
|
||||
'world_mode_locked_end_ts': self.world_mode_locked_end_ts,
|
||||
'locked_char_ids': [] # [1]
|
||||
}
|
||||
|
||||
def from_list(self, x: list) -> 'UserInfo':
|
||||
'''从数据库user表全部数据获取信息'''
|
||||
if not x:
|
||||
return None
|
||||
if self.user_id is None:
|
||||
self.user_id = x[0]
|
||||
self.name = x[1]
|
||||
self.join_date = int(x[3])
|
||||
self.user_code = x[4]
|
||||
self.rating_ptt = x[5]
|
||||
self.character = UserCharacter(self.c, x[6])
|
||||
self.is_skill_sealed = x[7] == 1
|
||||
self.character.is_uncapped = x[8] == 1
|
||||
self.character.is_uncapped_override = x[9] == 1
|
||||
self.is_hide_rating = x[10] == 1
|
||||
|
||||
self.recent_score.song.song_id = x[11]
|
||||
self.recent_score.song.difficulty = x[12]
|
||||
self.recent_score.set_score(
|
||||
x[13], x[14], x[15], x[16], x[17], x[18], x[19], x[20], x[21])
|
||||
self.recent_score.rating = x[22]
|
||||
|
||||
self.favorite_character = None if x[23] == - \
|
||||
1 else UserCharacter(self.c, x[23])
|
||||
self.max_stamina_notification_enabled = x[24] == 1
|
||||
self.current_map = Map(x[25]) if x[25] is not None else Map('')
|
||||
self.ticket = x[26]
|
||||
self.prog_boost = x[27] if x[27] is not None else 0
|
||||
self.email = x[28] if x[28] is not None else ''
|
||||
self.world_rank_score = x[29] if x[29] is not None else 0
|
||||
self.ban_flag = x[30] if x[30] is not None else ''
|
||||
|
||||
self.next_fragstam_ts = x[31] if x[31] else 0
|
||||
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[32], x[33])
|
||||
self.world_mode_locked_end_ts = x[34] if x[34] else -1
|
||||
|
||||
return self
|
||||
|
||||
def select_user(self) -> None:
|
||||
# 查user表所有信息
|
||||
self.c.execute(
|
||||
'''select * from user where user_id = :x''', {'x': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.from_list(x)
|
||||
|
||||
def select_user_about_current_map(self) -> None:
|
||||
self.c.execute('''select current_map from user where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.current_map = Map(x[0])
|
||||
|
||||
def select_user_about_stamina(self) -> None:
|
||||
self.c.execute('''select max_stamina_ts, stamina from user where user_id = :a''',
|
||||
{'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[0], x[1])
|
||||
|
||||
def from_list_about_character(self, x: list) -> None:
|
||||
'''从数据库user表获取搭档信息'''
|
||||
self.name = x[0]
|
||||
self.character = UserCharacter(self.c, x[1], self)
|
||||
self.is_skill_sealed = x[2] == 1
|
||||
self.character.is_uncapped = x[3] == 1
|
||||
self.character.is_uncapped_override = x[4] == 1
|
||||
self.favorite_character = None if x[5] == - \
|
||||
1 else UserCharacter(self.c, x[5], self)
|
||||
|
||||
def select_user_about_character(self) -> None:
|
||||
'''
|
||||
查询user表有关搭档的信息
|
||||
'''
|
||||
self.c.execute('''select name, character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, favorite_character from user where user_id = :a''', {
|
||||
'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.from_list_about_character(x)
|
||||
|
||||
def select_user_about_world_play(self) -> None:
|
||||
'''
|
||||
查询user表有关世界模式打歌的信息
|
||||
'''
|
||||
self.c.execute(
|
||||
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts from user where user_id=?''', (self.user_id,))
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.character = UserCharacter(self.c, x[0], self)
|
||||
self.stamina = UserStamina(self.c, self)
|
||||
self.stamina.set_value(x[1], x[2])
|
||||
self.is_skill_sealed = x[3] == 1
|
||||
self.character.is_uncapped = x[4] == 1
|
||||
self.character.is_uncapped_override = x[5] == 1
|
||||
self.current_map = UserMap(self.c, x[6], self)
|
||||
self.world_mode_locked_end_ts = x[7] if x[7] else -1
|
||||
|
||||
@property
|
||||
def global_rank(self) -> int:
|
||||
'''用户世界排名,如果超过设定最大值,返回0'''
|
||||
if self.world_rank_score is None:
|
||||
self.select_user_one_column('world_rank_score', 0)
|
||||
if self.world_rank_score is None:
|
||||
return 0
|
||||
|
||||
self.c.execute(
|
||||
'''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,))
|
||||
y = self.c.fetchone()
|
||||
if y and y[0] + 1 <= Config.WORLD_RANK_MAX:
|
||||
return y[0] + 1
|
||||
|
||||
return 0
|
||||
|
||||
def update_global_rank(self) -> None:
|
||||
'''用户世界排名计算,有新增成绩则要更新'''
|
||||
|
||||
self.c.execute('''select song_id, rating_ftr, rating_byn from chart''')
|
||||
x = self.c.fetchall()
|
||||
|
||||
song_list_ftr = [self.user_id]
|
||||
song_list_byn = [self.user_id]
|
||||
for i in x:
|
||||
if i[1] > 0:
|
||||
song_list_ftr.append(i[0])
|
||||
if i[2] > 0:
|
||||
song_list_byn.append(i[0])
|
||||
|
||||
score_sum = 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]
|
||||
|
||||
if len(song_list_byn) >= 2:
|
||||
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({0})'''.format(
|
||||
','.join(['?']*(len(song_list_byn)-1))), tuple(song_list_byn))
|
||||
|
||||
x = self.c.fetchone()
|
||||
if x[0] is not None:
|
||||
score_sum += x[0]
|
||||
|
||||
self.c.execute('''update user set world_rank_score = :b where user_id = :a''', {
|
||||
'a': self.user_id, 'b': score_sum})
|
||||
|
||||
self.world_rank_score = score_sum
|
||||
|
||||
def select_user_one_column(self, column_name: str, default_value=None) -> None:
|
||||
'''
|
||||
查询user表的某个属性\
|
||||
请注意必须是一个普通属性,不能是一个类的实例
|
||||
'''
|
||||
if column_name not in self.__dict__:
|
||||
raise InputError('No such column.')
|
||||
self.c.execute('''select %s from user where user_id = :a''' %
|
||||
column_name, {'a': self.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('No user.', 108, -3)
|
||||
|
||||
self.__dict__[column_name] = x[0] if x[0] else default_value
|
||||
|
||||
def update_user_one_column(self, column_name: str, value=None) -> None:
|
||||
'''
|
||||
更新user表的某个属性\
|
||||
请注意必须是一个普通属性,不能是一个类的实例
|
||||
'''
|
||||
if column_name not in self.__dict__:
|
||||
raise InputError('No such column.')
|
||||
if value is not None:
|
||||
self.__dict__[column_name] = value
|
||||
self.c.execute('''update user set %s = :a where user_id = :b''' %
|
||||
column_name, {'a': self.__dict__[column_name], 'b': self.user_id})
|
||||
|
||||
|
||||
class UserOnline(UserInfo):
|
||||
|
||||
def __init__(self, c, user_id=None) -> None:
|
||||
super().__init__(c, user_id)
|
||||
|
||||
def change_character(self, character_id: int, skill_sealed: bool = False):
|
||||
'''用户角色改变,包括技能封印的改变'''
|
||||
self.character = UserCharacter(self.c, character_id, self)
|
||||
self.character.select_character_uncap_condition()
|
||||
self.is_skill_sealed = skill_sealed
|
||||
|
||||
self.c.execute('''update user set is_skill_sealed = :a, character_id = :b, is_char_uncapped = :c, is_char_uncapped_override = :d where user_id = :e''', {
|
||||
'a': 1 if self.is_skill_sealed else 0, 'b': self.character.character_id, 'c': self.character.is_uncapped, 'd': self.character.is_uncapped_override, 'e': self.user_id})
|
||||
|
||||
def add_friend(self, friend_id: int):
|
||||
'''加好友'''
|
||||
if self.user_id == friend_id:
|
||||
raise FriendError('Add yourself as a friend.', 604)
|
||||
|
||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
if self.c.fetchone() == (0,):
|
||||
self.c.execute('''insert into friend values(:a, :b)''', {
|
||||
'a': self.user_id, 'b': friend_id})
|
||||
else:
|
||||
raise FriendError('The user has been your friend.', 602)
|
||||
|
||||
def delete_friend(self, friend_id: int):
|
||||
'''删好友'''
|
||||
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
if self.c.fetchone() == (1,):
|
||||
self.c.execute('''delete from friend where user_id_me = :x and user_id_other = :y''',
|
||||
{'x': self.user_id, 'y': friend_id})
|
||||
else:
|
||||
raise FriendError('No user or the user is not your friend.', 401)
|
||||
|
||||
def change_favorite_character(self, character_id: int) -> None:
|
||||
'''更改用户的favorite_character'''
|
||||
self.favorite_character = UserCharacter(self.c, character_id, self)
|
||||
self.c.execute('''update user set favorite_character = :a where user_id = :b''',
|
||||
{'a': self.favorite_character.character_id, 'b': self.user_id})
|
||||
|
||||
|
||||
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':
|
||||
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}))
|
||||
41
latest version/core/util.py
Normal file
41
latest version/core/util.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
||||
def md5(code: str) -> str:
|
||||
# md5加密算法
|
||||
code = code.encode()
|
||||
md5s = hashlib.md5()
|
||||
md5s.update(code)
|
||||
codes = md5s.hexdigest()
|
||||
|
||||
return codes
|
||||
|
||||
|
||||
def get_file_md5(file_path: str) -> str:
|
||||
'''计算文件MD5'''
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
myhash = hashlib.md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
b = f.read(8096)
|
||||
if not b:
|
||||
break
|
||||
myhash.update(b)
|
||||
|
||||
return myhash.hexdigest()
|
||||
|
||||
|
||||
def 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
|
||||
679
latest version/core/world.py
Normal file
679
latest version/core/world.py
Normal file
@@ -0,0 +1,679 @@
|
||||
import json
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from random import random
|
||||
from time import time
|
||||
|
||||
from .character import Character
|
||||
from .constant import Constant
|
||||
from .error import InputError, MapLocked, NoData
|
||||
from .item import ItemFactory
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list:
|
||||
'''获取所有地图名称,返回列表'''
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(file_dir):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] == '.json':
|
||||
file_list.append(os.path.splitext(file)[0])
|
||||
return file_list
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_world_info(map_id: str) -> dict:
|
||||
'''读取json文件内容,返回字典'''
|
||||
world_info = {}
|
||||
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, map_id+'.json'), 'r') as f:
|
||||
world_info = json.load(f)
|
||||
|
||||
return world_info
|
||||
|
||||
|
||||
def get_world_all(c, user) -> list:
|
||||
'''
|
||||
读取所有地图信息,返回列表\
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
worlds = get_world_name()
|
||||
return [UserMap(c, map_id, user) for map_id in worlds]
|
||||
|
||||
|
||||
class Step:
|
||||
'''台阶类'''
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.postion: int = None
|
||||
self.capture: int = None
|
||||
self.items: list = []
|
||||
self.restrict_id: str = None
|
||||
self.restrict_ids: list = []
|
||||
self.restrict_type: str = None
|
||||
self.step_type: list = None
|
||||
self.speed_limit_value: int = None
|
||||
self.plus_stamina_value: int = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
r = {
|
||||
'position': self.position,
|
||||
'capture': self.capture,
|
||||
}
|
||||
if self.items:
|
||||
r['items'] = [i.to_dict() for i in self.items]
|
||||
if self.restrict_id:
|
||||
r['restrict_id'] = self.restrict_id
|
||||
if self.restrict_ids:
|
||||
r['restrict_ids'] = self.restrict_ids
|
||||
if self.restrict_type:
|
||||
r['restrict_type'] = self.restrict_type
|
||||
if self.step_type:
|
||||
r['step_type'] = self.step_type
|
||||
if self.speed_limit_value:
|
||||
r['speed_limit_value'] = self.speed_limit_value
|
||||
if self.plus_stamina_value:
|
||||
r['plus_stamina_value'] = self.plus_stamina_value
|
||||
|
||||
return r
|
||||
|
||||
def from_dict(self, d: dict) -> 'Step':
|
||||
self.position = d['position']
|
||||
self.capture = d['capture']
|
||||
self.restrict_id = d.get('restrict_id')
|
||||
self.restrict_ids = d.get('restrict_ids')
|
||||
self.restrict_type = d.get('restrict_type')
|
||||
self.step_type = d.get('step_type')
|
||||
self.speed_limit_value = d.get('speed_limit_value')
|
||||
self.plus_stamina_value = d.get('plus_stamina_value')
|
||||
if 'items' in d:
|
||||
self.items = [ItemFactory.from_dict(i) for i in d['items']]
|
||||
return self
|
||||
|
||||
|
||||
class Map:
|
||||
def __init__(self, map_id: str = None) -> None:
|
||||
self.map_id: str = map_id
|
||||
self.is_legacy: bool = None
|
||||
self.is_beyond: bool = None
|
||||
self.beyond_health: int = None
|
||||
self.character_affinity: list = []
|
||||
self.affinity_multiplier: list = []
|
||||
self.chapter: int = None
|
||||
self.available_from: int = None
|
||||
self.available_to: int = None
|
||||
self.is_repeatable: bool = None
|
||||
self.require_id: str = None
|
||||
self.require_type: str = None
|
||||
self.require_value: int = None
|
||||
self.coordinate: str = None
|
||||
self.custom_bg: str = None
|
||||
self.stamina_cost: int = None
|
||||
self.steps: list = []
|
||||
self.__rewards: list = None
|
||||
|
||||
@property
|
||||
def rewards(self) -> list:
|
||||
if self.__rewards is None:
|
||||
self.get_rewards()
|
||||
return self.__rewards
|
||||
|
||||
def get_rewards(self) -> list:
|
||||
if self.steps:
|
||||
self.__rewards = []
|
||||
for step in self.steps:
|
||||
if step.items:
|
||||
self.__rewards.append(
|
||||
{'items': [i.to_dict() for i in step.items], 'position': step.position})
|
||||
return self.__rewards
|
||||
|
||||
@property
|
||||
def step_count(self):
|
||||
return len(self.steps)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
if self.chapter is None:
|
||||
self.select_map_info()
|
||||
return {
|
||||
'map_id': self.map_id,
|
||||
'is_legacy': self.is_legacy,
|
||||
'is_beyond': self.is_beyond,
|
||||
'beyond_health': self.beyond_health,
|
||||
'character_affinity': self.character_affinity,
|
||||
'affinity_multiplier': self.affinity_multiplier,
|
||||
'chapter': self.chapter,
|
||||
'available_from': self.available_from,
|
||||
'available_to': self.available_to,
|
||||
'is_repeatable': self.is_repeatable,
|
||||
'require_id': self.require_id,
|
||||
'require_type': self.require_type,
|
||||
'require_value': self.require_value,
|
||||
'coordinate': self.coordinate,
|
||||
'custom_bg': self.custom_bg,
|
||||
'stamina_cost': self.stamina_cost,
|
||||
'step_count': self.step_count,
|
||||
'steps': [s.to_dict() for s in self.steps],
|
||||
}
|
||||
|
||||
def from_dict(self, raw_dict: dict) -> 'Map':
|
||||
self.is_legacy = raw_dict.get('is_legacy')
|
||||
self.is_beyond = raw_dict.get('is_beyond')
|
||||
self.beyond_health = raw_dict.get('beyond_health')
|
||||
self.character_affinity = raw_dict.get('character_affinity', [])
|
||||
self.affinity_multiplier = raw_dict.get('affinity_multiplier', [])
|
||||
self.chapter = raw_dict.get('chapter')
|
||||
self.available_from = raw_dict.get('available_from', -1)
|
||||
self.available_to = raw_dict.get('available_to', 9999999999999)
|
||||
self.is_repeatable = raw_dict.get('is_repeatable')
|
||||
self.require_id = raw_dict.get('require_id', '')
|
||||
self.require_type = raw_dict.get('require_type', '')
|
||||
self.require_value = raw_dict.get('require_value', 1)
|
||||
self.coordinate = raw_dict.get('coordinate')
|
||||
self.custom_bg = raw_dict.get('custom_bg', '')
|
||||
self.stamina_cost = raw_dict.get('stamina_cost')
|
||||
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
|
||||
return self
|
||||
|
||||
def select_map_info(self):
|
||||
'''获取地图信息'''
|
||||
self.from_dict(get_world_info(self.map_id))
|
||||
|
||||
|
||||
class UserMap(Map):
|
||||
'''
|
||||
用户地图类\
|
||||
parameters: `user` - `User`类或者子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, map_id: str = None, user=None) -> None:
|
||||
super().__init__(map_id)
|
||||
self.c = c
|
||||
self.curr_position: int = None
|
||||
self.curr_capture: int = None
|
||||
self.is_locked: bool = None
|
||||
|
||||
self.prev_position: int = None
|
||||
self.prev_capture: int = None
|
||||
|
||||
self.user = user
|
||||
|
||||
@property
|
||||
def rewards_for_climbing(self) -> list:
|
||||
rewards = []
|
||||
for i in range(self.prev_position+1, self.curr_position+1):
|
||||
step = self.steps[i]
|
||||
if step.items:
|
||||
rewards.append(
|
||||
{'items': step.items, 'position': step.position})
|
||||
|
||||
return rewards
|
||||
|
||||
def rewards_for_climbing_to_dict(self) -> list:
|
||||
rewards = []
|
||||
for i in range(self.prev_position+1, self.curr_position+1):
|
||||
step = self.steps[i]
|
||||
if step.items:
|
||||
rewards.append(
|
||||
{'items': [i.to_dict() for i in step.items], 'position': step.position})
|
||||
|
||||
return rewards
|
||||
|
||||
@property
|
||||
def steps_for_climbing(self) -> list:
|
||||
return self.steps[self.prev_position:self.curr_position+1]
|
||||
|
||||
def to_dict(self, has_map_info: bool = False, has_steps: bool = False, has_rewards: bool = False) -> dict:
|
||||
if self.is_locked is None:
|
||||
self.select()
|
||||
if has_map_info:
|
||||
if self.chapter is None:
|
||||
self.select_map_info()
|
||||
r = super().to_dict()
|
||||
r['curr_position'] = self.curr_position
|
||||
r['curr_capture'] = self.curr_capture
|
||||
r['is_locked'] = self.is_locked
|
||||
r['user_id'] = self.user.user_id
|
||||
if not has_steps:
|
||||
del r['steps']
|
||||
if has_rewards:
|
||||
r['rewards'] = self.rewards
|
||||
else:
|
||||
r = {
|
||||
'map_id': self.map_id,
|
||||
'curr_position': self.curr_position,
|
||||
'curr_capture': self.curr_capture,
|
||||
'is_locked': self.is_locked,
|
||||
'user_id': self.user.user_id,
|
||||
}
|
||||
return r
|
||||
|
||||
def initialize(self):
|
||||
'''初始化数据库信息'''
|
||||
self.c.execute('''insert into user_world values(:a,:b,0,0,1)''', {
|
||||
'a': self.user.user_id, 'b': self.map_id})
|
||||
|
||||
def update(self):
|
||||
'''向数据库更新信息'''
|
||||
self.c.execute('''update user_world set curr_position=:a,curr_capture=:b,is_locked=:c where user_id=:d and map_id=:e''', {
|
||||
'a': self.curr_position, 'b': self.curr_capture, 'c': 1 if self.is_locked else 0, 'd': self.user.user_id, 'e': self.map_id})
|
||||
|
||||
def select(self):
|
||||
'''获取用户在此地图的信息'''
|
||||
self.c.execute('''select curr_position, curr_capture, is_locked from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': self.map_id, 'b': self.user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if x:
|
||||
self.curr_position = x[0]
|
||||
self.curr_capture = x[1]
|
||||
self.is_locked = x[2] == 1
|
||||
else:
|
||||
self.curr_position = 0
|
||||
self.curr_capture = 0
|
||||
self.is_locked = True
|
||||
self.initialize()
|
||||
|
||||
def change_user_current_map(self):
|
||||
'''改变用户当前地图为此地图'''
|
||||
self.user.current_map = self
|
||||
self.c.execute('''update user set current_map = :a where user_id=:b''', {
|
||||
'a': self.map_id, 'b': self.user.user_id})
|
||||
|
||||
def unlock(self) -> bool:
|
||||
'''解锁用户此地图,返回成功与否bool值'''
|
||||
self.select()
|
||||
|
||||
if self.is_locked:
|
||||
self.is_locked = False
|
||||
self.curr_position = 0
|
||||
self.curr_capture = 0
|
||||
self.select_map_info()
|
||||
if self.require_type is not None and self.require_type != '':
|
||||
if self.require_type in ['pack', 'single']:
|
||||
item = ItemFactory(self.c).get_item(self.require_type)
|
||||
item.item_id = self.require_id
|
||||
item.select(self.user)
|
||||
if not item.amount:
|
||||
self.is_locked = True
|
||||
|
||||
self.update()
|
||||
|
||||
return not self.is_locked
|
||||
|
||||
def climb(self, step_value: float) -> None:
|
||||
'''爬梯子,数值非负'''
|
||||
if step_value < 0:
|
||||
raise InputError('`Step_value` must be non-negative.')
|
||||
if self.curr_position is None:
|
||||
self.select()
|
||||
if self.is_beyond is None:
|
||||
self.select_map_info()
|
||||
if self.is_locked:
|
||||
raise MapLocked('The map is locked.')
|
||||
|
||||
self.prev_capture = self.curr_capture
|
||||
self.prev_position = self.curr_position
|
||||
|
||||
if self.is_beyond: # beyond判断
|
||||
dt = self.beyond_health - self.prev_capture
|
||||
self.curr_capture = self.prev_capture + \
|
||||
step_value if dt >= step_value else self.beyond_health
|
||||
|
||||
i = 0
|
||||
t = self.prev_capture + step_value
|
||||
while i < self.step_count and t > 0:
|
||||
dt = self.steps[i].capture
|
||||
if dt > t:
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
i += 1
|
||||
if i >= self.step_count:
|
||||
self.curr_position = self.step_count - 1
|
||||
else:
|
||||
self.curr_position = i
|
||||
|
||||
else:
|
||||
i = self.prev_position
|
||||
j = self.prev_capture
|
||||
t = step_value
|
||||
while t > 0 and i < self.step_count:
|
||||
dt = self.steps[i].capture - j
|
||||
if dt > t:
|
||||
j += t
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
j = 0
|
||||
i += 1
|
||||
if i >= self.step_count:
|
||||
self.curr_position = self.step_count - 1
|
||||
self.curr_capture = 0
|
||||
else:
|
||||
self.curr_position = i
|
||||
self.curr_capture = j
|
||||
|
||||
def reclimb(self, step_value: float) -> None:
|
||||
'''重新爬梯子计算'''
|
||||
self.curr_position = self.prev_position
|
||||
self.curr_capture = self.prev_capture
|
||||
self.climb(step_value)
|
||||
|
||||
|
||||
class Stamina:
|
||||
'''
|
||||
体力类
|
||||
'''
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__stamina: int = None
|
||||
self.max_stamina_ts: int = None
|
||||
|
||||
def set_value(self, max_stamina_ts: int, stamina: int):
|
||||
self.max_stamina_ts = int(max_stamina_ts) if max_stamina_ts else 0
|
||||
self.__stamina = int(stamina) if stamina else Constant.MAX_STAMINA
|
||||
|
||||
@property
|
||||
def stamina(self) -> int:
|
||||
'''通过计算得到当前的正确体力值'''
|
||||
stamina = round(Constant.MAX_STAMINA - (self.max_stamina_ts -
|
||||
int(time()*1000)) / Constant.STAMINA_RECOVER_TICK)
|
||||
|
||||
if stamina >= Constant.MAX_STAMINA:
|
||||
if self.__stamina >= Constant.MAX_STAMINA:
|
||||
stamina = self.__stamina
|
||||
else:
|
||||
stamina = Constant.MAX_STAMINA
|
||||
|
||||
return stamina
|
||||
|
||||
@stamina.setter
|
||||
def stamina(self, value: int) -> None:
|
||||
'''设置体力值,此处会导致max_stamina_ts变化'''
|
||||
self.__stamina = round(value)
|
||||
self.max_stamina_ts = int(
|
||||
time()*1000) - (self.__stamina-Constant.MAX_STAMINA) * Constant.STAMINA_RECOVER_TICK
|
||||
|
||||
|
||||
class UserStamina(Stamina):
|
||||
'''
|
||||
用户体力类\
|
||||
parameter: `user` - `User`类或子类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.user = user
|
||||
|
||||
def select(self):
|
||||
'''获取用户体力信息'''
|
||||
self.c.execute('''select max_stamina_ts, staminafrom user where user_id = :a''',
|
||||
{'a': self.user.user_id})
|
||||
x = self.c.fetchone()
|
||||
if not x:
|
||||
raise NoData('The user does not exist.')
|
||||
self.set_value(x[0], x[1])
|
||||
|
||||
def update(self):
|
||||
'''向数据库更新信息'''
|
||||
self.c.execute('''update user set max_stamina_ts=:b, stamina=:a where user_id=:c''', {
|
||||
'a': self.stamina, 'b': self.max_stamina_ts, 'c': self.user.user_id})
|
||||
|
||||
|
||||
class WorldPlay:
|
||||
'''
|
||||
世界模式打歌类,处理特殊角色技能,联动UserMap和UserPlay\
|
||||
parameter: `user` - `UserOnline`类或子类的实例\
|
||||
'user_play` - `UserPlay`类的实例
|
||||
'''
|
||||
|
||||
def __init__(self, c=None, user=None, user_play=None) -> None:
|
||||
self.c = c
|
||||
self.user = user
|
||||
self.user_play = user_play
|
||||
self.character_used = None
|
||||
|
||||
self.base_step_value: float = None
|
||||
self.step_value: float = None
|
||||
|
||||
self.prog_tempest: float = None
|
||||
self.overdrive_extra: float = None
|
||||
self.character_bonus_progress: float = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
arcmap: 'UserMap' = self.user.current_map
|
||||
r = {
|
||||
"rewards": arcmap.rewards_for_climbing_to_dict(),
|
||||
"exp": self.character_used.level.exp,
|
||||
"level": self.character_used.level.level,
|
||||
"base_progress": self.base_step_value,
|
||||
"progress": self.step_value,
|
||||
"user_map": {
|
||||
"user_id": self.user.user_id,
|
||||
"curr_position": arcmap.curr_position,
|
||||
"curr_capture": arcmap.curr_capture,
|
||||
"is_locked": arcmap.is_locked,
|
||||
"map_id": arcmap.map_id,
|
||||
"prev_capture": arcmap.prev_capture,
|
||||
"prev_position": arcmap.prev_position,
|
||||
"beyond_health": arcmap.beyond_health
|
||||
},
|
||||
"char_stats": {
|
||||
"character_id": self.character_used.character_id,
|
||||
"frag": self.character_used.frag.get_value(self.character_used.level),
|
||||
"prog": self.character_used.prog.get_value(self.character_used.level),
|
||||
"overdrive": self.character_used.overdrive.get_value(self.character_used.level)
|
||||
},
|
||||
"current_stamina": self.user.stamina.stamina,
|
||||
"max_stamina_ts": self.user.stamina.max_stamina_ts,
|
||||
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts
|
||||
}
|
||||
|
||||
if self.overdrive_extra is not None:
|
||||
r['char_stats']['overdrive'] += self.overdrive_extra
|
||||
|
||||
if self.prog_tempest is not None:
|
||||
r['char_stats']['prog'] += self.prog_tempest
|
||||
r['char_stats']['prog_tempest'] = self.prog_tempest
|
||||
|
||||
if self.character_bonus_progress is not None:
|
||||
# 猜的,为了让客户端正确显示,当然结果是没问题的
|
||||
# r['base_progress'] += self.character_bonus_progress # 肯定不是这样的
|
||||
r['character_bonus_progress'] = self.character_bonus_progress
|
||||
|
||||
if self.user_play.beyond_gauge == 0:
|
||||
r["user_map"]["steps"] = [
|
||||
x.to_dict() for x in arcmap.steps_for_climbing]
|
||||
else:
|
||||
r["user_map"]["steps"] = len(arcmap.steps_for_climbing)
|
||||
|
||||
if self.user_play.stamina_multiply != 1:
|
||||
r['stamina_multiply'] = self.user_play.stamina_multiply
|
||||
if self.user_play.fragment_multiply != 100:
|
||||
r['fragment_multiply'] = self.user_play.fragment_multiply
|
||||
if self.user_play.prog_boost_multiply != 0:
|
||||
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
|
||||
|
||||
return r
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@property
|
||||
def exp_times(self) -> float:
|
||||
return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply+100) / 100
|
||||
|
||||
def get_step(self) -> None:
|
||||
if self.user_play.beyond_gauge == 0:
|
||||
self.base_step_value = 2.5 + 2.45 * self.user_play.rating**0.5
|
||||
prog = self.character_used.prog.get_value(
|
||||
self.character_used.level)
|
||||
if self.prog_tempest:
|
||||
prog += self.prog_tempest
|
||||
|
||||
self.step_value = self.base_step_value * prog / 50 * self.step_times
|
||||
else:
|
||||
if self.user_play.clear_type == 0:
|
||||
self.base_step_value = 25/28 + \
|
||||
(self.user_play.rating)**0.5 * 0.43
|
||||
else:
|
||||
self.base_step_value = 75/28 + \
|
||||
(self.user_play.rating)**0.5 * 0.43
|
||||
|
||||
if self.character_used.character_id in self.user.current_map.character_affinity:
|
||||
affinity_multiplier = self.user.current_map.affinity_multiplier[self.user.current_map.character_affinity.index(
|
||||
self.character_used.character_id)]
|
||||
else:
|
||||
affinity_multiplier = 1
|
||||
|
||||
overdrive = self.character_used.overdrive.get_value(
|
||||
self.character_used.level)
|
||||
if self.overdrive_extra:
|
||||
overdrive += self.overdrive_extra
|
||||
|
||||
self.step_value = self.base_step_value * overdrive / \
|
||||
50 * self.step_times * affinity_multiplier
|
||||
|
||||
def update(self) -> None:
|
||||
'''世界模式更新'''
|
||||
if self.user_play.prog_boost_multiply != 0:
|
||||
self.user.update_user_one_column('prog_boost', 0)
|
||||
|
||||
self.user_play.clear_play_state()
|
||||
self.user.select_user_about_world_play()
|
||||
|
||||
self.character_used = Character()
|
||||
|
||||
self.user.character.select_character_info()
|
||||
if not self.user.is_skill_sealed:
|
||||
self.character_used = self.user.character
|
||||
else:
|
||||
self.character_used.character_id = self.user.character.character_id
|
||||
self.character_used.level.level = self.user.character.level.level
|
||||
self.character_used.level.exp = self.user.character.level.exp
|
||||
self.character_used.frag.set_parameter(50, 50, 50)
|
||||
self.character_used.prog.set_parameter(50, 50, 50)
|
||||
self.character_used.overdrive.set_parameter(50, 50, 50)
|
||||
|
||||
self.user.current_map.select_map_info()
|
||||
self.before_calculate()
|
||||
self.get_step()
|
||||
self.user.current_map.climb(self.step_value)
|
||||
self.after_climb()
|
||||
|
||||
for i in self.user.current_map.rewards_for_climbing: # 物品分发
|
||||
for j in i['items']:
|
||||
j.c = self.c
|
||||
j.user_claim_item(self.user)
|
||||
|
||||
x: 'Step' = self.user.current_map.steps_for_climbing[-1]
|
||||
if x.step_type:
|
||||
if 'plusstamina' in x.step_type and x.plus_stamina_value:
|
||||
# 体力格子
|
||||
self.user.stamina.stamina += x.plus_stamina_value
|
||||
self.user.stamina.update()
|
||||
|
||||
# 角色升级
|
||||
if self.character_used.database_table_name == 'user_char':
|
||||
self.character_used.upgrade(
|
||||
self.user, self.exp_times*self.user_play.rating*6)
|
||||
|
||||
if self.user.current_map.curr_position == self.user.current_map.step_count-1 and self.user.current_map.is_repeatable: # 循环图判断
|
||||
self.user.current_map.curr_position = 0
|
||||
|
||||
self.user.current_map.update()
|
||||
|
||||
def before_calculate(self) -> None:
|
||||
if self.user_play.beyond_gauge == 0:
|
||||
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
|
||||
self._special_tempest()
|
||||
else:
|
||||
if self.character_used.skill_id_displayed == 'skill_vita':
|
||||
self._skill_vita()
|
||||
|
||||
def after_climb(self) -> None:
|
||||
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
|
||||
'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:
|
||||
factory_dict[self.character_used.skill_id_displayed]()
|
||||
|
||||
def _special_tempest(self) -> None:
|
||||
'''风暴对立技能,prog随全角色等级提升'''
|
||||
if self.character_used.database_table_name == 'user_char_full':
|
||||
self.prog_tempest = 60
|
||||
else:
|
||||
self.c.execute(
|
||||
'''select sum(level) from user_char where user_id=?''', (self.user.user_id,))
|
||||
x = self.c.fetchone()
|
||||
self.prog_tempest = int(x[0]) / 10 if x else 0
|
||||
if self.prog_tempest > 60:
|
||||
self.prog_tempest = 60
|
||||
elif self.prog_tempest < 0:
|
||||
self.prog_tempest = 0
|
||||
|
||||
def _skill_vita(self) -> None:
|
||||
'''
|
||||
vita技能,overdrive随回忆率提升,提升量最多为10\
|
||||
此处采用线性函数
|
||||
'''
|
||||
self.overdrive_extra = 0
|
||||
if 0 < self.user_play.health <= 100:
|
||||
self.overdrive_extra = self.user_play.health / 10
|
||||
|
||||
def _eto_uncap(self) -> None:
|
||||
'''eto觉醒技能,获得残片奖励时世界模式进度加7'''
|
||||
fragment_flag = False
|
||||
|
||||
for i in self.user.current_map.rewards_for_climbing:
|
||||
for j in i['items']:
|
||||
if j.item_type == 'fragment':
|
||||
fragment_flag = True
|
||||
break
|
||||
if fragment_flag:
|
||||
break
|
||||
|
||||
if fragment_flag:
|
||||
self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS
|
||||
self.step_value += self.character_bonus_progress * self.step_times
|
||||
|
||||
self.user.current_map.reclimb(self.step_value)
|
||||
|
||||
def _luna_uncap(self) -> None:
|
||||
'''luna觉醒技能,限制格开始时世界模式进度加7'''
|
||||
x: 'Step' = self.user.current_map.steps_for_climbing[0]
|
||||
if x.restrict_id and x.restrict_type:
|
||||
self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
|
||||
self.step_value += self.character_bonus_progress * self.step_times
|
||||
|
||||
self.user.current_map.reclimb(self.step_value)
|
||||
|
||||
def _ayu_uncap(self) -> None:
|
||||
'''ayu觉醒技能,世界模式进度+5或-5,但不会小于0'''
|
||||
|
||||
self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random(
|
||||
) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS
|
||||
|
||||
self.step_value += self.character_bonus_progress * self.step_times
|
||||
if self.step_value < 0:
|
||||
self.character_bonus_progress += self.step_value / self.step_times
|
||||
self.step_value = 0
|
||||
|
||||
self.user.current_map.reclimb(self.step_value)
|
||||
|
||||
def _skill_fatalis(self) -> None:
|
||||
'''hikari fatalis技能,世界模式超载,打完休息60分钟'''
|
||||
|
||||
self.user.world_mode_locked_end_ts = int(
|
||||
time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME
|
||||
self.user.update_user_one_column('world_mode_locked_end_ts')
|
||||
|
||||
def _skill_amane(self) -> None:
|
||||
'''
|
||||
amane技能,起始格为限速或随机,成绩小于EX时,世界模式进度减半
|
||||
偷懒在after_climb里面,需要重爬一次
|
||||
'''
|
||||
x: 'Step' = self.user.current_map.steps_for_climbing[0]
|
||||
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
|
||||
self.character_bonus_progress = -self.step_value / 2 / self.step_times
|
||||
self.step_value = self.step_value / 2
|
||||
self.user.current_map.reclimb(self.step_value)
|
||||
16
latest version/database/database_initialize.py
Normal file
16
latest version/database/database_initialize.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
|
||||
# 数据库初始化文件,删掉arcaea_database.db文件后运行即可,谨慎使用
|
||||
|
||||
|
||||
def main(core_path: str = '../', db_path: str = './arcaea_database.db', init_folder_path: str = './init'):
|
||||
sys.path.append(os.path.join(sys.path[0], core_path))
|
||||
sys.path.append(os.path.join(sys.path[0], core_path, './core/'))
|
||||
init = import_module('init').DatabaseInit(db_path, init_folder_path)
|
||||
init.init()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
86
latest version/database/init/arc_data.py
Normal file
86
latest version/database/init/arc_data.py
Normal file
@@ -0,0 +1,86 @@
|
||||
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']
|
||||
|
||||
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_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
|
||||
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
||||
|
||||
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 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]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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, 35, 86, 92]
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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}]
|
||||
}
|
||||
|
||||
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']
|
||||
|
||||
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']
|
||||
|
||||
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']
|
||||
power_caption = ['系统权限', '总体查询权限', '自我查询权限', '总体修改权限',
|
||||
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限']
|
||||
|
||||
role_power = {'system': power,
|
||||
'admin': ['select', 'select_me', 'change_me', 'grant_inf', 'select_song_rank', 'select_song_info'],
|
||||
'user': ['select_me', 'change_me', 'select_song_rank', 'select_song_info'],
|
||||
'selector': ['select']
|
||||
}
|
||||
449
latest version/database/init/courses.json
Normal file
449
latest version/database/init/courses.json
Normal file
@@ -0,0 +1,449 @@
|
||||
[
|
||||
{
|
||||
"course_id": "4.0-dan-1",
|
||||
"style": 1,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "inkarusi"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "snowwhite"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "sakurafubuki"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 1,
|
||||
"id": "purgatorium"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 1",
|
||||
"course_name": "新世界的第一步",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment300",
|
||||
"course_banner_1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-2",
|
||||
"style": 2,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "vexaria"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "clotho"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "brandnewworld"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "oblivia"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 2",
|
||||
"course_name": "一次心跳的加速",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment400",
|
||||
"course_banner_2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-3",
|
||||
"style": 3,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "suomi"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "grimheart"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "bookmaker"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "avril"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 3",
|
||||
"course_name": "永不屈服的精神",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment500",
|
||||
"course_banner_3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-4",
|
||||
"style": 4,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "gekka"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "solitarydream"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "kanagawa"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "iconoclast"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 4",
|
||||
"course_name": "光芒的永恒之地",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment600",
|
||||
"course_banner_4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-5",
|
||||
"style": 5,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "faintlight"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "eveninginscarlet"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "kyogenkigo"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "blrink"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 5",
|
||||
"course_name": "玻璃之中的残酷",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment700",
|
||||
"course_banner_5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-6",
|
||||
"style": 6,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "vindication"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "heartjackin"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "ascent"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "macromod"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 6",
|
||||
"course_name": "沉浸于悲喜交加",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment800",
|
||||
"course_banner_6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-7",
|
||||
"style": 7,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "galaxyfriends"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "neokosmo"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "conflict"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "redolentshape"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 7",
|
||||
"course_name": "在命运到来之际",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment900",
|
||||
"course_banner_7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-8",
|
||||
"style": 8,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "corruption"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "neowings"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "valhallazero"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "izana"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [],
|
||||
"flag_as_hidden_when_requirements_not_met": false,
|
||||
"dan_name": "Phase 8",
|
||||
"course_name": "趋于混沌的时空",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"course_banner_8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-9",
|
||||
"style": 9,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "felis"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "lightningscrew"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "seclusion"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": false,
|
||||
"difficulty": 2,
|
||||
"id": "livefastdieyoung"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-8",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 9",
|
||||
"course_name": "分崩离析的自我",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"core_generic_10",
|
||||
"course_banner_9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-10",
|
||||
"style": 10,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "cyaegha"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "divinelight"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "dantalion"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "aegleseeker"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-9",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 10",
|
||||
"course_name": "光与对立的洪流",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment1000",
|
||||
"core_generic_10",
|
||||
"course_banner_10"
|
||||
]
|
||||
},
|
||||
{
|
||||
"course_id": "4.0-dan-11",
|
||||
"style": 11,
|
||||
"songs": [
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "infinitestrife"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "grievouslady"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 2,
|
||||
"id": "fractureray"
|
||||
},
|
||||
{
|
||||
"flag_as_hidden": true,
|
||||
"difficulty": 3,
|
||||
"id": "tempestissimo"
|
||||
}
|
||||
],
|
||||
"gauge_requirement": "default",
|
||||
"requirements": [
|
||||
{
|
||||
"value": "4.0-dan-10",
|
||||
"type": "course"
|
||||
}
|
||||
],
|
||||
"flag_as_hidden_when_requirements_not_met": true,
|
||||
"dan_name": "Phase 11",
|
||||
"course_name": "创世之曙光",
|
||||
"can_start": true,
|
||||
"is_completed": false,
|
||||
"high_score": 0,
|
||||
"best_clear_type": -1,
|
||||
"rewards": [
|
||||
"fragment2000",
|
||||
"core_generic_20",
|
||||
"course_banner_11"
|
||||
]
|
||||
}
|
||||
]
|
||||
626
latest version/database/init/packs.json
Normal file
626
latest version/database/init/packs.json
Normal file
@@ -0,0 +1,626 @@
|
||||
[
|
||||
{
|
||||
"name": "core",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "core",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
},
|
||||
{
|
||||
"name": "shiawase",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "shiawase",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "character",
|
||||
"id": "kou",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "dynamix",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "dynamix",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "character",
|
||||
"id": "sapphire",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "mirai",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "mirai",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "character",
|
||||
"id": "lethe",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "yugamu",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "yugamu",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "lanota",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "lanota",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "nijuusei",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "nijuusei",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
},
|
||||
{
|
||||
"name": "rei",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "rei",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "tonesphere",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "tonesphere",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "groovecoaster",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "groovecoaster",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "zettai",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "zettai",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "chunithm",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "chunithm",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 300,
|
||||
"orig_price": 300
|
||||
},
|
||||
{
|
||||
"name": "prelude",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "prelude",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 400,
|
||||
"orig_price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "omatsuri",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "omatsuri",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "vs",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "vs",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "extend",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "extend",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 7,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"price": 0,
|
||||
"orig_price": 700,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000,
|
||||
"discount_reason": "extend"
|
||||
},
|
||||
{
|
||||
"name": "alice",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "alice",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "alice_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "alice_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "ongeki",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "ongeki",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "maimai",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "maimai",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "chunithm_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "chunithm_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
},
|
||||
{
|
||||
"name": "observer_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "observer_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "observer",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "observer",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "observer_append_2",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "observer_append_2",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "wacca",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "wacca",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "nijuusei_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "nijuusei_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
},
|
||||
{
|
||||
"name": "dividedheart",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "dividedheart",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "musedash",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "musedash",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "lanota_append_1",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "lanota_append_1",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 3,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
},
|
||||
{
|
||||
"name": "finale",
|
||||
"items": [
|
||||
{
|
||||
"type": "pack",
|
||||
"id": "finale",
|
||||
"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
|
||||
}
|
||||
]
|
||||
1256
latest version/database/init/singles.json
Normal file
1256
latest version/database/init/singles.json
Normal file
File diff suppressed because it is too large
Load Diff
312
latest version/database/init/tables.sql
Normal file
312
latest version/database/init/tables.sql
Normal file
@@ -0,0 +1,312 @@
|
||||
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
|
||||
);
|
||||
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
|
||||
);
|
||||
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;
|
||||
376
latest version/database/map/byd_bookmaker.json
Normal file
376
latest version/database/map/byd_bookmaker.json
Normal file
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "bookmaker2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-650,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 300,
|
||||
"character_affinity": [
|
||||
23,
|
||||
34,
|
||||
47
|
||||
],
|
||||
"affinity_multiplier": [
|
||||
1.3,
|
||||
5,
|
||||
3.8
|
||||
],
|
||||
"step_count": 31,
|
||||
"custom_bg": "",
|
||||
"curr_position": 3,
|
||||
"curr_capture": 35.81730523472207,
|
||||
"is_locked": false,
|
||||
"steps": [
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
],
|
||||
"restrict_ids": [
|
||||
"bookmaker",
|
||||
"viciousheroism",
|
||||
"battlenoone",
|
||||
"galaxyfriends",
|
||||
"rekkaresonance"
|
||||
],
|
||||
"restrict_type": "song_id"
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 11,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 12,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 13,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 14,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 15,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 16,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 17,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 18,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 19,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 20,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 21,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 22,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 23,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 24,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 25,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 26,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 27,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 28,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 29,
|
||||
"capture": 10,
|
||||
"items": [
|
||||
{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"map_id": "byd_bookmaker",
|
||||
"position": 30,
|
||||
"capture": 0,
|
||||
"items": [
|
||||
{
|
||||
"id": "bookmaker3",
|
||||
"type": "world_song"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"map_id": "byd_dement",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "dement2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-400,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [13, 1],
|
||||
"affinity_multiplier": [2.6, 1.6],
|
||||
"step_count": 4,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_dement",
|
||||
"position": 0,
|
||||
"capture": 100.2865329512894
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 1,
|
||||
"capture": 110.31518624641834,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 2,
|
||||
"capture": 39.39828080229226,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1200
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 3,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "dement3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_dement",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "dement2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-400,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [13, 1],
|
||||
"affinity_multiplier": [2.6, 1.6],
|
||||
"step_count": 4,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_dement",
|
||||
"position": 0,
|
||||
"capture": 100.2865329512894
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 1,
|
||||
"capture": 110.31518624641834,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 2,
|
||||
"capture": 39.39828080229226,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1200
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_dement",
|
||||
"position": 3,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "dement3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"map_id": "byd_fairytale",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "fairytale2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "400,650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 3,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 0,
|
||||
"capture": 100,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 65
|
||||
}, {
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 1,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 4
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 2,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "fairytale3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_fairytale",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "fairytale2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "400,650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 3,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 0,
|
||||
"capture": 100,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 65
|
||||
}, {
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 1,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 4
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_fairytale",
|
||||
"position": 2,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "fairytale3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"map_id": "byd_goodtek",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "goodtek2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "900,650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 6,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 0,
|
||||
"capture": 50
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 1,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 2,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 250
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 3,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 4,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 275
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 5,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "goodtek3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_goodtek",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "goodtek2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "900,650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 6,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 0,
|
||||
"capture": 50
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 1,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 2,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 250
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 3,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 4,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 275
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_goodtek",
|
||||
"position": 5,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "goodtek3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,91 +1,91 @@
|
||||
{
|
||||
"map_id": "byd_infinityheaven",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "infinityheaven2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "650,900",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 9,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 0,
|
||||
"capture": 25
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 1,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 2,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 3,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 4,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 5,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 6,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 7,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 8,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "infinityheaven3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_infinityheaven",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "infinityheaven2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "650,900",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 9,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 0,
|
||||
"capture": 25
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 1,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 2,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 3,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 4,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 5,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 6,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 7,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_infinityheaven",
|
||||
"position": 8,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "infinityheaven3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
78
latest version/database/map/byd_lostcivilization.json
Normal file
78
latest version/database/map/byd_lostcivilization.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"map_id": "byd_lostcivilization",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "lostcivilization2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-650,-900",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 250,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [1.9, 1.9],
|
||||
"step_count": 7,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 0,
|
||||
"capture": 85
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 1,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 2,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 3,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 4,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 5,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 525
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lostcivilization",
|
||||
"position": 6,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "lostcivilization3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
70
latest version/database/map/byd_lumia.json
Normal file
70
latest version/database/map/byd_lumia.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"map_id": "byd_lumia",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "lumia2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "650,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 150,
|
||||
"character_affinity": [0, 15, 14, 16],
|
||||
"affinity_multiplier": [2.4, 1.9, 2.4, 2.9],
|
||||
"step_count": 6,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_lumia",
|
||||
"position": 0,
|
||||
"capture": 10,
|
||||
"restrict_id": "core",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"map_id": "byd_lumia",
|
||||
"position": 1,
|
||||
"capture": 20,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 200
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lumia",
|
||||
"position": 2,
|
||||
"capture": 30,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lumia",
|
||||
"position": 3,
|
||||
"capture": 40,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 250
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lumia",
|
||||
"position": 4,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_lumia",
|
||||
"position": 5,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "lumia3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,61 +1,61 @@
|
||||
{
|
||||
"map_id": "byd_purgatorium",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "purgatorium2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-900,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [13, 1],
|
||||
"affinity_multiplier": [2.6, 1.6],
|
||||
"step_count": 5,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 0,
|
||||
"capture": 75
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 1,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 2,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 3,
|
||||
"capture": 75,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 4,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "purgatorium3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_purgatorium",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "purgatorium2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "-900,-650",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [13, 1],
|
||||
"affinity_multiplier": [2.6, 1.6],
|
||||
"step_count": 5,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 0,
|
||||
"capture": 75
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 1,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 2,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 3,
|
||||
"capture": 75,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_purgatorium",
|
||||
"position": 4,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "purgatorium3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
{
|
||||
"map_id": "byd_vexaria",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "vexaria2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "650,400",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 150,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 3,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 0,
|
||||
"capture": 140
|
||||
}, {
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 616
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 2,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "vexaria3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "byd_vexaria",
|
||||
"is_legacy": false,
|
||||
"chapter": 1001,
|
||||
"available_from": -1,
|
||||
"available_to": 9999999999999,
|
||||
"is_repeatable": false,
|
||||
"require_id": "vexaria2",
|
||||
"require_type": "chart_unlock",
|
||||
"coordinate": "650,400",
|
||||
"is_beyond": true,
|
||||
"stamina_cost": 3,
|
||||
"beyond_health": 150,
|
||||
"character_affinity": [0, 1],
|
||||
"affinity_multiplier": [2.5, 1.5],
|
||||
"step_count": 3,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 0,
|
||||
"capture": 140
|
||||
}, {
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 616
|
||||
}]
|
||||
}, {
|
||||
"map_id": "byd_vexaria",
|
||||
"position": 2,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "vexaria3",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,428 +1,428 @@
|
||||
{
|
||||
"map_id": "chapter3_conflict",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 3,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-360,0",
|
||||
"step_count": 85,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 1,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 1
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 1,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 1,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 1
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 20,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 10
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 2
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 2,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 2,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 2,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 30,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 15
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 30,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 3,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 3,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 3,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 40,
|
||||
"items": [{
|
||||
"id": "bookmaker",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 40,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 200
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 4,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 4,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}]
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 50,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 250
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 5,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 5,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"id": "darakunosono",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 60,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 60,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 300
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 6,
|
||||
"restrict_id": "darakunosono",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 6,
|
||||
"restrict_id": "darakunosono",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 3
|
||||
}]
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 70,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 40
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 70
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 70,
|
||||
"items": [{
|
||||
"id": "espebranch",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 70
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 70,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 80,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 80,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "chapter3_conflict",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 3,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-360,0",
|
||||
"step_count": 85,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 1,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 1
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 1,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 1,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 1
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 20,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 10
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 100
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 2
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 2,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 2,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 2,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 30,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 15
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 30
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 30,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 3,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 3,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 3,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 40,
|
||||
"items": [{
|
||||
"id": "bookmaker",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 40
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 40,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 200
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 4,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 4,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 2
|
||||
}]
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 50,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 50
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 50,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 250
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 5,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 5,
|
||||
"restrict_id": "bookmaker",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"id": "darakunosono",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 60,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 30
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 60
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 60,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 300
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 6,
|
||||
"restrict_id": "darakunosono",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 6,
|
||||
"restrict_id": "darakunosono",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 3
|
||||
}]
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 70,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 40
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 70
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 70,
|
||||
"items": [{
|
||||
"id": "espebranch",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 70
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 70,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 8,
|
||||
"restrict_id": "suomi",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 80,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 80
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 80,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 9,
|
||||
"restrict_id": "rugie",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 90
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,268 +1,268 @@
|
||||
{
|
||||
"map_id": "chapter3_conflict_2",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 3,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-240,-180",
|
||||
"step_count": 51,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"id": "nhelv",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 100,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_ambivalent",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 100,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 20,
|
||||
"restrict_id": "nhelv",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_ambivalent",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "chapter3_conflict_2",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 3,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-240,-180",
|
||||
"step_count": 51,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"id": "nhelv",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 100,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_ambivalent",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 100,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 20,
|
||||
"restrict_id": "nhelv",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_ambivalent",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,236 +1,236 @@
|
||||
{
|
||||
"map_id": "chapter4_ripples",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 4,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-280,-200",
|
||||
"step_count": 51,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 170
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 15,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 14,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 170
|
||||
}]
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 15,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 14,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"id": "grimheart",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 190
|
||||
}]
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 18,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 17,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 16,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 19,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 190
|
||||
}]
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 18,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 17,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 16,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 15,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "vector",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "chapter4_ripples",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 4,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-280,-200",
|
||||
"step_count": 51,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 170
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 15,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 14,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 170
|
||||
}]
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 15,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 14,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"id": "grimheart",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 190
|
||||
}]
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 18,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 17,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 16,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 19,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 190
|
||||
}]
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 18,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 17,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 16,
|
||||
"restrict_id": "grimheart",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 15,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "vector",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,199 +1,199 @@
|
||||
{
|
||||
"map_id": "chapter4_waves",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 4,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-480,-200",
|
||||
"step_count": 41,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"id": "revixy",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 28
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 31
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 35
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 40,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 400
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 35,
|
||||
"restrict_id": "revixy",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 31,
|
||||
"restrict_id": "revixy",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 28,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 25,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 28
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 31
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 35
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "supernova",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "chapter4_waves",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 4,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "fragment",
|
||||
"require_value": 200,
|
||||
"coordinate": "-480,-200",
|
||||
"step_count": 41,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 20,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 16,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 13,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 20
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 25,
|
||||
"items": [{
|
||||
"id": "revixy",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 28
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 31
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 35
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 40,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 400
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 35,
|
||||
"restrict_id": "revixy",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 31,
|
||||
"restrict_id": "revixy",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 28,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 25,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 26
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 28
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 31
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 35
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"id": "supernova",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,477 +1,477 @@
|
||||
{
|
||||
"map_id": "extra_originals",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 2,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"coordinate": "0,-200",
|
||||
"step_count": 101,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 2
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 6,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 11,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 12,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 16,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 16,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 12,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 11,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 3,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"id": "syro",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 8,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 13,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 14,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 18,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 18,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 14,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 13,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 8,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "blaster",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 10,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 11,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 15,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 16,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 19
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 20,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 21,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 90
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 22
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 23
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 23
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 22
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 21,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 90
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 20,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 19
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 16,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 15,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 11,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 91,
|
||||
"capture": 10,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 92,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 93,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 94,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 95,
|
||||
"capture": 8,
|
||||
"items": [{
|
||||
"id": "cyberneciacatharsis",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 96,
|
||||
"capture": 400
|
||||
}, {
|
||||
"position": 97,
|
||||
"capture": 2,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 6
|
||||
}, {
|
||||
"position": 98,
|
||||
"capture": 450,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 99,
|
||||
"capture": 1,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 6
|
||||
}, {
|
||||
"position": 100,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "extra_originals",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 2,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"coordinate": "0,-200",
|
||||
"step_count": 101,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 2
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 3
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 6,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 11,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 12,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 16,
|
||||
"restrict_id": "infinityheaven",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 17,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 16,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 12,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 11,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6,
|
||||
"restrict_id": "reinvent",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 4
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 3,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 4,
|
||||
"items": [{
|
||||
"id": "syro",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 8,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 13,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 14,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 18,
|
||||
"restrict_id": "dement",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 19,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 18,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 16
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 15
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 14,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 13,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 11
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 9,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 60
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 8,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 5,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "blaster",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 10,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 11,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 15,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 16,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 19
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 20,
|
||||
"restrict_id": "syro",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 21,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 90
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 22
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 23
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 23
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 22
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 21,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 90
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 20,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 19
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 18
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 17
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 16,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 80
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 15,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 14
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 13
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 12
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 11,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 70
|
||||
}]
|
||||
}, {
|
||||
"position": 91,
|
||||
"capture": 10,
|
||||
"restrict_id": "blaster",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 92,
|
||||
"capture": 9
|
||||
}, {
|
||||
"position": 93,
|
||||
"capture": 8
|
||||
}, {
|
||||
"position": 94,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 95,
|
||||
"capture": 8,
|
||||
"items": [{
|
||||
"id": "cyberneciacatharsis",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 96,
|
||||
"capture": 400
|
||||
}, {
|
||||
"position": 97,
|
||||
"capture": 2,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 6
|
||||
}, {
|
||||
"position": 98,
|
||||
"capture": 450,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 99,
|
||||
"capture": 1,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 6
|
||||
}, {
|
||||
"position": 100,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,407 +1,407 @@
|
||||
{
|
||||
"map_id": "hikari_art",
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 1,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"is_legacy": true,
|
||||
"stamina_cost": 1,
|
||||
"coordinate": "-270,150",
|
||||
"step_count": 91,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "babaroque",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"id": "shadesoflight",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 7,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 7,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"id": "kanagawa",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 100,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 150,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 150,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 200,
|
||||
"restrict_id": "ignotus",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "hikari_art",
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 1,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"is_legacy": true,
|
||||
"stamina_cost": 1,
|
||||
"coordinate": "-270,150",
|
||||
"step_count": 91,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "babaroque",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 6,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"id": "shadesoflight",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 7,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 7,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 7,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"id": "kanagawa",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 100,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 150,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 150,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 200,
|
||||
"restrict_id": "ignotus",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_hollow",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,407 +1,407 @@
|
||||
{
|
||||
"map_id": "tairitsu_tech",
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 1,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"is_legacy": true,
|
||||
"stamina_cost": 1,
|
||||
"coordinate": "270,-150",
|
||||
"step_count": 91,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "lucifer",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"id": "anokumene",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 7,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 7,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"id": "ignotus",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 100,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 150,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 150,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 200,
|
||||
"restrict_id": "kanagawa",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "tairitsu_tech",
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 1,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 1,
|
||||
"is_legacy": true,
|
||||
"stamina_cost": 1,
|
||||
"coordinate": "270,-150",
|
||||
"step_count": 91,
|
||||
"custom_bg": "",
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 5
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"id": "lucifer",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 12,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 13,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 14,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 15,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 16,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 17,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 18,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 19,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 20,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 21,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 22,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 23,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 24,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 25,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 26,
|
||||
"capture": 6
|
||||
}, {
|
||||
"position": 27,
|
||||
"capture": 6,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 28,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 29,
|
||||
"capture": 6,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 30,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"id": "anokumene",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 31,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 32,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 33,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 34,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 35,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 36,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 37,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 38,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 39,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 40,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 41,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 42,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 43,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 44,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 45,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 46,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 47,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 48,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 49,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 50,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 51,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 52,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 53,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 54,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 55,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 56,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 57,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 58,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 59,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 60,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 50
|
||||
}]
|
||||
}, {
|
||||
"position": 61,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 62,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 63,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 64,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 65,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 25
|
||||
}]
|
||||
}, {
|
||||
"position": 66,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 67,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 68,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 69,
|
||||
"capture": 7
|
||||
}, {
|
||||
"position": 70,
|
||||
"capture": 7,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 71,
|
||||
"capture": 7,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 72,
|
||||
"capture": 7,
|
||||
"restrict_id": "lucifer",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 73,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 74,
|
||||
"capture": 7,
|
||||
"restrict_id": "anokumene",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 75,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"id": "ignotus",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 76,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 77,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 78,
|
||||
"capture": 100
|
||||
}, {
|
||||
"position": 79,
|
||||
"capture": 100,
|
||||
"restrict_id": "babaroque",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 80,
|
||||
"capture": 100,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 81,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 82,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 83,
|
||||
"capture": 150
|
||||
}, {
|
||||
"position": 84,
|
||||
"capture": 150,
|
||||
"restrict_id": "shadesoflight",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 85,
|
||||
"capture": 150,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}, {
|
||||
"position": 86,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 87,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 88,
|
||||
"capture": 200
|
||||
}, {
|
||||
"position": 89,
|
||||
"capture": 200,
|
||||
"restrict_id": "kanagawa",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 90,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_desolate",
|
||||
"amount": 5
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,94 +1,94 @@
|
||||
{
|
||||
"map_id": "test",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": true,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "0,0",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 20,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10000,
|
||||
"step_type": ["plusstamina","speedlimit"],
|
||||
"plus_stamina_value": 2,
|
||||
"speed_limit_value": 5,
|
||||
"restrict_id": "fractureray",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 500
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "test",
|
||||
"is_legacy": false,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 200,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": true,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "0,0",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 2,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 20,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id"
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 20
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10000,
|
||||
"step_type": ["plusstamina","speedlimit"],
|
||||
"plus_stamina_value": 2,
|
||||
"speed_limit_value": 5,
|
||||
"restrict_id": "fractureray",
|
||||
"restrict_type": "song_id"
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 500
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -1,120 +1,120 @@
|
||||
{
|
||||
"map_id": "test2",
|
||||
"is_legacy": true,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": true,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "200,200",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 1,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"],
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 60,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 50
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
{
|
||||
"map_id": "test2",
|
||||
"is_legacy": true,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": true,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "200,200",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 1,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"],
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 60,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 50
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
}
|
||||
114
latest version/database/map/test3.json
Normal file
114
latest version/database/map/test3.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"map_id": "test3",
|
||||
"is_legacy": true,
|
||||
"is_beyond": false,
|
||||
"beyond_health": 100,
|
||||
"character_affinity": [],
|
||||
"affinity_multiplier": [],
|
||||
"chapter": 0,
|
||||
"available_from": -1,
|
||||
"available_to": 2106124800000,
|
||||
"is_repeatable": false,
|
||||
"require_id": "",
|
||||
"require_type": "",
|
||||
"require_value": 0,
|
||||
"coordinate": "300,200",
|
||||
"step_count": 12,
|
||||
"custom_bg": "",
|
||||
"stamina_cost": 1,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": false,
|
||||
"steps": [{
|
||||
"position": 0,
|
||||
"capture": 10
|
||||
}, {
|
||||
"position": 1,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1000
|
||||
}]
|
||||
}, {
|
||||
"position": 2,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"items": [{
|
||||
"type": "memory",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 3,
|
||||
"capture": 10,
|
||||
"restrict_id": "base",
|
||||
"restrict_type": "pack_id",
|
||||
"step_type": ["randomsong"],
|
||||
"items": [{
|
||||
"id": "supernova",
|
||||
"type": "world_song"
|
||||
}]
|
||||
}, {
|
||||
"position": 4,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_generic",
|
||||
"amount": 1
|
||||
}]
|
||||
}, {
|
||||
"position": 5,
|
||||
"capture": 10,
|
||||
"step_type": ["speedlimit"],
|
||||
"speed_limit_value": 60,
|
||||
"items": [{
|
||||
"id": "scenery_chap4",
|
||||
"type": "world_unlock"
|
||||
}]
|
||||
}, {
|
||||
"position": 6,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 1250
|
||||
}]
|
||||
}, {
|
||||
"position": 7,
|
||||
"capture": 10,
|
||||
"step_type": ["plusstamina"],
|
||||
"plus_stamina_value": 2,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 8,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}, {
|
||||
"position": 9,
|
||||
"capture": 10,
|
||||
"items": [{
|
||||
"type": "memory",
|
||||
"amount": 150
|
||||
}]
|
||||
}, {
|
||||
"position": 10,
|
||||
"capture": 10,
|
||||
"items": [{"id": "2", "type": "character"}]
|
||||
}, {
|
||||
"position": 11,
|
||||
"capture": 0,
|
||||
"items": [{
|
||||
"type": "core",
|
||||
"id": "core_crimson",
|
||||
"amount": 50
|
||||
}, {
|
||||
"type": "fragment",
|
||||
"amount": 125
|
||||
}]
|
||||
}]
|
||||
}
|
||||
22
latest version/database/songs/dement/3.aff
Normal file
22
latest version/database/songs/dement/3.aff
Normal file
@@ -0,0 +1,22 @@
|
||||
AudioOffset:408
|
||||
-
|
||||
timing(0,210.00,4.00);
|
||||
(1142,4);
|
||||
flick(1142,0.50,0.50,0.00,1.00);
|
||||
(1571,2);
|
||||
(2857,1);
|
||||
(3142,2);
|
||||
(3428,3);
|
||||
(3857,4);
|
||||
(4142,3);
|
||||
(4571,2);
|
||||
(5000,1);
|
||||
(5428,3);
|
||||
arc(5714,6857,0.00,0.50,s,1.00,1.00,0,none,false);
|
||||
arc(6857,8000,0.50,0.00,s,1.00,1.00,0,none,false);
|
||||
arc(8000,9142,0.00,0.50,s,1.00,1.00,0,none,false);
|
||||
arc(9142,10285,0.50,0.00,s,1.00,1.00,0,none,false);
|
||||
arc(10285,11428,1.00,0.50,s,1.00,1.00,1,none,false);
|
||||
arc(11428,12571,0.50,1.00,s,1.00,1.00,1,none,false);
|
||||
arc(12571,13714,1.00,0.50,s,1.00,1.00,1,none,false);
|
||||
arc(13714,14571,0.50,1.00,s,1.00,1.00,1,none,false);
|
||||
12
latest version/database/songs/songlist
Normal file
12
latest version/database/songs/songlist
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"songs": [
|
||||
{
|
||||
"id": "dement",
|
||||
"difficulties": [
|
||||
{
|
||||
"ratingClass": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
latest version/linkplay_server/__init__.py
Normal file
1
latest version/linkplay_server/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .main import link_play
|
||||
24
latest version/linkplay_server/aes.py
Normal file
24
latest version/linkplay_server/aes.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from os import urandom
|
||||
from cryptography.hazmat.primitives.ciphers import (
|
||||
Cipher, algorithms, modes
|
||||
)
|
||||
|
||||
|
||||
def encrypt(key, plaintext, associated_data):
|
||||
iv = urandom(12)
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, min_tag_length=12),
|
||||
).encryptor()
|
||||
encryptor.authenticate_additional_data(associated_data)
|
||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return (iv, ciphertext, encryptor.tag)
|
||||
|
||||
|
||||
def decrypt(key, associated_data, iv, ciphertext, tag):
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, tag, min_tag_length=12),
|
||||
).decryptor()
|
||||
decryptor.authenticate_additional_data(associated_data)
|
||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||
27
latest version/linkplay_server/config.py
Normal file
27
latest version/linkplay_server/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
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'
|
||||
'''
|
||||
--------------------------------------------------
|
||||
'''
|
||||
|
||||
TIME_LIMIT = 3600000
|
||||
|
||||
COMMAND_INTERVAL = 1000000
|
||||
|
||||
COUNTDOWM_TIME = 3999
|
||||
|
||||
PLAYER_PRE_TIMEOUT = 3000000
|
||||
PLAYER_TIMEOUT = 20000000
|
||||
|
||||
LINK_PLAY_UNLOCK_LENGTH = 512
|
||||
264
latest version/linkplay_server/main.py
Normal file
264
latest version/linkplay_server/main.py
Normal file
@@ -0,0 +1,264 @@
|
||||
import base64
|
||||
import logging
|
||||
import random
|
||||
import socketserver
|
||||
import threading
|
||||
import time
|
||||
from os import urandom
|
||||
|
||||
# import binascii
|
||||
from .aes import decrypt, encrypt
|
||||
from .config import Config
|
||||
from .udp_class import Player, Room, bi
|
||||
from .udp_parser import CommandParser
|
||||
|
||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||
link_play_data = {}
|
||||
room_id_dict = {} # 'room_id': Room
|
||||
room_code_dict = {} # 'room_code': Room
|
||||
player_dict = {} # 'player_id' : Player
|
||||
clean_timer = 0
|
||||
lock = threading.RLock()
|
||||
|
||||
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
level=logging.INFO)
|
||||
|
||||
|
||||
def random_room_code():
|
||||
# 随机生成房间号
|
||||
re = ''
|
||||
for _ in range(4):
|
||||
re += chr(random.randint(65, 90))
|
||||
for _ in range(2):
|
||||
re += str(random.randint(0, 9))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def unique_random(dataset, length=8, random_func=None):
|
||||
'''无重复随机,且默认非0'''
|
||||
if random_func is None:
|
||||
x = bi(urandom(length))
|
||||
while x in dataset or x == 0:
|
||||
x = bi(urandom(length))
|
||||
else:
|
||||
x = random_func()
|
||||
while x in dataset:
|
||||
x = random_func()
|
||||
return x
|
||||
|
||||
|
||||
def clear_player(token):
|
||||
# 清除玩家信息和token
|
||||
del player_dict[link_play_data[token]['player_id']]
|
||||
del link_play_data[token]
|
||||
|
||||
|
||||
def clear_room(room):
|
||||
# 清除房间信息
|
||||
room_id = room.room_id
|
||||
room_code = room.room_code
|
||||
del room_id_dict[room_id]
|
||||
del room_code_dict[room_code]
|
||||
del room
|
||||
|
||||
|
||||
def memory_clean(now):
|
||||
# 内存清理
|
||||
with lock:
|
||||
clean_room_list = []
|
||||
clean_player_list = []
|
||||
for token in link_play_data:
|
||||
room = link_play_data[token]['room']
|
||||
if now - room.timestamp >= Config.TIME_LIMIT:
|
||||
clean_room_list.append(room.room_id)
|
||||
|
||||
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
|
||||
clean_player_list.append(token)
|
||||
|
||||
for room_id in room_id_dict:
|
||||
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
|
||||
clean_room_list.append(room_id)
|
||||
|
||||
for room_id in clean_room_list:
|
||||
if room_id in room_id_dict:
|
||||
clear_room(room_id_dict[room_id])
|
||||
|
||||
for token in clean_player_list:
|
||||
clear_player(token)
|
||||
|
||||
|
||||
class UDP_handler(socketserver.BaseRequestHandler):
|
||||
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 int.from_bytes(token, byteorder='little') in link_play_data:
|
||||
user = link_play_data[bi(token)]
|
||||
else:
|
||||
return None
|
||||
|
||||
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return None
|
||||
# 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 = encrypt(user['key'], i, b'')
|
||||
# print(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')
|
||||
# print(message)
|
||||
data = message.split('|')
|
||||
if data[0] != Config.AUTHENTICATION:
|
||||
self.wfile.write(b'No authentication')
|
||||
logging.warning('TCP-%s-No authentication' %
|
||||
self.client_address[0])
|
||||
return None
|
||||
|
||||
global clean_timer
|
||||
now = round(time.time() * 1000)
|
||||
if now - clean_timer >= Config.TIME_LIMIT:
|
||||
logging.info('Start cleaning memory...')
|
||||
clean_timer = now
|
||||
memory_clean(now)
|
||||
|
||||
self.wfile.write(data_swap(data[1:]).encode('utf-8'))
|
||||
|
||||
|
||||
def data_swap(data: list) -> str:
|
||||
# data: list[str] = [command, ...]
|
||||
if data[0] == '1':
|
||||
# 开房
|
||||
# data = ['1', name, song_unlock, ]
|
||||
# song_unlock: base64 str
|
||||
name = data[1]
|
||||
song_unlock = base64.b64decode(data[2])
|
||||
|
||||
key = urandom(16)
|
||||
with lock:
|
||||
room_id = unique_random(room_id_dict)
|
||||
|
||||
room = Room()
|
||||
room.room_id = room_id
|
||||
room_id_dict[room_id] = room
|
||||
|
||||
player_id = unique_random(player_dict, 3)
|
||||
player = Player()
|
||||
player.player_id = player_id
|
||||
player.set_player_name(name)
|
||||
player_dict[player_id] = player
|
||||
|
||||
player.song_unlock = song_unlock
|
||||
room.song_unlock = song_unlock
|
||||
room.host_id = player_id
|
||||
room.players[0] = player
|
||||
room.player_num = 1
|
||||
|
||||
room_code = unique_random(
|
||||
room_code_dict, random_func=random_room_code)
|
||||
room.room_code = room_code
|
||||
room_code_dict[room_code] = room
|
||||
|
||||
token = room_id
|
||||
player.token = token
|
||||
|
||||
link_play_data[token] = {'key': key,
|
||||
'room': room,
|
||||
'player_index': 0,
|
||||
'player_id': player_id}
|
||||
|
||||
logging.info('TCP-Create room `%s` by player `%s`' % (room_code, name))
|
||||
return '|'.join([str(x) for x in (0, room_code, room_id, token, base64.b64encode(key).decode('utf-8'), player_id)])
|
||||
|
||||
elif data[0] == '2':
|
||||
# 入房
|
||||
# data = ['2', name, song_unlock, room_code]
|
||||
# song_unlock: base64 str
|
||||
room_code = data[3].upper()
|
||||
with lock:
|
||||
if room_code not in room_code_dict:
|
||||
# 房间号错误
|
||||
return '1202'
|
||||
room = room_code_dict[room_code]
|
||||
|
||||
if room.player_num == 4:
|
||||
# 满人
|
||||
return '1201'
|
||||
elif room.state != 2:
|
||||
# 无法加入
|
||||
return '1205'
|
||||
|
||||
name = data[1]
|
||||
song_unlock = base64.b64decode(data[2])
|
||||
|
||||
key = urandom(16)
|
||||
with lock:
|
||||
token = unique_random(link_play_data)
|
||||
player_id = unique_random(player_dict, 3)
|
||||
|
||||
player = Player()
|
||||
player.player_id = player_id
|
||||
player.set_player_name(name)
|
||||
player.token = token
|
||||
player_dict[player_id] = player
|
||||
player.song_unlock = song_unlock
|
||||
room.update_song_unlock()
|
||||
for i in range(4):
|
||||
if room.players[i].player_id == 0:
|
||||
room.players[i] = player
|
||||
player_index = i
|
||||
break
|
||||
link_play_data[token] = {'key': key,
|
||||
'room': room,
|
||||
'player_index': player_index,
|
||||
'player_id': player_id}
|
||||
|
||||
logging.info('TCP-Player `%s` joins room `%s`' % (name, room_code))
|
||||
return '|'.join([str(x) for x in (0, room_code, room.room_id, token, base64.b64encode(key).decode('utf-8'), player_id, base64.b64encode(room.song_unlock).decode('utf-8'))])
|
||||
|
||||
elif data[0] == '3':
|
||||
# 房间信息更新
|
||||
# data = ['3', token]
|
||||
token = int(data[1])
|
||||
with lock:
|
||||
if token in link_play_data:
|
||||
r = link_play_data[token]
|
||||
logging.info('TCP-Room `%s` info update' % room_code)
|
||||
return '|'.join([str(x) for x in (0, r['room'].room_code, r['room'].room_id, base64.b64encode(r['key']).decode('utf-8'), r['room'].players[r['player_index']].player_id, base64.b64encode(r['room'].song_unlock).decode('utf-8'))])
|
||||
else:
|
||||
return '108'
|
||||
|
||||
|
||||
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):
|
||||
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]
|
||||
180
latest version/linkplay_server/udp_class.py
Normal file
180
latest version/linkplay_server/udp_class.py
Normal file
@@ -0,0 +1,180 @@
|
||||
from .config import Config
|
||||
|
||||
|
||||
def b(value, length=1):
|
||||
return value.to_bytes(length=length, byteorder='little')
|
||||
|
||||
|
||||
def bi(value):
|
||||
return int.from_bytes(value, byteorder='little')
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self) -> None:
|
||||
self.player_id = 0
|
||||
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
||||
self.token = 0
|
||||
|
||||
self.character_id = 0xff
|
||||
self.last_character_id = 0xff
|
||||
self.is_uncapped = 0
|
||||
|
||||
self.difficulty = 0xff
|
||||
self.last_difficulty = 0xff
|
||||
self.score = 0
|
||||
self.last_score = 0
|
||||
self.timer = 0
|
||||
self.last_timer = 0
|
||||
self.cleartype = 0
|
||||
self.last_cleartype = 0
|
||||
self.best_score_flag = 0
|
||||
self.best_player_flag = 0
|
||||
self.finish_flag = 0
|
||||
|
||||
self.player_state = 1
|
||||
self.download_percent = 0
|
||||
self.online = 0
|
||||
|
||||
self.last_timestamp = 0
|
||||
self.extra_command_queue = []
|
||||
|
||||
self.song_unlock: bytes = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.start_command_num = 0
|
||||
|
||||
def set_player_name(self, player_name: str):
|
||||
self.player_name = player_name.encode('ascii')
|
||||
if len(self.player_name) > 16:
|
||||
self.player_name = self.player_name[:16]
|
||||
else:
|
||||
self.player_name += b'\x00' * (16 - len(self.player_name))
|
||||
|
||||
|
||||
class Room:
|
||||
def __init__(self) -> None:
|
||||
self.room_id = 0
|
||||
self.room_code = 'AAAA00'
|
||||
|
||||
self.countdown = 0xffffffff
|
||||
self.timestamp = 0
|
||||
self.state = 0
|
||||
self.song_idx = 0xffff
|
||||
self.last_song_idx = 0xffff
|
||||
|
||||
self.song_unlock = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.host_id = 0
|
||||
self.players = [Player(), Player(), Player(), Player()]
|
||||
self.player_num = 0
|
||||
|
||||
self.interval = 1000
|
||||
self.times = 100
|
||||
|
||||
self.round_switch = 0
|
||||
|
||||
self.command_queue = []
|
||||
self.command_queue_length = 0
|
||||
|
||||
def get_players_info(self):
|
||||
# 获取所有玩家信息
|
||||
re = b''
|
||||
for i in self.players:
|
||||
re += b(i.player_id, 8) + b(i.character_id) + b(i.is_uncapped) + b(i.difficulty) + b(i.score, 4) + \
|
||||
b(i.timer, 4) + b(i.cleartype) + b(i.player_state) + \
|
||||
b(i.download_percent) + b(i.online) + b'\x00' + i.player_name
|
||||
return re
|
||||
|
||||
def get_player_last_score(self):
|
||||
# 获取上次曲目玩家分数,返回bytes
|
||||
if self.last_song_idx == 0xffff:
|
||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' * 4
|
||||
re = b''
|
||||
|
||||
for i in range(4):
|
||||
player = self.players[i]
|
||||
|
||||
if player.player_id != 0:
|
||||
re += b(player.last_character_id) + b(player.last_difficulty) + b(player.last_score, 4) + b(
|
||||
player.last_cleartype) + b(player.best_score_flag) + b(player.best_player_flag)
|
||||
else:
|
||||
re += b'\xff\xff\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
return re
|
||||
|
||||
def make_round(self):
|
||||
# 轮换房主
|
||||
for i in range(4):
|
||||
if self.players[i].player_id == self.host_id:
|
||||
for j in range(1, 4):
|
||||
if self.players[(i + j) % 4].player_id != 0:
|
||||
self.host_id = self.players[(i + j) % 4].player_id
|
||||
break
|
||||
break
|
||||
|
||||
def delete_player(self, player_index: int):
|
||||
# 删除某个玩家
|
||||
self.player_num -= 1
|
||||
if self.players[player_index].player_id == self.host_id:
|
||||
self.make_round()
|
||||
|
||||
self.players[player_index].online = 0
|
||||
self.players[player_index] = Player()
|
||||
self.update_song_unlock()
|
||||
|
||||
def update_song_unlock(self):
|
||||
# 更新房间可用歌曲
|
||||
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
for i in self.players:
|
||||
if i.player_id != 0:
|
||||
r &= bi(i.song_unlock)
|
||||
|
||||
self.song_unlock = b(r, Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
|
||||
def is_ready(self, old_state: int, player_state: int):
|
||||
# 是否全部准备就绪
|
||||
if self.state == old_state:
|
||||
for i in self.players:
|
||||
if i.player_id != 0 and (i.player_state != player_state or i.online == 0):
|
||||
return False
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_finish(self):
|
||||
# 是否全部进入结算
|
||||
if self.state == 8:
|
||||
return False
|
||||
|
||||
for i in self.players:
|
||||
if i.player_id != 0 and (i.finish_flag == 0 or i.online == 0):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def make_finish(self):
|
||||
# 结算
|
||||
self.state = 8
|
||||
self.last_song_idx = self.song_idx
|
||||
|
||||
max_score = 0
|
||||
max_score_i = []
|
||||
for i in range(4):
|
||||
player = self.players[i]
|
||||
if player.player_id != 0:
|
||||
player.finish_flag = 0
|
||||
player.last_timer = player.timer
|
||||
player.last_score = player.score
|
||||
player.last_cleartype = player.cleartype
|
||||
player.last_character_id = player.character_id
|
||||
player.last_difficulty = player.difficulty
|
||||
player.best_player_flag = 0
|
||||
|
||||
if player.last_score > max_score:
|
||||
max_score = player.last_score
|
||||
max_score_i = [i]
|
||||
elif player.last_score == max_score:
|
||||
max_score_i.append(i)
|
||||
|
||||
for i in max_score_i:
|
||||
self.players[i].best_player_flag = 1
|
||||
335
latest version/linkplay_server/udp_parser.py
Normal file
335
latest version/linkplay_server/udp_parser.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import time
|
||||
|
||||
from .udp_class import Room, bi
|
||||
from .config import Config
|
||||
from .udp_sender import CommandSender
|
||||
|
||||
|
||||
class CommandParser:
|
||||
def __init__(self, room: Room, player_index: int = 0) -> None:
|
||||
self.room = room
|
||||
self.player_index = player_index
|
||||
|
||||
def get_commands(self, command):
|
||||
self.command = command
|
||||
l = {b'\x06\x16\x01': self.command_01,
|
||||
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 = []
|
||||
|
||||
flag_13 = False
|
||||
for i in range(max(bi(self.command[12:16]), self.room.players[self.player_index].start_command_num), self.room.command_queue_length):
|
||||
if self.room.command_queue[i][:3] == b'\x06\x16\x13':
|
||||
if flag_13:
|
||||
break
|
||||
flag_13 = True
|
||||
re.append(self.room.command_queue[i])
|
||||
|
||||
if self.room.players[self.player_index].extra_command_queue:
|
||||
re += self.room.players[self.player_index].extra_command_queue
|
||||
self.room.players[self.player_index].extra_command_queue = []
|
||||
|
||||
if r:
|
||||
re += r
|
||||
|
||||
return re
|
||||
|
||||
def command_01(self):
|
||||
# 给房主
|
||||
player_id = bi(self.command[24:32])
|
||||
for i in self.room.players:
|
||||
if i.player_id == player_id and i.online == 1:
|
||||
self.room.host_id = player_id
|
||||
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_10())
|
||||
|
||||
return None
|
||||
|
||||
def command_02(self):
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
song_idx = bi(self.command[24:26])
|
||||
|
||||
flag = 2
|
||||
if self.room.state == 2:
|
||||
flag = 0
|
||||
self.room.state = 3
|
||||
self.room.song_idx = song_idx
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_11())
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_13())
|
||||
|
||||
return [x.command_0d(flag)]
|
||||
|
||||
def command_03(self):
|
||||
# 尝试进入结算
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
player = self.room.players[self.player_index]
|
||||
player.score = bi(self.command[24:28])
|
||||
player.cleartype = self.command[28]
|
||||
player.difficulty = self.command[29]
|
||||
player.best_score_flag = self.command[30]
|
||||
player.finish_flag = 1
|
||||
player.last_timestamp -= Config.COMMAND_INTERVAL
|
||||
self.room.last_song_idx = self.room.song_idx
|
||||
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_12(self.player_index))
|
||||
|
||||
if self.room.is_finish():
|
||||
self.room.make_finish()
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_13())
|
||||
|
||||
return None
|
||||
|
||||
def command_04(self):
|
||||
# 踢人
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
player_id = bi(self.command[24:32])
|
||||
flag = 2
|
||||
if self.room.players[self.player_index].player_id == self.room.host_id and player_id != self.room.host_id:
|
||||
for i in range(4):
|
||||
if self.room.players[i].player_id == player_id:
|
||||
flag = 1
|
||||
self.room.delete_player(i)
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_12(i))
|
||||
self.room.update_song_unlock()
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_14())
|
||||
break
|
||||
|
||||
return [x.command_0d(flag)]
|
||||
|
||||
def command_05(self):
|
||||
pass
|
||||
|
||||
def command_06(self):
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_13())
|
||||
return None
|
||||
|
||||
def command_07(self):
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
self.room.players[self.player_index].song_unlock = self.command[24:536]
|
||||
self.room.update_song_unlock()
|
||||
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_14())
|
||||
return None
|
||||
|
||||
def command_08(self):
|
||||
self.room.round_switch = bi(self.command[24:25])
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_13())
|
||||
|
||||
return None
|
||||
|
||||
def command_09(self):
|
||||
re = []
|
||||
x = CommandSender(self.room)
|
||||
x.random_code = self.command[16:24]
|
||||
player = self.room.players[self.player_index]
|
||||
|
||||
if bi(self.command[12:16]) == 0:
|
||||
player.online = 1
|
||||
self.room.state = 1
|
||||
self.room.update_song_unlock()
|
||||
player.start_command_num = self.room.command_queue_length
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_15())
|
||||
else:
|
||||
if x.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
||||
re.append(x.command_0c())
|
||||
player.last_timestamp = x.timestamp
|
||||
|
||||
flag_13 = False
|
||||
# 离线判断
|
||||
for i in range(4):
|
||||
if i != self.player_index:
|
||||
t = self.room.players[i]
|
||||
if t.player_id != 0:
|
||||
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_12 = False
|
||||
|
||||
if player.online == 0:
|
||||
flag_12 = True
|
||||
player.online = 1
|
||||
|
||||
if self.room.is_ready(1, 1):
|
||||
flag_13 = True
|
||||
self.room.state = 2
|
||||
|
||||
if player.player_state != self.command[32]:
|
||||
flag_12 = True
|
||||
player.player_state = self.command[32]
|
||||
|
||||
if player.difficulty != self.command[33] and player.player_state != 5 and player.player_state != 6 and player.player_state != 7 and player.player_state != 8:
|
||||
flag_12 = True
|
||||
player.difficulty = self.command[33]
|
||||
|
||||
if player.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
||||
flag_12 = True
|
||||
player.cleartype = self.command[34]
|
||||
|
||||
if player.download_percent != self.command[35]:
|
||||
flag_12 = True
|
||||
player.download_percent = self.command[35]
|
||||
|
||||
if player.character_id != self.command[36]:
|
||||
flag_12 = True
|
||||
player.character_id = self.command[36]
|
||||
|
||||
if player.is_uncapped != self.command[37]:
|
||||
flag_12 = True
|
||||
player.is_uncapped = self.command[37]
|
||||
|
||||
if self.room.state == 3 and player.score != bi(self.command[24:28]):
|
||||
flag_12 = True
|
||||
player.score = bi(self.command[24:28])
|
||||
|
||||
if self.room.is_ready(3, 4):
|
||||
flag_13 = True
|
||||
self.room.countdown = Config.COUNTDOWM_TIME
|
||||
self.room.timestamp = round(time.time() * 1000)
|
||||
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:
|
||||
timestamp = round(time.time() * 1000)
|
||||
self.room.countdown -= timestamp - self.room.timestamp
|
||||
self.room.timestamp = timestamp
|
||||
if self.room.state == 4 and self.room.countdown <= 0:
|
||||
# 此处不清楚
|
||||
self.room.state = 5
|
||||
self.room.countdown = 5999
|
||||
flag_11 = True
|
||||
flag_13 = True
|
||||
|
||||
if self.room.state == 5 and self.room.is_ready(5, 6):
|
||||
self.room.state = 6
|
||||
flag_13 = True
|
||||
|
||||
if self.room.state == 5 and self.room.is_ready(5, 7):
|
||||
self.room.state = 7
|
||||
self.room.countdown = 0xffffffff
|
||||
flag_13 = True
|
||||
|
||||
if self.room.state == 5 and self.room.countdown <= 0:
|
||||
print('我怎么知道这是啥')
|
||||
|
||||
if self.room.state == 6 and self.room.countdown <= 0:
|
||||
# 此处不清楚
|
||||
self.room.state = 7
|
||||
self.room.countdown = 0xffffffff
|
||||
flag_13 = True
|
||||
|
||||
if self.room.countdown <= 0:
|
||||
self.room.countdown = 0
|
||||
|
||||
if self.room.state == 7 or self.room.state == 8:
|
||||
if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0:
|
||||
player.last_timer = player.timer
|
||||
player.last_score = player.score
|
||||
player.timer = bi(self.command[28:32])
|
||||
player.score = bi(self.command[24:28])
|
||||
|
||||
if player.timer != 0 or self.room.state != 8:
|
||||
for i in self.room.players:
|
||||
i.extra_command_queue.append(
|
||||
x.command_0e(self.player_index))
|
||||
|
||||
if self.room.is_ready(8, 1):
|
||||
flag_13 = True
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
|
||||
for i in self.room.players:
|
||||
i.timer = 0
|
||||
i.score = 0
|
||||
|
||||
if self.room.is_finish():
|
||||
# 有人退房导致的结算
|
||||
self.room.make_finish()
|
||||
flag_13 = True
|
||||
|
||||
if flag_11:
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_11())
|
||||
if flag_12:
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_12(self.player_index))
|
||||
if flag_13:
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_13())
|
||||
|
||||
return re
|
||||
|
||||
def command_0a(self):
|
||||
# 退出房间
|
||||
self.room.delete_player(self.player_index)
|
||||
|
||||
x = CommandSender(self.room)
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_12(self.player_index))
|
||||
|
||||
if self.room.state == 3 or self.room.state == 2:
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
# self.room.command_queue_length += 1
|
||||
# self.room.command_queue.append(x.command_11())
|
||||
self.room.command_queue_length += 1
|
||||
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):
|
||||
# 推荐歌曲
|
||||
song_idx = bi(self.command[16:18])
|
||||
x = CommandSender(self.room)
|
||||
for i in range(4):
|
||||
if self.player_index != i and self.room.players[i].online == 1:
|
||||
self.room.players[i].extra_command_queue.append(
|
||||
x.command_0f(self.player_index, song_idx))
|
||||
|
||||
return None
|
||||
46
latest version/linkplay_server/udp_sender.py
Normal file
46
latest version/linkplay_server/udp_sender.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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'
|
||||
0
latest version/log/error.log
Normal file
0
latest version/log/error.log
Normal file
217
latest version/main.py
Normal file
217
latest version/main.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import os
|
||||
from importlib import import_module
|
||||
|
||||
from core.config_manager import Config, ConfigManager
|
||||
|
||||
if os.path.exists('config.py') or os.path.exists('config'):
|
||||
# 导入用户自定义配置
|
||||
ConfigManager.load(import_module('config').Config)
|
||||
|
||||
if Config.DEPLOY_MODE == 'gevent':
|
||||
# 异步
|
||||
from gevent import monkey
|
||||
monkey.patch_all()
|
||||
|
||||
|
||||
import sys
|
||||
from logging.config import dictConfig
|
||||
from multiprocessing import Process, set_start_method
|
||||
from traceback import format_exc
|
||||
|
||||
from flask import Flask, make_response, request, send_from_directory
|
||||
|
||||
import api
|
||||
import server
|
||||
import web.index
|
||||
import web.login
|
||||
from core.constant import Constant
|
||||
from core.download import (UserDownload, initialize_songfile)
|
||||
from core.error import ArcError, NoAccess, RateLimit
|
||||
from core.init import FileChecker
|
||||
from core.sql import Connect
|
||||
from server.func import error_return
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
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]) # 更改工作路径,以便于愉快使用相对路径
|
||||
|
||||
|
||||
app.config.from_mapping(SECRET_KEY=Config.SECRET_KEY)
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.register_blueprint(web.login.bp)
|
||||
app.register_blueprint(web.index.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
app.register_blueprint(server.bp)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
return "Hello World!"
|
||||
|
||||
|
||||
@app.route('/favicon.ico', methods=['GET']) # 图标
|
||||
def favicon():
|
||||
# Pixiv ID: 82374369
|
||||
# 我觉得这张图虽然并不是那么精细,但很有感觉,色彩的强烈对比下给人带来一种惊艳
|
||||
# 然后在压缩之下什么也看不清了:(
|
||||
|
||||
return app.send_static_file('favicon.ico')
|
||||
|
||||
|
||||
@app.route('/download/<path:file_path>', methods=['GET']) # 下载
|
||||
def download(file_path):
|
||||
with Connect(in_memory=True) as c:
|
||||
try:
|
||||
x = UserDownload(c)
|
||||
x.token = request.args.get('t')
|
||||
x.song_id, x.file_name = file_path.split('/', 1)
|
||||
x.select_for_check()
|
||||
if x.is_limited:
|
||||
raise RateLimit('You have reached the download limit.', 903)
|
||||
if not x.is_valid:
|
||||
raise NoAccess('Expired token.')
|
||||
x.download_hit()
|
||||
if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT:
|
||||
# nginx X-Accel-Redirect
|
||||
response = make_response()
|
||||
response.headers['Content-Type'] = 'application/octet-stream'
|
||||
response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path
|
||||
return response
|
||||
return send_from_directory(Constant.SONG_FILE_FOLDER_PATH, file_path, as_attachment=True, conditional=True)
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
app.logger.warning(format_exc())
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
app.run(Config.HOST, Config.PORT, ssl_context=(
|
||||
Config.SSL_CERT, Config.SSL_KEY))
|
||||
else:
|
||||
app.run(Config.HOST, Config.PORT)
|
||||
|
||||
|
||||
def main():
|
||||
log_dict = {
|
||||
'version': 1,
|
||||
'root': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['wsgi', 'error_file']
|
||||
},
|
||||
'handlers': {
|
||||
'wsgi': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'stream': 'ext://flask.logging.wsgi_errors_stream',
|
||||
'formatter': 'default'
|
||||
},
|
||||
"error_file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "ERROR",
|
||||
"formatter": "default",
|
||||
"filename": "./log/error.log"
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
|
||||
}
|
||||
}
|
||||
}
|
||||
if Config.ALLOW_INFO_LOG:
|
||||
log_dict['root']['handlers'].append('info_file')
|
||||
log_dict['handlers']['info_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "INFO",
|
||||
"formatter": "default",
|
||||
"filename": "./log/info.log"
|
||||
}
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
log_dict['root']['handlers'].append('warning_file')
|
||||
log_dict['handlers']['warning_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
"backupCount": 1,
|
||||
"encoding": "utf-8",
|
||||
"level": "WARNING",
|
||||
"formatter": "default",
|
||||
"filename": "./log/warning.log"
|
||||
}
|
||||
|
||||
dictConfig(log_dict)
|
||||
|
||||
if not FileChecker(app).check_before_run():
|
||||
app.logger.error('Something wrong. The server will not run.')
|
||||
input('Press ENTER key to exit.')
|
||||
sys.exit()
|
||||
|
||||
app.logger.info("Start to initialize song data...")
|
||||
try:
|
||||
initialize_songfile()
|
||||
app.logger.info('Complete!')
|
||||
except:
|
||||
app.logger.warning('Initialization error!')
|
||||
|
||||
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
||||
from linkplay_server import link_play
|
||||
process = [Process(target=link_play, args=(
|
||||
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
||||
[p.start() for p in process]
|
||||
app.logger.info("Link Play UDP server is running on " +
|
||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_UDP_PORT) + " ...")
|
||||
app.logger.info("Link Play TCP server is running on " +
|
||||
Config.LINKPLAY_HOST + ':' + str(Config.LINKPLAY_TCP_PORT) + " ...")
|
||||
tcp_server_run()
|
||||
[p.join() for p in process]
|
||||
else:
|
||||
tcp_server_run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
set_start_method("spawn")
|
||||
main()
|
||||
|
||||
|
||||
# Made By Lost 2020.9.11
|
||||
3
latest version/requirements.txt
Normal file
3
latest version/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask>=2.0.2
|
||||
cryptography>=3.0.0
|
||||
limits>=2.7.0
|
||||
3
latest version/run.bat
Normal file
3
latest version/run.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
cd /d %~dp0
|
||||
:: Develop server
|
||||
python -B main.py
|
||||
9
latest version/run_linkplay_server.py
Normal file
9
latest version/run_linkplay_server.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import linkplay_server
|
||||
|
||||
os.chdir(sys.path[0])
|
||||
|
||||
if __name__ == '__main__':
|
||||
linkplay_server.link_play()
|
||||
17
latest version/server/__init__.py
Normal file
17
latest version/server/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from core.config_manager import Config
|
||||
from flask import Blueprint
|
||||
|
||||
from . import (auth, course, friend, multiplayer, others, present, purchase,
|
||||
score, user, world)
|
||||
|
||||
bp = Blueprint('server', __name__, url_prefix=Config.GAME_API_PREFIX)
|
||||
bp.register_blueprint(user.bp)
|
||||
bp.register_blueprint(auth.bp)
|
||||
bp.register_blueprint(friend.bp)
|
||||
bp.register_blueprint(score.bp)
|
||||
bp.register_blueprint(world.bp)
|
||||
bp.register_blueprint(purchase.bp)
|
||||
bp.register_blueprint(present.bp)
|
||||
bp.register_blueprint(others.bp)
|
||||
bp.register_blueprint(multiplayer.bp)
|
||||
bp.register_blueprint(course.bp)
|
||||
56
latest version/server/arcscore.py
Normal file
56
latest version/server/arcscore.py
Normal file
@@ -0,0 +1,56 @@
|
||||
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
|
||||
63
latest version/server/auth.py
Normal file
63
latest version/server/auth.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import base64
|
||||
from functools import wraps
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.sql import Connect
|
||||
from core.user import UserAuth, UserLogin
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from .func import arc_try, error_return
|
||||
|
||||
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
|
||||
@bp.route('/login', methods=['POST']) # 登录接口
|
||||
@arc_try
|
||||
def login():
|
||||
headers = request.headers
|
||||
if 'AppVersion' in headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Wrong app version.', 1203)
|
||||
|
||||
request.form['grant_type']
|
||||
with Connect() as c:
|
||||
id_pwd = headers['Authorization']
|
||||
id_pwd = base64.b64decode(id_pwd[6:]).decode()
|
||||
name, password = id_pwd.split(':', 1)
|
||||
if 'DeviceId' in headers:
|
||||
device_id = headers['DeviceId']
|
||||
else:
|
||||
device_id = 'low_version'
|
||||
|
||||
user = UserLogin(c)
|
||||
user.login(name, password, device_id, request.remote_addr)
|
||||
|
||||
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
||||
|
||||
|
||||
def auth_required(request):
|
||||
# arcaea登录验证,写成了修饰器
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
headers = request.headers
|
||||
|
||||
if 'AppVersion' in headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
return error_return(NoAccess('Wrong app version.', 1203))
|
||||
|
||||
with Connect() as c:
|
||||
try:
|
||||
user = UserAuth(c)
|
||||
user.token = headers['Authorization'][7:]
|
||||
user_id = user.token_get_id()
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return view(user_id, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
29
latest version/server/course.py
Normal file
29
latest version/server/course.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from core.constant import Constant
|
||||
from core.course import UserCourseList
|
||||
from core.item import ItemCore
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('course', __name__, url_prefix='/course')
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def course_me(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
core = ItemCore(c)
|
||||
core.item_id = 'core_course_skip_purchase'
|
||||
core.select(user)
|
||||
x = UserCourseList(c, user)
|
||||
x.select_all()
|
||||
return success_return({
|
||||
'courses': x.to_dict_list(),
|
||||
"stamina_cost": Constant.COURSE_STAMINA_COST,
|
||||
"course_skip_purchase_ticket": core.amount
|
||||
})
|
||||
43
latest version/server/friend.py
Normal file
43
latest version/server/friend.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline, code_get_id
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('friend', __name__, url_prefix='/friend')
|
||||
|
||||
|
||||
@bp.route('/me/add', methods=['POST']) # 加好友
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def add_friend(user_id):
|
||||
with Connect() as c:
|
||||
friend_code = request.form['friend_code']
|
||||
friend_id = code_get_id(c, friend_code)
|
||||
user = UserOnline(c, user_id)
|
||||
user.add_friend(friend_id)
|
||||
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": user.friends
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/delete', methods=['POST']) # 删好友
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def delete_friend(user_id):
|
||||
with Connect() as c:
|
||||
friend_id = int(request.form['friend_id'])
|
||||
user = UserOnline(c, user_id)
|
||||
user.delete_friend(friend_id)
|
||||
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": user.friends
|
||||
})
|
||||
88
latest version/server/func.py
Normal file
88
latest version/server/func.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from functools import wraps
|
||||
from traceback import format_exc
|
||||
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError
|
||||
from flask import current_app, jsonify
|
||||
|
||||
default_error = ArcError('Unknown Error', status=500)
|
||||
|
||||
|
||||
def error_return(e: ArcError = default_error): # 错误返回
|
||||
# -7 处理交易时发生了错误
|
||||
# -5 所有的曲目都已经下载完毕
|
||||
# -4 您的账号已在别处登录
|
||||
# -3 无法连接至服务器
|
||||
# 2 Arcaea服务器正在维护
|
||||
# 9 新版本请等待几分钟
|
||||
# 100 无法在此ip地址下登录游戏
|
||||
# 101 用户名占用
|
||||
# 102 电子邮箱已注册
|
||||
# 103 已有一个账号由此设备创建
|
||||
# 104 用户名密码错误
|
||||
# 105 24小时内登入两台设备
|
||||
# 106 121 账户冻结
|
||||
# 107 你没有足够的体力
|
||||
# 113 活动已结束
|
||||
# 114 该活动已结束,您的成绩不会提交
|
||||
# 115 请输入有效的电子邮箱地址
|
||||
# 120 封号警告
|
||||
# 122 账户暂时冻结
|
||||
# 123 账户被限制
|
||||
# 124 你今天不能再使用这个IP地址创建新的账号
|
||||
# 150 非常抱歉您已被限制使用此功能
|
||||
# 151 目前无法使用此功能
|
||||
# 401 用户不存在
|
||||
# 403 无法连接至服务器
|
||||
# 501 502 -6 此物品目前无法获取
|
||||
# 504 无效的序列码
|
||||
# 505 此序列码已被使用
|
||||
# 506 你已拥有了此物品
|
||||
# 601 好友列表已满
|
||||
# 602 此用户已是好友
|
||||
# 604 你不能加自己为好友
|
||||
# 903 下载量超过了限制,请24小时后重试
|
||||
# 905 请在再次使用此功能前等待24小时
|
||||
# 1001 设备数量达到上限
|
||||
# 1002 此设备已使用过此功能
|
||||
# 1201 房间已满
|
||||
# 1202 房间号码无效
|
||||
# 1203 请将Arcaea更新至最新版本
|
||||
# 1205 此房间目前无法加入
|
||||
# 9801 下载歌曲时发生问题,请再试一次
|
||||
# 9802 保存歌曲时发生问题,请检查设备空间容量
|
||||
# 9803 下载已取消
|
||||
# 9905 没有在云端发现任何数据
|
||||
# 9907 更新数据时发生了问题
|
||||
# 9908 服务器只支持最新的版本,请更新Arcaea
|
||||
# 其它 发生未知错误
|
||||
r = {"success": False, "error_code": e.error_code}
|
||||
if e.extra_data:
|
||||
r['extra'] = e.extra_data
|
||||
|
||||
return jsonify(r), e.status
|
||||
|
||||
|
||||
def success_return(value=None):
|
||||
r = {"success": True}
|
||||
if value is not None:
|
||||
r['value'] = value
|
||||
return jsonify(r)
|
||||
|
||||
|
||||
def arc_try(view):
|
||||
'''替代try/except,记录`ArcError`为warning'''
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
try:
|
||||
data = view(*args, **kwargs)
|
||||
if data is None:
|
||||
return error_return()
|
||||
else:
|
||||
return data
|
||||
except ArcError as e:
|
||||
if Config.ALLOW_WARNING_LOG:
|
||||
current_app.logger.warning(format_exc())
|
||||
return error_return(e)
|
||||
|
||||
return wrapped_view
|
||||
91
latest version/server/init.py
Normal file
91
latest version/server/init.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
from shutil import copy, copy2
|
||||
|
||||
from core.sql import Connect
|
||||
from database.database_initialize import ARCAEA_SERVER_VERSION, main
|
||||
from web.system import update_database
|
||||
|
||||
|
||||
def try_rename(path, new_path):
|
||||
# 尝试重命名文件,并尝试避免命名冲突,返回最终路径
|
||||
final_path = new_path
|
||||
if os.path.exists(new_path):
|
||||
i = 1
|
||||
while os.path.exists(new_path + str(i)):
|
||||
i += 1
|
||||
os.rename(path, new_path + str(i))
|
||||
final_path = new_path + str(i)
|
||||
else:
|
||||
os.rename(path, new_path)
|
||||
|
||||
return final_path
|
||||
|
||||
|
||||
def check_before_run(app):
|
||||
# 运行前检查关键文件,返回布尔值,其实是因为有人经常忘了
|
||||
|
||||
f = True
|
||||
|
||||
if not os.path.exists('database'):
|
||||
app.logger.warning('Folder `database` is missing.')
|
||||
f = False
|
||||
|
||||
if not os.path.exists('database/songs'):
|
||||
app.logger.warning('Folder `database/songs` is missing.')
|
||||
f = False
|
||||
|
||||
if not os.path.exists('database/arcaea_database.db'):
|
||||
app.logger.warning('File `database/arcaea_database.db` is missing.')
|
||||
f = False
|
||||
try:
|
||||
app.logger.info(
|
||||
'Try to new the file `database/arcaea_database.db`.')
|
||||
main('./database/')
|
||||
app.logger.info(
|
||||
'Success to new the file `database/arcaea_database.db`.')
|
||||
f = True
|
||||
except:
|
||||
app.logger.warning(
|
||||
'Fail to new the file `database/arcaea_database.db`.')
|
||||
|
||||
else:
|
||||
with Connect() as c:
|
||||
try:
|
||||
c.execute('''select value from config where id="version"''')
|
||||
x = c.fetchone()
|
||||
except:
|
||||
x = None
|
||||
|
||||
# 数据库自动更新,不强求
|
||||
if not x or x[0] != ARCAEA_SERVER_VERSION:
|
||||
app.logger.warning(
|
||||
'Maybe the file `database/arcaea_database.db` is an old version.')
|
||||
try:
|
||||
app.logger.info(
|
||||
'Try to update the file `database/arcaea_database.db`.')
|
||||
|
||||
path = try_rename('database/arcaea_database.db',
|
||||
'database/arcaea_database.db.bak')
|
||||
|
||||
try:
|
||||
copy2(path, 'database/arcaea_database.db')
|
||||
except:
|
||||
copy(path, 'database/arcaea_database.db')
|
||||
|
||||
if os.path.isfile("database/old_arcaea_database.db"):
|
||||
os.remove('database/old_arcaea_database.db')
|
||||
|
||||
try_rename('database/arcaea_database.db',
|
||||
'database/old_arcaea_database.db')
|
||||
|
||||
main('./database/')
|
||||
update_database()
|
||||
|
||||
app.logger.info(
|
||||
'Success to update the file `database/arcaea_database.db`.')
|
||||
|
||||
except:
|
||||
app.logger.warning(
|
||||
'Fail to update the file `database/arcaea_database.db`.')
|
||||
|
||||
return f
|
||||
69
latest version/server/multiplayer.py
Normal file
69
latest version/server/multiplayer.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError
|
||||
from core.linkplay import Player, RemoteMultiPlayer, Room
|
||||
from core.sql import Connect
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('multiplayer', __name__, url_prefix='/multiplayer')
|
||||
|
||||
|
||||
@bp.route('/me/room/create', methods=['POST']) # 创建房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_create(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.get_song_unlock(request.json['clientSongMap'])
|
||||
x.create_room(user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/room/join/<room_code>', methods=['POST']) # 加入房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def room_join(user_id, room_code):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.get_song_unlock(request.json['clientSongMap'])
|
||||
room = Room()
|
||||
room.room_code = room_code
|
||||
x.join_room(room, user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/update', methods=['POST']) # 更新房间
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def multiplayer_update(user_id):
|
||||
if not Config.LINKPLAY_HOST:
|
||||
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||
|
||||
with Connect() as c:
|
||||
x = RemoteMultiPlayer()
|
||||
user = Player(c, user_id)
|
||||
user.token = int(request.json['token'])
|
||||
x.update_room(user)
|
||||
r = x.to_dict()
|
||||
r['endPoint'] = request.host.split(
|
||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||
return success_return(r)
|
||||
106
latest version/server/others.py
Normal file
106
latest version/server/others.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
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 flask import Blueprint, jsonify, request
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, error_return, success_return
|
||||
from .present import present_info
|
||||
from .purchase import bundle_bundle, bundle_pack
|
||||
from .score import song_score_friend
|
||||
from .user import user_me
|
||||
from .world import world_all
|
||||
|
||||
bp = Blueprint('others', __name__)
|
||||
|
||||
|
||||
@bp.route('/game/info', methods=['GET']) # 系统信息
|
||||
def game_info():
|
||||
return success_return(GameInfo().to_dict())
|
||||
|
||||
|
||||
@bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def download_song(user_id):
|
||||
with Connect(in_memory=True) as c:
|
||||
x = DownloadList(c, UserOnline(None, user_id))
|
||||
x.song_ids = request.args.getlist('sid')
|
||||
x.url_flag = json.loads(request.args.get('url', 'true'))
|
||||
if x.url_flag and x.is_limited:
|
||||
raise RateLimit('You have reached the download limit.', 903)
|
||||
|
||||
x.add_songs()
|
||||
return success_return(x.urls)
|
||||
|
||||
|
||||
@bp.route('/finale/progress', methods=['GET'])
|
||||
def finale_progress():
|
||||
# 世界boss血条
|
||||
return success_return({'percentage': 100000})
|
||||
|
||||
|
||||
@bp.route('/finale/finale_start', methods=['POST'])
|
||||
def finale_start():
|
||||
# testify开始,对立再见
|
||||
# 没数据
|
||||
return success_return({})
|
||||
|
||||
|
||||
map_dict = {'/user/me': user_me,
|
||||
'/purchase/bundle/pack': bundle_pack,
|
||||
'/serve/download/me/song': download_song,
|
||||
'/game/info': game_info,
|
||||
'/present/me': present_info,
|
||||
'/world/map/me': world_all,
|
||||
'/score/song/friend': song_score_friend,
|
||||
'/purchase/bundle/bundle': bundle_bundle,
|
||||
'/finale/progress': finale_progress}
|
||||
|
||||
|
||||
@bp.route('/compose/aggregate', methods=['GET']) # 集成式请求
|
||||
def aggregate():
|
||||
try:
|
||||
#global request
|
||||
finally_response = {'success': True, 'value': []}
|
||||
#request_ = request
|
||||
get_list = json.loads(request.args.get('calls'))
|
||||
if len(get_list) > 10:
|
||||
# 请求太多驳回
|
||||
return error_return()
|
||||
|
||||
for i in get_list:
|
||||
endpoint = i['endpoint']
|
||||
request.args = ImmutableMultiDict(
|
||||
{key: value[0] for key, value in parse_qs(urlparse(endpoint).query).items()})
|
||||
|
||||
resp_t = map_dict[urlparse(endpoint).path]()
|
||||
if isinstance(resp_t, tuple):
|
||||
# The response may be a tuple, if it is an error response
|
||||
resp_t = resp_t[0]
|
||||
|
||||
if hasattr(resp_t, "response"):
|
||||
resp_t = resp_t.response[0].decode().rstrip('\n')
|
||||
resp = json.loads(resp_t)
|
||||
|
||||
if hasattr(resp, 'get') and resp.get('success') is False:
|
||||
finally_response = {'success': False, 'error_code': resp.get(
|
||||
'error_code'), 'id': i['id']}
|
||||
if "extra" in resp:
|
||||
finally_response['extra'] = resp['extra']
|
||||
#request = request_
|
||||
return jsonify(finally_response)
|
||||
|
||||
finally_response['value'].append(
|
||||
{'id': i.get('id'), 'value': resp['value'] if hasattr(resp, 'get') else resp})
|
||||
|
||||
#request = request_
|
||||
return jsonify(finally_response)
|
||||
except KeyError:
|
||||
return error_return()
|
||||
32
latest version/server/present.py
Normal file
32
latest version/server/present.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from core.error import ArcError
|
||||
from core.present import UserPresent, UserPresentList
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, error_return, success_return
|
||||
|
||||
bp = Blueprint('present', __name__, url_prefix='/present')
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET']) # 用户奖励信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def present_info(user_id):
|
||||
with Connect() as c:
|
||||
x = UserPresentList(c, UserOnline(c, user_id))
|
||||
x.select_user_presents()
|
||||
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/me/claim/<present_id>', methods=['POST']) # 礼物确认
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def claim_present(user_id, present_id):
|
||||
with Connect() as c:
|
||||
x = UserPresent(c, UserOnline(c, user_id))
|
||||
x.claim_user_present(present_id)
|
||||
|
||||
return success_return()
|
||||
152
latest version/server/purchase.py
Normal file
152
latest version/server/purchase.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from time import time
|
||||
|
||||
from core.error import InputError, ItemUnavailable, PostError
|
||||
from core.item import ItemFactory, Stamina6
|
||||
from core.purchase import Purchase, PurchaseList
|
||||
from core.redeem import UserRedeem
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('purchase', __name__, url_prefix='/purchase')
|
||||
|
||||
|
||||
@bp.route('/bundle/pack', methods=['GET']) # 曲包信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def bundle_pack(user_id):
|
||||
with Connect() as c:
|
||||
x = PurchaseList(c, UserOnline(c, user_id)
|
||||
).select_from_type('pack')
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/bundle/single', methods=['GET']) # 单曲购买信息获取
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def get_single(user_id):
|
||||
with Connect() as c:
|
||||
x = PurchaseList(c, UserOnline(c, user_id)
|
||||
).select_from_type('single')
|
||||
return success_return(x.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/bundle/bundle', methods=['GET']) # 捆绑包
|
||||
def bundle_bundle():
|
||||
return success_return([])
|
||||
# 感觉上是定死的,没意义啊
|
||||
# return success_return([{
|
||||
# "name": "chronicles",
|
||||
# "items": [{
|
||||
# "type": "bundle",
|
||||
# "id": "core"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "prelude"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "rei"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "yugamu"
|
||||
# }, {
|
||||
# "type": "bundle",
|
||||
# "id": "vs"
|
||||
# }],
|
||||
# "orig_price": 2000,
|
||||
# "price": 2000,
|
||||
# "discount_from": 1657152000000,
|
||||
# "discount_to": 1758447999000,
|
||||
# "discount_reason": "chronicle"
|
||||
# }])
|
||||
|
||||
|
||||
@bp.route('/me/pack', methods=['POST']) # 曲包和单曲购买
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def buy_pack_or_single(user_id):
|
||||
with Connect() as c:
|
||||
if 'pack_id' in request.form:
|
||||
purchase_name = request.form['pack_id']
|
||||
elif 'single_id' in request.form:
|
||||
purchase_name = request.form['single_id']
|
||||
else:
|
||||
return success_return()
|
||||
|
||||
x = Purchase(c, UserOnline(c, user_id)).select(purchase_name)
|
||||
x.buy()
|
||||
|
||||
return success_return({
|
||||
'user_id': x.user.user_id,
|
||||
'ticket': x.user.ticket,
|
||||
'packs': x.user.packs,
|
||||
'singles': x.user.singles,
|
||||
'characters': x.user.characters_list
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/item', methods=['POST']) # 特殊购买,world模式boost和stamina
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def buy_special(user_id):
|
||||
with Connect() as c:
|
||||
if 'item_id' not in request.form:
|
||||
raise PostError('`item_id` is required')
|
||||
item_id = request.form['item_id']
|
||||
|
||||
x = Purchase(c, UserOnline(c, user_id))
|
||||
x.purchase_name = item_id
|
||||
x.price = 50
|
||||
x.orig_price = 50
|
||||
x.discount_from = -1
|
||||
x.discount_to = -1
|
||||
x.items = [ItemFactory(c).get_item(item_id)]
|
||||
x.buy()
|
||||
|
||||
r = {'user_id': x.user.user_id, 'ticket': x.user.ticket}
|
||||
if item_id == 'stamina6':
|
||||
r['stamina'] = x.user.stamina.stamina
|
||||
r['max_stamina_ts'] = x.user.stamina.max_stamina_ts
|
||||
r['world_mode_locked_end_ts'] = -1
|
||||
return success_return(r)
|
||||
|
||||
|
||||
@bp.route('/me/stamina/<buy_stamina_type>', methods=['POST']) # 购买体力
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def purchase_stamina(user_id, buy_stamina_type):
|
||||
with Connect() as c:
|
||||
if buy_stamina_type != 'fragment':
|
||||
raise InputError('Invalid type of buying stamina')
|
||||
|
||||
user = UserOnline(c, user_id)
|
||||
user.select_user_one_column('next_fragstam_ts', -1)
|
||||
now = int(time()*1000)
|
||||
if user.next_fragstam_ts > now:
|
||||
return ItemUnavailable('Buying stamina by fragment is not available yet.', 905)
|
||||
|
||||
user.update_user_one_column(
|
||||
'next_fragstam_ts', now + 24 * 3600 * 1000)
|
||||
s = Stamina6(c)
|
||||
s.user_claim_item(user)
|
||||
return success_return({
|
||||
"user_id": user.user_id,
|
||||
"stamina": user.stamina.stamina,
|
||||
"max_stamina_ts": user.stamina.max_stamina_ts,
|
||||
"next_fragstam_ts": user.next_fragstam_ts,
|
||||
'world_mode_locked_end_ts': -1
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/me/redeem', methods=['POST']) # 兑换码
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def redeem(user_id):
|
||||
with Connect() as c:
|
||||
x = UserRedeem(c, UserOnline(c, user_id))
|
||||
x.claim_user_redeem(request.form['code'])
|
||||
|
||||
return success_return({"coupon": "fragment" + str(x.fragment) if x.fragment > 0 else ""})
|
||||
138
latest version/server/score.py
Normal file
138
latest version/server/score.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from time import time
|
||||
from core.course import CoursePlay
|
||||
|
||||
from core.error import InputError
|
||||
from core.rank import RankList
|
||||
from core.score import UserPlay
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('score', __name__, url_prefix='/score')
|
||||
|
||||
|
||||
@bp.route('/token', methods=['GET']) # 成绩上传所需的token,显然我不想验证
|
||||
def score_token():
|
||||
return success_return({'token': '1145141919810'})
|
||||
|
||||
|
||||
@bp.route('/token/world', methods=['GET']) # 世界模式成绩上传所需的token
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def score_token_world(user_id):
|
||||
|
||||
stamina_multiply = int(
|
||||
request.args['stamina_multiply']) if 'stamina_multiply' in request.args else 1
|
||||
fragment_multiply = int(
|
||||
request.args['fragment_multiply']) if 'fragment_multiply' in request.args else 100
|
||||
prog_boost_multiply = int(
|
||||
request.args['prog_boost_multiply']) if 'prog_boost_multiply' in request.args else 0
|
||||
with Connect() as c:
|
||||
x = UserPlay(c, UserOnline(c, user_id))
|
||||
x.song.set_chart(request.args['song_id'], int(
|
||||
request.args['difficulty']))
|
||||
x.set_play_state_for_world(stamina_multiply,
|
||||
fragment_multiply, prog_boost_multiply)
|
||||
return success_return({
|
||||
"stamina": x.user.stamina.stamina,
|
||||
"max_stamina_ts": x.user.stamina.max_stamina_ts,
|
||||
"token": x.song_token
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def score_token_course(user_id):
|
||||
with Connect() as c:
|
||||
use_course_skip_purchase = request.args.get(
|
||||
'use_course_skip_purchase', 'false') == 'true'
|
||||
|
||||
user = UserOnline(c, user_id)
|
||||
user_play = UserPlay(c, user)
|
||||
user_play.song_token = request.args.get('previous_token', None)
|
||||
user_play.get_play_state()
|
||||
|
||||
status = 'created'
|
||||
if user_play.course_play_state == -1:
|
||||
# 没有token,课题模式刚开始
|
||||
course_play = CoursePlay(c, user, user_play)
|
||||
course_play.course_id = request.args['course_id']
|
||||
user_play.course_play = course_play
|
||||
user_play.set_play_state_for_course(
|
||||
use_course_skip_purchase)
|
||||
elif 0 <= user_play.course_play_state <= 3:
|
||||
# 验证token
|
||||
user_play.update_token_for_course()
|
||||
else:
|
||||
# 课题模式已经结束
|
||||
user_play.clear_play_state()
|
||||
user.select_user_about_stamina()
|
||||
status = 'cleared' if user_play.course_play_state == 4 else 'failed'
|
||||
|
||||
return success_return({
|
||||
"stamina": user.stamina.stamina,
|
||||
"max_stamina_ts": user.stamina.max_stamina_ts,
|
||||
"token": user_play.song_token,
|
||||
'status': status
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/song', methods=['POST']) # 成绩上传
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_post(user_id):
|
||||
with Connect() as c:
|
||||
x = UserPlay(c, UserOnline(c, user_id))
|
||||
x.song_token = request.form['song_token']
|
||||
x.song_hash = request.form['song_hash']
|
||||
x.song.set_chart(
|
||||
request.form['song_id'], request.form['difficulty'])
|
||||
x.set_score(request.form['score'], request.form['shiny_perfect_count'], request.form['perfect_count'], request.form['near_count'],
|
||||
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.submission_hash = request.form['submission_hash']
|
||||
if not x.is_valid:
|
||||
raise InputError('Invalid score.', 107)
|
||||
x.upload_score()
|
||||
return success_return(x.to_dict())
|
||||
|
||||
|
||||
@bp.route('/song', methods=['GET']) # TOP20
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_top(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_top()
|
||||
return success_return(rank_list.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/song/me', methods=['GET']) # 我的排名,默认最多20
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_me(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_me(UserOnline(c, user_id))
|
||||
return success_return(rank_list.to_dict_list())
|
||||
|
||||
|
||||
@bp.route('/song/friend', methods=['GET']) # 好友排名,默认最多50
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def song_score_friend(user_id):
|
||||
with Connect() as c:
|
||||
rank_list = RankList(c)
|
||||
rank_list.song.set_chart(request.args.get(
|
||||
'song_id'), request.args.get('difficulty'))
|
||||
rank_list.select_friend(UserOnline(c, user_id))
|
||||
return success_return(rank_list.to_dict_list())
|
||||
167
latest version/server/user.py
Normal file
167
latest version/server/user.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from core.character import UserCharacter
|
||||
from core.config_manager import Config
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.item import ItemCore
|
||||
from core.save import SaveData
|
||||
from core.sql import Connect
|
||||
from core.user import User, UserLogin, UserOnline, UserRegister
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('user', __name__, url_prefix='/user')
|
||||
|
||||
|
||||
@bp.route('', methods=['POST']) # 注册接口
|
||||
@arc_try
|
||||
def register():
|
||||
if 'AppVersion' in request.headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
raise NoAccess('Wrong app version.', 1203)
|
||||
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
new_user.set_name(request.form['name'])
|
||||
new_user.set_password(request.form['password'])
|
||||
new_user.set_email(request.form['email'])
|
||||
if 'device_id' in request.form:
|
||||
device_id = request.form['device_id']
|
||||
else:
|
||||
device_id = 'low_version'
|
||||
|
||||
new_user.register()
|
||||
|
||||
# 注册后自动登录
|
||||
user = UserLogin(c)
|
||||
user.login(new_user.name, new_user.password,
|
||||
device_id, request.remote_addr)
|
||||
return success_return({'user_id': user.user_id, 'access_token': user.token})
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET']) # 用户信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def user_me(user_id):
|
||||
with Connect() as c:
|
||||
return success_return(UserOnline(c, user_id).to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/character', methods=['POST']) # 角色切换
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_change(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.change_character(
|
||||
int(request.form['character']), request.form['skill_sealed'] == 'true')
|
||||
|
||||
return success_return({'user_id': user.user_id, 'character': user.character.character_id})
|
||||
|
||||
|
||||
# 角色觉醒切换
|
||||
@bp.route('/me/character/<int:character_id>/toggle_uncap', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def toggle_uncap(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
character = UserCharacter(c, character_id)
|
||||
character.change_uncap_override(user)
|
||||
character.select_character_info(user)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()]})
|
||||
|
||||
|
||||
# 角色觉醒
|
||||
@bp.route('/me/character/<int:character_id>/uncap', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_first_uncap(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
character = UserCharacter(c, character_id)
|
||||
character.select_character_info(user)
|
||||
character.character_uncap(user)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
|
||||
|
||||
|
||||
# 角色使用以太之滴
|
||||
@bp.route('/me/character/<int:character_id>/exp', methods=['POST'])
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def character_exp(user_id, character_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
character = UserCharacter(c, character_id)
|
||||
character.select_character_info(user)
|
||||
core = ItemCore(c)
|
||||
core.amount = - int(request.form['amount'])
|
||||
core.item_id = 'core_generic'
|
||||
character.upgrade_by_core(user, core)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict()], 'cores': user.cores})
|
||||
|
||||
|
||||
@bp.route('/me/save', methods=['GET']) # 从云端同步
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def cloud_get(user_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
save = SaveData(c)
|
||||
save.select_all(user)
|
||||
return success_return(save.to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/save', methods=['POST']) # 向云端同步
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def cloud_post(user_id):
|
||||
with Connect() as c:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
save = SaveData(c)
|
||||
save.set_value(
|
||||
'scores_data', request.form['scores_data'], request.form['scores_checksum'])
|
||||
save.set_value(
|
||||
'clearlamps_data', request.form['clearlamps_data'], request.form['clearlamps_checksum'])
|
||||
save.set_value(
|
||||
'clearedsongs_data', request.form['clearedsongs_data'], request.form['clearedsongs_checksum'])
|
||||
save.set_value(
|
||||
'unlocklist_data', request.form['unlocklist_data'], request.form['unlocklist_checksum'])
|
||||
save.set_value(
|
||||
'installid_data', request.form['installid_data'], request.form['installid_checksum'])
|
||||
save.set_value('devicemodelname_data',
|
||||
request.form['devicemodelname_data'], request.form['devicemodelname_checksum'])
|
||||
save.set_value(
|
||||
'story_data', request.form['story_data'], request.form['story_checksum'])
|
||||
save.set_value(
|
||||
'finalestate_data', request.form.get('finalestate_data'), request.form.get('finalestate_checksum'))
|
||||
|
||||
save.update_all(user)
|
||||
return success_return({'user_id': user.user_id})
|
||||
|
||||
|
||||
@bp.route('/me/setting/<set_arg>', methods=['POST']) # 三个设置
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def sys_set(user_id, set_arg):
|
||||
with Connect() as c:
|
||||
value = request.form['value']
|
||||
user = UserOnline(c, user_id)
|
||||
if 'favorite_character' == set_arg:
|
||||
user.change_favorite_character(int(value))
|
||||
else:
|
||||
value = 'true' == value
|
||||
if 'is_hide_rating' == set_arg or 'max_stamina_notification_enabled' == set_arg:
|
||||
user.update_user_one_column(set_arg, value)
|
||||
return success_return(user.to_dict())
|
||||
|
||||
|
||||
@bp.route('/me/request_delete', methods=['POST']) # 删除账号
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def user_delete(user_id):
|
||||
raise ArcError('Cannot delete the account.', 151, status=404)
|
||||
47
latest version/server/world.py
Normal file
47
latest version/server/world.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from core.sql import Connect
|
||||
from core.user import UserOnline
|
||||
from core.world import UserMap, get_world_all
|
||||
from flask import Blueprint, request
|
||||
|
||||
from .auth import auth_required
|
||||
from .func import arc_try, success_return
|
||||
|
||||
bp = Blueprint('world', __name__, url_prefix='/world')
|
||||
|
||||
|
||||
@bp.route('/map/me', methods=['GET']) # 获得世界模式信息,所有地图
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_all(user_id):
|
||||
with Connect() as c:
|
||||
user = UserOnline(c, user_id)
|
||||
user.select_user_about_current_map()
|
||||
return success_return({
|
||||
"current_map": user.current_map.map_id,
|
||||
"user_id": user_id,
|
||||
"maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in get_world_all(c, user)]
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/map/me', methods=['POST']) # 进入地图
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_in(user_id):
|
||||
with Connect() as c:
|
||||
arcmap = UserMap(c, request.form['map_id'], UserOnline(c, user_id))
|
||||
if arcmap.unlock():
|
||||
return success_return(arcmap.to_dict())
|
||||
|
||||
|
||||
@bp.route('/map/me/<map_id>', methods=['GET']) # 获得单个地图完整信息
|
||||
@auth_required(request)
|
||||
@arc_try
|
||||
def world_one(user_id, map_id):
|
||||
with Connect() as c:
|
||||
arcmap = UserMap(c, map_id, UserOnline(c, user_id))
|
||||
arcmap.change_user_current_map()
|
||||
return success_return({
|
||||
"user_id": user_id,
|
||||
"current_map": map_id,
|
||||
"maps": [arcmap.to_dict(has_map_info=True, has_steps=True)]
|
||||
})
|
||||
BIN
latest version/static/favicon.ico
Normal file
BIN
latest version/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,217 +1,237 @@
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
background: #eee;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: serif;
|
||||
color: #377ba8;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #377ba8;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid lightgray;
|
||||
}
|
||||
|
||||
nav {
|
||||
background: lightgray;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav h1 a {
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li a,
|
||||
nav ul li span,
|
||||
header .action {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.content>header {
|
||||
border-bottom: 1px solid lightgray;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.content>header h1 {
|
||||
flex: auto;
|
||||
margin: 1rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.flash {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: #cae6f6;
|
||||
border: 1px solid #377ba8;
|
||||
}
|
||||
|
||||
.post>header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.post>header>div:first-of-type {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.post>header h1 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post .about {
|
||||
color: slategray;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post .body {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.content:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content form {
|
||||
margin: 1em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.content input,
|
||||
.content textarea {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.content textarea {
|
||||
min-height: 12em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input.danger {
|
||||
color: #cc2f2e;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
|
||||
.score-item {
|
||||
margin-bottom: 10px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.difficulty_pst {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(10, 130, 190);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_prs {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(100, 140, 60);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_ftr {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(80, 25, 75);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_byd {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(130, 35, 40);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-size: 0.8em;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.rank_big {
|
||||
font-size: 1.2em;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.song-detail {
|
||||
font-size: 0.8em;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.song-score {
|
||||
font-size: 1.375em;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
.song-clear-type {
|
||||
font-size: 0.875em;
|
||||
padding-left: 0.625em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.375em;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.12em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer{
|
||||
font-size: 0.5em;
|
||||
color: grey;
|
||||
text-align: center;
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
background: #eee;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: serif;
|
||||
color: #377ba8;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #377ba8;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid lightgray;
|
||||
}
|
||||
|
||||
nav {
|
||||
background: lightgray;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
nav h1 a {
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li a,
|
||||
nav ul li span,
|
||||
header .action {
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.content>header {
|
||||
border-bottom: 1px solid lightgray;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.content>header h1 {
|
||||
flex: auto;
|
||||
margin: 1rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.flash {
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
background: #cae6f6;
|
||||
border: 1px solid #377ba8;
|
||||
}
|
||||
|
||||
.post>header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.post>header>div:first-of-type {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.post>header h1 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.post .about {
|
||||
color: slategray;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.post .body {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.content:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content form {
|
||||
margin: 1em 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.content input,
|
||||
.content textarea {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.content textarea {
|
||||
min-height: 12em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input.danger {
|
||||
color: #cc2f2e;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
|
||||
.score-item {
|
||||
margin-bottom: 10px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.difficulty_pst {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(10, 130, 190);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_prs {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(100, 140, 60);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_ftr {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(80, 25, 75);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty_byd {
|
||||
font-size: 0.9em;
|
||||
background-color: rgb(130, 35, 40);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-size: 0.8em;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.rank_big {
|
||||
font-size: 1.2em;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.song-detail {
|
||||
font-size: 0.8em;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.song-score {
|
||||
font-size: 1.375em;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
.song-clear-type {
|
||||
font-size: 0.875em;
|
||||
padding-left: 0.625em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.375em;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.12em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 0.5em;
|
||||
color: grey;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.char-item {
|
||||
margin-bottom: 10px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.char-name {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.char-id {
|
||||
font-size: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.char-num {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
<!doctype html>
|
||||
<title>{% block title %}{% endblock %} - Arcaea Server</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<nav>
|
||||
<h1>Arcaea Server</h1>
|
||||
<ul>
|
||||
{% if g.user %}
|
||||
<li><span>{{ g.user['username'] }}</span>
|
||||
<li><a href="{{ url_for('index.index') }}">Home</a>
|
||||
<li><a href="{{ url_for('login.logout') }}">Log Out</a> {% else %}
|
||||
<li><a href="{{ url_for('login.login') }}">Log In</a> {% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="content">
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %} {% block content %}{% endblock %}
|
||||
</section>
|
||||
|
||||
<footer id="footer" class="footer">Made by Lost@2020</footer>
|
||||
<!doctype html>
|
||||
<title>{% block title %}{% endblock %} - Arcaea Server</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<nav>
|
||||
<h1>Arcaea Server</h1>
|
||||
<ul>
|
||||
{% if g.user %}
|
||||
<li><span>{{ g.user['username'] }}</span>
|
||||
<li><a href="{{ url_for('index.index') }}">Home</a>
|
||||
<li><a href="{{ url_for('login.logout') }}">Log Out</a> {% else %}
|
||||
<li><a href="{{ url_for('login.login') }}">Log In</a> {% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="content">
|
||||
<header>
|
||||
{% block header %}{% endblock %}
|
||||
</header>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %} {% block content %}{% endblock %}
|
||||
</section>
|
||||
|
||||
<footer id="footer" class="footer">Made by Lost@2020-2022</footer>
|
||||
87
latest version/templates/web/allchar.html
Normal file
87
latest version/templates/web/allchar.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<h1>{% block title %}All characters{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if posts %}<br />
|
||||
{% for char in posts %}
|
||||
|
||||
<div class="char-item">
|
||||
<span>Id: </span>
|
||||
<span class="char-id">{{char['character_id']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Name: </span>
|
||||
<span class="char-name">{{char['name']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Max level: </span>
|
||||
<span class="char-num">{{char['max_level']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Frag-level 1: </span>
|
||||
<span class="char-num">{{char['frag1']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Prog-level 1: </span>
|
||||
<span class="char-num">{{char['prog1']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Overdrive-level 1: </span>
|
||||
<span class="char-num">{{char['overdrive1']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Frag-level 20: </span>
|
||||
<span class="char-num">{{char['frag20']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Prog-level 20: </span>
|
||||
<span class="char-num">{{char['prog20']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Overdrive-level 20: </span>
|
||||
<span class="char-num">{{char['overdrive20']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Frag-level 30: </span>
|
||||
<span class="char-num">{{char['frag30']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Prog-level 30: </span>
|
||||
<span class="char-num">{{char['prog30']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Overdrive-level 30: </span>
|
||||
<span class="char-num">{{char['overdrive30']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Skill id: </span>
|
||||
<span class="char-num">{{char['skill_id']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Skill unlock level: </span>
|
||||
<span class="char-num">{{char['skill_unlock_level']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Uncapped skill id: </span>
|
||||
<span class="char-num">{{char['skill_id_uncap']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Is uncapped: </span>
|
||||
<span class="char-num">{{char['is_uncapped']}}</span>
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
33
latest version/templates/web/allitem.html
Normal file
33
latest version/templates/web/allitem.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<h1>{% block title %}All items{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if posts %}<br />
|
||||
{% for item in posts %}
|
||||
|
||||
<div class="char-item">
|
||||
<span>Id: </span>
|
||||
<span class="name">{{item['item_id']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Type: </span>
|
||||
<span class="char-num">{{item['type']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Is available: </span>
|
||||
<span class="char-num">{{item['is_available']}}</span>
|
||||
<br />
|
||||
|
||||
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,81 +1,85 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<h1>{% block title %}All players{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if posts %}
|
||||
{% for user in posts %}
|
||||
|
||||
<div id="user-info">
|
||||
<div class="name">{{user['name']}}
|
||||
<span class="rank">UID: {{user['user_id']}}</span>
|
||||
<span class="rank">User code: {{user['user_code']}}</span>
|
||||
</div>
|
||||
<div class="join-date">注册于 Registered in: {{user['join_date']}}</div>
|
||||
<div class="ptt">PTT: {{user['rating_ptt']//100 ~ '.' ~ user['rating_ptt']%100}}</div>
|
||||
<div>
|
||||
<div>Recent plays: </div>
|
||||
<div>
|
||||
<div class="score-item">
|
||||
<span class="song-title">
|
||||
{{user['song_id']}}
|
||||
</span>
|
||||
{% if user['difficulty'] == 0 %}
|
||||
<span class="difficulty_pst">PST</span>
|
||||
{% elif user['difficulty'] == 1 %}
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif user['difficulty'] == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="song-detail">
|
||||
<br />
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PURE: </td>
|
||||
<td>{{user['perfect_count']}}</td>
|
||||
<td> {{'(' ~ user['shiny_perfect_count'] ~ ')'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FAR: </td>
|
||||
<td>{{user['near_count']}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOST: </td>
|
||||
<td>{{user['miss_count']}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="song-score">{{user['score']}}</div>
|
||||
<div class="song-clear-type">
|
||||
{% if user['clear_type'] == 3 %}Pure Memory
|
||||
{% elif user['clear_type'] == 2 %}Full Recall
|
||||
{% elif user['clear_type'] == 5 %}Hard Clear
|
||||
{% elif user['clear_type'] == 1 %}Normal Clear
|
||||
{% elif user['clear_type'] == 4 %}Easy Clear
|
||||
{% else%}Track Lost
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="song-rating">成绩评价 Rating: {{user['rating']}}</div>
|
||||
<div class="song-clear-date">日期 Date:
|
||||
{{user['time_played']}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<hr />
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<h1>{% block title %}All players{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if posts %}
|
||||
{% for user in posts %}
|
||||
|
||||
<div id="user-info">
|
||||
<div class="name">{{user['name']}}
|
||||
<span class="rank">UID: {{user['user_id']}}</span>
|
||||
<span class="rank">User code: {{user['user_code']}}</span>
|
||||
{% if user['ban_flag'] %}
|
||||
<span class="rank">Banned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="join-date">注册于 Registered in: {{user['join_date']}}</div>
|
||||
<div class="ptt">PTT: {{'%0.2f'|format(user['rating_ptt']/100|float)}}</div>
|
||||
<div class="ptt">Memories: {{user['ticket']}}</div>
|
||||
<div>
|
||||
<div>Recent plays: </div>
|
||||
<div>
|
||||
<div class="score-item">
|
||||
<span class="song-title">
|
||||
{{user['song_id']}}
|
||||
</span>
|
||||
{% if user['difficulty'] == 0 %}
|
||||
<span class="difficulty_pst">PST</span>
|
||||
{% elif user['difficulty'] == 1 %}
|
||||
<span class="difficulty_prs">PRS</span>
|
||||
{% elif user['difficulty'] == 2 %}
|
||||
<span class="difficulty_ftr">FTR</span>
|
||||
{% else %}
|
||||
<span class="difficulty_byd">BYD</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="song-detail">
|
||||
<br />
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>PURE: </td>
|
||||
<td>{{user['perfect_count']}}</td>
|
||||
<td> {{'(' ~ user['shiny_perfect_count'] ~ ')'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FAR: </td>
|
||||
<td>{{user['near_count']}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LOST: </td>
|
||||
<td>{{user['miss_count']}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="song-score">{{user['score']}}</div>
|
||||
<div class="song-clear-type">
|
||||
{% if user['clear_type'] == 3 %}Pure Memory
|
||||
{% elif user['clear_type'] == 2 %}Full Recall
|
||||
{% elif user['clear_type'] == 5 %}Hard Clear
|
||||
{% elif user['clear_type'] == 1 %}Normal Clear
|
||||
{% elif user['clear_type'] == 4 %}Easy Clear
|
||||
{% else%}Track Lost
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="song-rating">成绩评价 Rating: {{user['rating']}}</div>
|
||||
<div class="song-clear-date">日期 Date:
|
||||
{{user['time_played']}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<hr />
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
55
latest version/templates/web/allpresent.html
Normal file
55
latest version/templates/web/allpresent.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<h1>{% block title %}All presents{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if posts %}<br />
|
||||
{% for present in posts %}
|
||||
|
||||
<div class="char-present">
|
||||
<span>Id: </span>
|
||||
<span class="name">{{present['present_id']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Expire time: </span>
|
||||
<span class="char-num">{{present['expire_ts']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Description: </span>
|
||||
<span class="char-num">{{present['description']}}</span>
|
||||
<br />
|
||||
|
||||
{% if present['items'] %}<br />
|
||||
{% for x in present['items'] %}
|
||||
<span>Item id: </span>
|
||||
<span class="char-num">{{x['item_id']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Item type: </span>
|
||||
<span class="char-num">{{x['type']}}</span>
|
||||
<br />
|
||||
|
||||
<span>Amount: </span>
|
||||
<span class="char-num">{{x['amount']}}</span>
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
{% if not loop.last %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user