mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-08 16:57:26 +08:00
Compare commits
54 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 |
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...
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,5 +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
|
||||
*$py.class
|
||||
|
||||
# setting/config files
|
||||
latest version/config/
|
||||
latest version/config.py
|
||||
60
README.md
60
README.md
@@ -21,6 +21,7 @@ This procedure is mainly used for study and research, and shall not be used for
|
||||
- PTT
|
||||
- 世界排名 Global rank
|
||||
- 排名 Rank
|
||||
- 段位系统 Course system
|
||||
- Link Play
|
||||
- 好友系统 Friends
|
||||
- 数据同步 Data synchronization
|
||||
@@ -66,31 +67,36 @@ It is just so interesting. What it can do is under exploration.
|
||||
> Tips: When updating, please keep the original database in case of data loss.
|
||||
|
||||
|
||||
### Version 2.8.5
|
||||
- 适用于Arcaea 3.12.8版本 For Arcaea 3.12.8
|
||||
- 更新了歌曲数据库 Update the song database.
|
||||
- 修复一个导致无法升级角色的拼写错误 Fix a typing error, which makes giving characters Exp wrong.
|
||||
- 尝试强制指定多进程启动方式为spawn,这可能对UNIX系统中UDP服务器的启动有所帮助 Try to forcibly specify the multiprocess startup mode as spawn, which may be helpful for the startup of UDP server in UNIX system.
|
||||
- 添加了注册API对外接口 Add an external API interface of user register.
|
||||
- 重构一些代码,顺手修复了登陆时因多设备登录封号的用户没有正确显示错误提示的问题 Refactoring some codes and in passing fix the problem that users which has been banned because of multiple devices do not show error messages correctly when logging in.
|
||||
|
||||
> 注意:
|
||||
> - 现在Flask最低版本要求提高到2.0
|
||||
> - 服务端可能不再完全支持低版本客户端
|
||||
> - 对3.12.6c版本,愚人节开关打开时点击`网络`按钮会闪退,原因不明
|
||||
>
|
||||
> Note:
|
||||
> - Now the version of Flask which is required should be up to 2.0.
|
||||
> - The server may no longer fully support lower version clients.
|
||||
> - For version 3.12.6c, when the switch of April Fool's Day is on, clicking the `Network` button will make the client break down. The reason is not clear now.
|
||||
|
||||
|
||||
|
||||
### 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/Linux/Mac OS/Android...
|
||||
- Python 3
|
||||
- Flask module >= 2.0, Cryptography module
|
||||
- Python >= 3.6
|
||||
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
|
||||
- Charles, IDA, proxy app... (optional)
|
||||
|
||||
<!--
|
||||
@@ -108,9 +114,15 @@ It is just so interesting. What it can do is under exploration.
|
||||
[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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,156 +1,105 @@
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import random
|
||||
from server.sql import Connect
|
||||
import functools
|
||||
from setting import Config
|
||||
from flask import jsonify
|
||||
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
|
||||
|
||||
|
||||
class User():
|
||||
# 用户类,当数据类型用
|
||||
def __init__(self, user_id=None, role='', power=[]):
|
||||
self.user_id = user_id
|
||||
self.role = role
|
||||
self.power = power
|
||||
|
||||
|
||||
def login(auth: str, ip: str):
|
||||
# 登录接口,返回字典和错误码
|
||||
|
||||
try:
|
||||
auth_decode = bytes.decode(base64.b64decode(auth))
|
||||
except:
|
||||
return {}, -100
|
||||
|
||||
if not ':' in auth_decode:
|
||||
return {}, -100
|
||||
|
||||
name, password = auth_decode.split(':', 1)
|
||||
|
||||
with Connect() as c:
|
||||
hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
c.execute('''select user_id, password from user where name = :name''', {
|
||||
'name': name})
|
||||
x = c.fetchone()
|
||||
if x is None:
|
||||
return {}, -201
|
||||
if x[1] == '':
|
||||
return {}, -202
|
||||
if hash_pwd != x[1]:
|
||||
return {}, -201
|
||||
|
||||
user_id = str(x[0])
|
||||
now = int(time.time() * 1000)
|
||||
token = hashlib.sha256(
|
||||
(user_id + str(random.randint(10000, 99999)) + str(now)).encode("utf8")).hexdigest()
|
||||
c.execute('''delete from api_login where user_id=?''', (user_id,))
|
||||
c.execute('''insert into api_login values(?,?,?,?)''',
|
||||
(user_id, token, now, ip))
|
||||
|
||||
return {'token': token, 'user_id': user_id}, 0
|
||||
|
||||
|
||||
def logout(user: User):
|
||||
# 登出接口,返回错误码
|
||||
code = 999
|
||||
with Connect() as c:
|
||||
c.execute('''delete from api_login where user_id=?''', (user.user_id,))
|
||||
code = 0
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def id_get_role_id(c, user_id):
|
||||
# user_id获取role_id
|
||||
role_id = 1
|
||||
c.execute('''select role_id from user_role where user_id = :a''',
|
||||
{'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
role_id = int(x[0])
|
||||
|
||||
return role_id
|
||||
|
||||
|
||||
def role_id_get_role(c, role_id):
|
||||
# role_id获取role
|
||||
role = ''
|
||||
c.execute('''select role_name from role where role_id = :a''',
|
||||
{'a': role_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
role = x[0]
|
||||
|
||||
return role
|
||||
|
||||
|
||||
def api_token_get_id(c, token):
|
||||
# api的token获取user_id
|
||||
user_id = None
|
||||
c.execute('''select user_id from api_login where token = :token''', {
|
||||
'token': token})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
user_id = x[0]
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def get_role_power(c, role_id):
|
||||
# 获取role_id对应power,返回列表
|
||||
|
||||
role_power = []
|
||||
|
||||
c.execute('''select power_name from power where power_id in (select power_id from role_power where role_id=:a)''', {
|
||||
'a': role_id})
|
||||
x = c.fetchall()
|
||||
for i in x:
|
||||
role_power.append(i[0])
|
||||
|
||||
return role_power
|
||||
|
||||
|
||||
def role_required(request, power=[]):
|
||||
# api token验证,写成了修饰器
|
||||
def role_required(request, powers=[]):
|
||||
'''api token验证,写成了修饰器'''
|
||||
def decorator(view):
|
||||
@functools.wraps(view)
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
try:
|
||||
request.json # 检查请求json格式
|
||||
if request.data:
|
||||
request.json # 检查请求json格式
|
||||
except:
|
||||
return jsonify({'status': 400, 'code': -1, 'data': {}, 'msg': 'Payload must be a valid json'})
|
||||
return error_return(PostError('Payload must be a valid json', api_error_code=-1), 400)
|
||||
|
||||
if not 'Token' in request.headers:
|
||||
return jsonify({'status': 401, 'code': -1, 'data': {}, 'msg': 'No token'})
|
||||
return error_return(PostError('No token', api_error_code=-1), 401)
|
||||
|
||||
user = User()
|
||||
if Config.API_TOKEN == request.headers['Token'] and Config.API_TOKEN != '':
|
||||
user.user_id = 0
|
||||
elif power == []:
|
||||
return jsonify({'status': 403, 'code': -1, 'data': {}, 'msg': 'No permission'})
|
||||
else:
|
||||
with Connect() as c:
|
||||
user.user_id = api_token_get_id(
|
||||
c, request.headers['Token'])
|
||||
if user.user_id is None:
|
||||
return jsonify({'status': 401, 'code': -1, 'data': {}, 'msg': 'No token'})
|
||||
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()
|
||||
|
||||
role_id = id_get_role_id(c, user.user_id)
|
||||
user.role = role_id_get_role(c, role_id)
|
||||
user.role_power = get_role_power(c, role_id)
|
||||
|
||||
f = False
|
||||
for i in power:
|
||||
if i in user.role_power:
|
||||
f = True
|
||||
break
|
||||
if not f:
|
||||
return jsonify({'status': 403, 'code': -1, 'data': {}, 'msg': 'No permission'})
|
||||
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
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
from core.error import ArcError
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
def code_get_msg(code):
|
||||
# api接口code获取msg,返回字符串
|
||||
msg = {
|
||||
0: '',
|
||||
-1: 'See status code',
|
||||
-2: 'No data',
|
||||
-3: 'No data or user',
|
||||
-4: 'No user_id',
|
||||
-100: 'Wrong post data',
|
||||
-101: 'Wrong data type',
|
||||
-102: 'Wrong query parameter',
|
||||
-103: 'Wrong sort parameter',
|
||||
-104: 'Wrong sort order parameter',
|
||||
-201: 'Wrong username or password',
|
||||
-202: 'User is banned',
|
||||
-203: 'Username exists',
|
||||
-204: 'Email address exists',
|
||||
-999: 'Unknown error'
|
||||
}
|
||||
|
||||
return msg[code]
|
||||
default_error = ArcError('Unknown Error')
|
||||
|
||||
|
||||
def return_encode(code: int = 0, data: dict = {}, status: int = 200, msg: str = ''):
|
||||
# 构造返回,返回jsonify处理过后的response_class
|
||||
if msg == '':
|
||||
msg = code_get_msg(code)
|
||||
if code < 0:
|
||||
return jsonify({'status': status, 'code': code, 'data': {}, 'msg': msg})
|
||||
else:
|
||||
return jsonify({'status': status, 'code': code, 'data': data, 'msg': msg})
|
||||
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
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
from flask import (
|
||||
Blueprint, request, jsonify
|
||||
)
|
||||
import functools
|
||||
import api.api_auth
|
||||
from . import users
|
||||
import api.songs
|
||||
from .api_code import code_get_msg, return_encode
|
||||
|
||||
|
||||
bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
bp.register_blueprint(users.bp)
|
||||
|
||||
|
||||
class Query():
|
||||
# 查询类,当查询附加参数的数据类型用
|
||||
def __init__(self, limit=-1, offset=0, query={}, sort=[]) -> None:
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.query = query # {'name': 'admin'}
|
||||
self.sort = sort # [{'column': 'user_id', 'order': 'ASC'}, ...]
|
||||
|
||||
|
||||
def get_query_parameter(request, query_able=[], sort_able=[]):
|
||||
# 提取查询请求参数,返回Query类查询参数,写成修饰器
|
||||
|
||||
def decorator(view):
|
||||
@functools.wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
re = Query()
|
||||
|
||||
if 'limit' in request.json:
|
||||
try:
|
||||
re.limit = int(request.json['limit'])
|
||||
except:
|
||||
return jsonify({'status': 200, 'code': -101, 'data': {}, 'msg': code_get_msg(-101)})
|
||||
|
||||
if 'offset' in request.json:
|
||||
try:
|
||||
re.offset = int(request.json['offset'])
|
||||
except:
|
||||
return jsonify({'status': 200, 'code': -101, 'data': {}, 'msg': code_get_msg(-101)})
|
||||
if 'query' in request.json:
|
||||
re.query = request.json['query']
|
||||
for i in re.query:
|
||||
if i not in query_able:
|
||||
return jsonify({'status': 200, 'code': -102, 'data': {}, 'msg': code_get_msg(-102)})
|
||||
if 'sort' in request.json:
|
||||
re.sort = request.json['sort']
|
||||
for i in re.sort:
|
||||
if 'column' not in i or i['column'] not in sort_able:
|
||||
return jsonify({'status': 200, 'code': -103, 'data': {}, 'msg': code_get_msg(-103)})
|
||||
if not 'order' in i:
|
||||
i['order'] = 'ASC'
|
||||
else:
|
||||
if i['order'] not in ['ASC', 'DESC']:
|
||||
return jsonify({'status': 200, 'code': -104, 'data': {}, 'msg': code_get_msg(-104)})
|
||||
|
||||
return view(re, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def ping():
|
||||
return return_encode()
|
||||
|
||||
|
||||
@bp.route('/token', methods=['POST'])
|
||||
def token_post():
|
||||
# 登录,获取token
|
||||
# {'auth': `base64(user_id:password)`}
|
||||
|
||||
if 'auth' in request.json:
|
||||
data, code = api.api_auth.login(
|
||||
str(request.json['auth']), request.remote_addr)
|
||||
return return_encode(code, data)
|
||||
else:
|
||||
return return_encode(-1, {}, 401, 'No authentication')
|
||||
|
||||
|
||||
@bp.route('/token', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select_me', 'select'])
|
||||
def token_get(user):
|
||||
# 判断登录有效性
|
||||
return return_encode()
|
||||
|
||||
|
||||
@bp.route('/token', methods=['DELETE'])
|
||||
@api.api_auth.role_required(request, ['change_me', 'select_me', 'select'])
|
||||
def token_delete(user):
|
||||
# 登出
|
||||
return return_encode(api.api_auth.logout(user))
|
||||
|
||||
|
||||
@bp.route('/users', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select'])
|
||||
@get_query_parameter(request, ['user_id', 'name', 'user_code'], [
|
||||
'user_id', 'name', 'user_code', 'join_date', 'rating_ptt', 'time_played', 'ticket', 'world_rank_score'])
|
||||
def users_get(query, user):
|
||||
# 查询全用户信息
|
||||
|
||||
data = users.get_users(query)
|
||||
|
||||
if not data:
|
||||
return return_encode(-2)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_me'])
|
||||
def users_user_get(user, user_id):
|
||||
# 查询用户信息
|
||||
|
||||
if user_id == 'me':
|
||||
user_id = user.user_id
|
||||
|
||||
if user_id <= 0:
|
||||
return return_encode(-4)
|
||||
|
||||
if user_id != user.user_id and not 'select' in user.power and user.user_id != 0: # 查别人需要select权限
|
||||
return return_encode(-1, {}, 403, 'No permission')
|
||||
|
||||
data = users.get_user_info(user_id)
|
||||
|
||||
if not data:
|
||||
return return_encode(-3)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/b30', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_me'])
|
||||
def users_user_b30_get(user, user_id):
|
||||
# 查询用户b30
|
||||
|
||||
if user_id <= 0:
|
||||
return return_encode(-4)
|
||||
|
||||
if user_id != user.user_id and not 'select' in user.power and user.user_id != 0: # 查别人需要select权限
|
||||
return return_encode(-1, {}, 403, 'No permission')
|
||||
|
||||
data = users.get_user_b30(user_id)
|
||||
|
||||
if data['data'] == []:
|
||||
return return_encode(-3)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/best', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_me'])
|
||||
@get_query_parameter(request, ['song_id', 'difficulty'], [
|
||||
'song_id', 'difficulty', 'score', 'time_played', 'rating'])
|
||||
def users_user_best_get(query, user, user_id):
|
||||
# 查询用户所有best成绩
|
||||
|
||||
if user_id <= 0:
|
||||
return return_encode(-4)
|
||||
|
||||
if user_id != user.user_id and not 'select' in user.power and user.user_id != 0: # 查别人需要select权限
|
||||
return return_encode(-1, {}, 403, 'No permission')
|
||||
|
||||
data = users.get_user_best(user_id, query)
|
||||
|
||||
if data['data'] == []:
|
||||
return return_encode(-3)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/r30', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_me'])
|
||||
def users_user_r30_get(user, user_id):
|
||||
# 查询用户r30
|
||||
|
||||
if user_id <= 0:
|
||||
return return_encode(-4)
|
||||
|
||||
if user_id != user.user_id and not 'select' in user.power and user.user_id != 0: # 查别人需要select权限
|
||||
return return_encode(-1, {}, 403, 'No permission')
|
||||
|
||||
data = users.get_user_r30(user_id)
|
||||
|
||||
if data['data'] == []:
|
||||
return return_encode(-3)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/songs/<string:song_id>', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_song_info'])
|
||||
def songs_song_get(user, song_id):
|
||||
# 查询歌曲信息
|
||||
|
||||
data = api.songs.get_song_info(song_id)
|
||||
|
||||
if not data:
|
||||
return return_encode(-2)
|
||||
|
||||
return return_encode(0, data)
|
||||
|
||||
|
||||
@bp.route('/songs', methods=['GET'])
|
||||
@api.api_auth.role_required(request, ['select', 'select_song_info'])
|
||||
@get_query_parameter(request, ['sid', 'name_en', 'name_jp', 'pakset', 'artist'], [
|
||||
'sid', 'name_en', 'name_jp', 'pakset', 'artist', 'date', 'rating_pst', 'rating_prs', 'rating_ftr', 'rating_byn'])
|
||||
def songs_get(query, user):
|
||||
# 查询全歌曲信息
|
||||
|
||||
data = api.songs.get_songs(query)
|
||||
|
||||
if not data:
|
||||
return return_encode(-2)
|
||||
|
||||
return return_encode(0, data)
|
||||
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']
|
||||
@@ -1,58 +1,42 @@
|
||||
from server.sql import Connect
|
||||
from server.sql import Sql
|
||||
import time
|
||||
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')
|
||||
|
||||
|
||||
def get_song_info(song_id):
|
||||
# 查询指定歌曲信息,返回字典
|
||||
r = {}
|
||||
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
c.execute('''select * from songs where sid=:a''', {'a': song_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
r = {'song_id': x[0],
|
||||
'name': {'name_en': x[1],
|
||||
'name_jp': x[2]},
|
||||
'pakset': x[5],
|
||||
'artist': x[6],
|
||||
'date': x[9],
|
||||
'rating_pst': x[13]/10,
|
||||
'rating_prs': x[14]/10,
|
||||
'rating_ftr': x[15]/10,
|
||||
'rating_byn': x[16]/10,
|
||||
'difficultly_pst': x[17]/2,
|
||||
'difficultly_prs': x[18]/2,
|
||||
'difficultly_ftr': x[19]/2,
|
||||
'difficultly_byn': x[20]/2
|
||||
}
|
||||
|
||||
return r
|
||||
@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())
|
||||
|
||||
|
||||
def get_songs(query=None):
|
||||
# 查询全部歌曲信息,返回字典列表
|
||||
r = []
|
||||
@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))
|
||||
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
x = Sql.select(c, 'songs', [], query)
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
if x:
|
||||
for i in x:
|
||||
r.append({'sid': i[0],
|
||||
'name': {'name_en': i[1],
|
||||
'name_jp': i[2]},
|
||||
'pakset': i[5],
|
||||
'artist': i[6],
|
||||
'date': i[9],
|
||||
'rating_pst': i[13]/10,
|
||||
'rating_prs': i[14]/10,
|
||||
'rating_ftr': i[15]/10,
|
||||
'rating_byn': i[16]/10,
|
||||
'difficultly_pst': i[17]/2,
|
||||
'difficultly_prs': i[18]/2,
|
||||
'difficultly_ftr': i[19]/2,
|
||||
'difficultly_byn': i[20]/2
|
||||
})
|
||||
|
||||
return r
|
||||
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()
|
||||
@@ -1,138 +1,193 @@
|
||||
from flask import (
|
||||
Blueprint, request
|
||||
)
|
||||
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_code import code_get_msg, return_encode
|
||||
from .api_auth import role_required
|
||||
from core.user import UserRegister
|
||||
from core.error import ArcError, PostError
|
||||
from server.sql import Connect
|
||||
from server.sql import Sql
|
||||
import time
|
||||
import web.webscore
|
||||
import server.info
|
||||
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'])
|
||||
def users_post(user):
|
||||
# 注册用户
|
||||
@request_json_handle(request, ['name', 'password', 'email'])
|
||||
@api_try
|
||||
def users_post(data, user):
|
||||
'''注册一个用户'''
|
||||
with Connect() as c:
|
||||
new_user = UserRegister(c)
|
||||
try:
|
||||
if 'name' in request.json:
|
||||
new_user.set_name(request.json['name'])
|
||||
else:
|
||||
raise PostError('No name provided.')
|
||||
|
||||
if 'password' in request.json:
|
||||
new_user.set_password(request.json['password'])
|
||||
else:
|
||||
raise PostError('No password provided.')
|
||||
|
||||
if 'email' in request.json:
|
||||
new_user.set_email(request.json['email'])
|
||||
else:
|
||||
raise PostError('No email provided.')
|
||||
|
||||
new_user.register()
|
||||
except ArcError as e:
|
||||
return return_encode(e.api_error_code)
|
||||
|
||||
return return_encode(0, {'user_id': new_user.user_id, 'user_code': new_user.user_code})
|
||||
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})
|
||||
|
||||
|
||||
def get_users(query=None):
|
||||
# 获取全用户信息,返回字典列表
|
||||
|
||||
r = []
|
||||
@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:
|
||||
x = Sql.select(c, 'user', [], query)
|
||||
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 x:
|
||||
for i in x:
|
||||
if i[23] != -1:
|
||||
character_id = i[23]
|
||||
else:
|
||||
character_id = i[6]
|
||||
r.append({
|
||||
'user_id': i[0],
|
||||
'name': i[1],
|
||||
'join_date': i[3],
|
||||
'user_code': i[4],
|
||||
'rating_ptt': i[5]/100,
|
||||
'character_id': character_id,
|
||||
'is_char_uncapped': i[8] == 1,
|
||||
'is_char_uncapped_override': i[9] == 1,
|
||||
'is_hide_rating': i[10],
|
||||
'ticket': i[26]
|
||||
})
|
||||
if not r:
|
||||
raise NoData(api_error_code=-2)
|
||||
|
||||
return r
|
||||
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])
|
||||
|
||||
|
||||
def get_user_info(user_id):
|
||||
# 获取用户信息,返回字典,其实就是调用user/me信息
|
||||
@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)
|
||||
|
||||
r = {}
|
||||
with Connect() as c:
|
||||
r = server.info.get_user_me(c, user_id)
|
||||
|
||||
return r
|
||||
u = UserInfo(c, user_id)
|
||||
return success_return(u.to_dict())
|
||||
|
||||
|
||||
def get_user_b30(user_id):
|
||||
# 获取用户b30信息,返回字典
|
||||
|
||||
r = []
|
||||
@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:
|
||||
r = web.webscore.get_user_score(c, user_id, 30)
|
||||
|
||||
bestptt = 0
|
||||
for i in r:
|
||||
if i['rating']:
|
||||
bestptt += i['rating']
|
||||
if 'time_played' in i:
|
||||
i['time_played'] = int(time.mktime(time.strptime(
|
||||
i['time_played'], '%Y-%m-%d %H:%M:%S')))
|
||||
|
||||
return {'user_id': user_id, 'b30_ptt': bestptt / 30, 'data': r}
|
||||
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)
|
||||
|
||||
|
||||
def get_user_best(user_id, query=None):
|
||||
# 获取用户b30信息,返回字典
|
||||
@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)
|
||||
|
||||
r = []
|
||||
with Connect() as c:
|
||||
x = Sql.select(c, 'best_score', [], query)
|
||||
if x:
|
||||
for i in x:
|
||||
r.append({
|
||||
"song_id": i[1],
|
||||
"difficulty": i[2],
|
||||
"score": i[3],
|
||||
"shiny_perfect_count": i[4],
|
||||
"perfect_count": i[5],
|
||||
"near_count": i[6],
|
||||
"miss_count": i[7],
|
||||
"health": i[8],
|
||||
"modifier": i[9],
|
||||
"time_played": i[10],
|
||||
"best_clear_type": i[11],
|
||||
"clear_type": i[12],
|
||||
"rating": i[13]
|
||||
})
|
||||
|
||||
return {'user_id': user_id, 'data': r}
|
||||
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})
|
||||
|
||||
|
||||
def get_user_r30(user_id):
|
||||
# 获取用户r30信息,返回字典
|
||||
@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)
|
||||
|
||||
r = []
|
||||
with Connect() as c:
|
||||
r, r10_ptt = web.webscore.get_user_recent30(c, user_id)
|
||||
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})
|
||||
|
||||
return {'user_id': user_id, 'r10_ptt': r10_ptt, '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]})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class Config():
|
||||
'''
|
||||
This is the setting file. You can change some parameters here.
|
||||
This is the example setting file.
|
||||
The user's setting file's name is `config.py`.
|
||||
'''
|
||||
|
||||
'''
|
||||
@@ -9,7 +10,7 @@ class Config():
|
||||
Host and port of your server
|
||||
'''
|
||||
HOST = '0.0.0.0'
|
||||
PORT = '80'
|
||||
PORT = 80
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
@@ -19,7 +20,7 @@ class Config():
|
||||
游戏API地址前缀
|
||||
Game API's URL prefix
|
||||
'''
|
||||
GAME_API_PREFIX = '/years/19'
|
||||
GAME_API_PREFIX = '/join/21'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
@@ -30,29 +31,30 @@ class Config():
|
||||
Allowed game versions
|
||||
If it is blank, all are allowed.
|
||||
'''
|
||||
ALLOW_APPVERSION = ['3.12.6', '3.12.6c', '3.12.8', '3.12.8c']
|
||||
ALLOW_APPVERSION = ['3.12.6', '3.12.6c',
|
||||
'4.0.256', '4.0.256c', '4.1.0', '4.1.0c']
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
联机功能的端口号,若为空,则默认不开启联机功能
|
||||
Port of your link play server
|
||||
If it is blank, link play will be unavailable.
|
||||
联机功能相关设置,请确保与Link Play服务器端的设置一致
|
||||
Setting of your link play server
|
||||
Please ensure that the settings on the side of Link Play server are consistent.
|
||||
'''
|
||||
UDP_PORT = '10900'
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
联机功能地址,留空则自动获取
|
||||
Link Play address
|
||||
If left blank, it will be obtained automatically.
|
||||
'''
|
||||
LINK_PLAY_HOST = '' # ***.com
|
||||
# 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 = ''
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
@@ -75,7 +77,7 @@ class Config():
|
||||
愚人节模式开关
|
||||
Switch of April Fool's Day
|
||||
'''
|
||||
IS_APRILFOOLS = False
|
||||
IS_APRILFOOLS = True
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
@@ -186,7 +188,8 @@ class Config():
|
||||
是否记录详细的服务器日志
|
||||
If recording detailed server logs is enabled
|
||||
'''
|
||||
ALLOW_LOG_INFO = False
|
||||
ALLOW_INFO_LOG = False
|
||||
ALLOW_WARNING_LOG = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
@@ -247,18 +250,10 @@ class Config():
|
||||
--------------------
|
||||
是否强制使用全解锁云端存档
|
||||
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
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
|
||||
'''
|
||||
--------------------
|
||||
是否使用最好的 10 条记录(而不是最近的 30 条记录中较好的 10 条)来计算 PTT
|
||||
Calculate PTT with best 10 instead of recent best 10
|
||||
'''
|
||||
USE_B10_AS_R10 = False
|
||||
'''
|
||||
--------------------
|
||||
'''
|
||||
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))
|
||||
@@ -1,54 +1,73 @@
|
||||
from setting import Config
|
||||
from .error import ArcError, NoData
|
||||
from .config_manager import Config
|
||||
from .constant import Constant
|
||||
from .item import Item
|
||||
from .error import ArcError, InputError, ItemNotEnough, NoData
|
||||
from .item import Item, ItemCore
|
||||
|
||||
|
||||
class Level:
|
||||
max_level = None
|
||||
mid_level = 20
|
||||
min_level = 1
|
||||
level = None
|
||||
exp = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
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:
|
||||
skill_id = None
|
||||
skill_id_uncap = None
|
||||
skill_unlock_level = None
|
||||
skill_requires_uncap = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
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'
|
||||
amount = None
|
||||
|
||||
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:
|
||||
start = None
|
||||
mid = None
|
||||
end = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
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):
|
||||
@@ -82,32 +101,62 @@ class CharacterValue:
|
||||
|
||||
|
||||
class Character:
|
||||
character_id = None
|
||||
name = None
|
||||
char_type = None
|
||||
is_uncapped = None
|
||||
is_uncapped_override = None
|
||||
skill = Skill()
|
||||
level = Level()
|
||||
frag = CharacterValue()
|
||||
prog = CharacterValue()
|
||||
overdrive = CharacterValue()
|
||||
uncap_cores = None
|
||||
database_table_name = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
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) -> None:
|
||||
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):
|
||||
# 获取此角色所需核心
|
||||
@@ -119,31 +168,31 @@ class UserCharacter(Character):
|
||||
for i in x:
|
||||
self.uncap_cores.append(Core(i[0], i[1]))
|
||||
|
||||
def select_character_uncap_condition(self, user):
|
||||
def select_character_uncap_condition(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取此角色的觉醒信息
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
self.c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
|
||||
{'a': user.user_id, 'b': self.character_id})
|
||||
else:
|
||||
self.c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user.user_id, 'b': self.character_id})
|
||||
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:
|
||||
raise NoData('The character of the user does not exist.')
|
||||
self.is_uncapped = False
|
||||
self.is_uncapped_override = False
|
||||
# raise NoData('The character of the user does not exist.')
|
||||
else:
|
||||
self.is_uncapped = x[0] == 1
|
||||
self.is_uncapped_override = x[1] == 1
|
||||
|
||||
self.is_uncapped = x[0] == 1
|
||||
self.is_uncapped_override = x[1] == 1
|
||||
|
||||
def select_character_info(self, user):
|
||||
def select_character_info(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 获取所给用户此角色信息
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
self.c.execute('''select * from user_char a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
|
||||
(user.user_id, self.character_id))
|
||||
else:
|
||||
self.c.execute('''select * from user_char_full a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
|
||||
(user.user_id, self.character_id))
|
||||
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.')
|
||||
@@ -162,52 +211,167 @@ class UserCharacter(Character):
|
||||
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()
|
||||
|
||||
@property
|
||||
def to_dict(self):
|
||||
return {"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.overdrive.get_value(self.level),
|
||||
"frag": self.overdrive.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
|
||||
}
|
||||
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):
|
||||
def change_uncap_override(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 切换觉醒状态
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
self.c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user.user_id, 'b': self.character_id})
|
||||
else:
|
||||
self.c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
|
||||
{'a': user.user_id, 'b': self.character_id})
|
||||
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': user.user_id})
|
||||
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
|
||||
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
self.c.execute('''update user_char set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||
'a': 1 if x[1] == 0 else 0, 'b': user.user_id, 'c': self.character_id})
|
||||
else:
|
||||
self.c.execute('''update user_char_full set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||
'a': 1 if x[1] == 0 else 0, 'b': user.user_id, 'c': self.character_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})
|
||||
|
||||
def character_uncap(self):
|
||||
self.is_uncapped_override = x[1] == 0
|
||||
|
||||
def character_uncap(self, user=None):
|
||||
# parameter: user - User类或子类的实例
|
||||
# 觉醒角色
|
||||
pass
|
||||
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)
|
||||
@@ -1,3 +1,8 @@
|
||||
from .config_manager import Config
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.10.1'
|
||||
|
||||
|
||||
class Constant:
|
||||
|
||||
BAN_TIME = [1, 3, 7, 15, 31]
|
||||
@@ -6,6 +11,8 @@ class Constant:
|
||||
|
||||
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,
|
||||
@@ -14,3 +21,81 @@ class Constant:
|
||||
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()
|
||||
@@ -1,51 +1,111 @@
|
||||
class ArcError(Exception):
|
||||
api_error_code = -999
|
||||
error_code = 108
|
||||
message = None
|
||||
extra_data = None
|
||||
|
||||
def __init__(self, message=None, error_code=None, api_error_code=None, extra_data=None) -> None:
|
||||
self.message = message
|
||||
if error_code:
|
||||
self.error_code = error_code
|
||||
if api_error_code:
|
||||
self.api_error_code = api_error_code
|
||||
if extra_data:
|
||||
self.extra_data = extra_data
|
||||
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=None, api_error_code=-100, extra_data=None) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data)
|
||||
'''输入类型错误'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class DataExist(ArcError):
|
||||
# 数据存在
|
||||
'''数据存在'''
|
||||
pass
|
||||
|
||||
|
||||
class NoData(ArcError):
|
||||
# 数据不存在
|
||||
def __init__(self, message=None, error_code=None, api_error_code=-2, extra_data=None) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data)
|
||||
'''数据不存在'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-2, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class PostError(ArcError):
|
||||
# 缺少输入
|
||||
def __init__(self, message=None, error_code=None, api_error_code=-100, extra_data=None) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data)
|
||||
'''缺少输入'''
|
||||
|
||||
def __init__(self, message=None, error_code=108, api_error_code=-100, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class UserBan(ArcError):
|
||||
# 用户封禁
|
||||
def __init__(self, message=None, error_code=121, api_error_code=None, extra_data=None) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data)
|
||||
'''用户封禁'''
|
||||
|
||||
def __init__(self, message=None, error_code=121, api_error_code=-202, extra_data=None, status=200) -> None:
|
||||
super().__init__(message, error_code, api_error_code, extra_data, status)
|
||||
|
||||
|
||||
class ItemNotEnough(ArcError):
|
||||
'''物品数量不足'''
|
||||
|
||||
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()
|
||||
@@ -1,7 +1,390 @@
|
||||
from .config_manager import Config
|
||||
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData
|
||||
|
||||
|
||||
class Item:
|
||||
item_id = None
|
||||
item_type = None
|
||||
is_available = 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)
|
||||
@@ -1,83 +1,409 @@
|
||||
import sqlite3
|
||||
from flask import current_app
|
||||
import traceback
|
||||
from atexit import register
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from .constant import Constant
|
||||
from .error import ArcError, InputError
|
||||
|
||||
|
||||
class Connect():
|
||||
class Connect:
|
||||
# 数据库连接类,上下文管理
|
||||
|
||||
def __init__(self, file_path='./database/arcaea_database.db'):
|
||||
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False):
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db
|
||||
接受:文件路径
|
||||
返回:sqlite3连接操作对象
|
||||
数据库连接,默认连接arcaea_database.db\
|
||||
接受:文件路径\
|
||||
返回:sqlite3连接操作对象
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.in_memory: bool = in_memory
|
||||
|
||||
def __enter__(self):
|
||||
self.conn = sqlite3.connect(self.file_path)
|
||||
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):
|
||||
if self.conn:
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||
flag = True
|
||||
if exc_type is not None:
|
||||
current_app.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
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
|
||||
|
||||
|
||||
class Sql():
|
||||
@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 select(c, table_name, target_column=[], query=None):
|
||||
# 执行查询单句sql语句,返回fetchall数据
|
||||
# 使用准确查询,且在单表内
|
||||
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')
|
||||
|
||||
sql = 'select '
|
||||
sql_dict = {}
|
||||
if len(target_column) >= 2:
|
||||
sql += target_column[0]
|
||||
for i in range(1, len(target_column)):
|
||||
sql += ',' + target_column[i]
|
||||
sql += ' from ' + table_name
|
||||
elif len(target_column) == 1:
|
||||
sql += target_column[0] + ' from ' + table_name
|
||||
else:
|
||||
sql += '* from ' + table_name
|
||||
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]
|
||||
|
||||
where_field = []
|
||||
where_value = []
|
||||
if query:
|
||||
for i in query.query:
|
||||
where_field.append(i)
|
||||
where_value.append(query.query[i])
|
||||
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||
self.update_one_table(c1, c2, 'character')
|
||||
|
||||
if where_field and where_value:
|
||||
sql += ' where '
|
||||
sql += where_field[0] + '=:' + where_field[0]
|
||||
sql_dict[where_field[0]] = where_value[0]
|
||||
if len(where_field) >= 2:
|
||||
for i in range(1, len(where_field)):
|
||||
sql_dict[where_field[i]] = where_value[i]
|
||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
||||
self.update_user_char_full(c2) # 更新user_char_full
|
||||
self.update_user_epilogue(c2) # 更新user的epilogue
|
||||
|
||||
if query and query.sort:
|
||||
sql += ' order by ' + \
|
||||
query.sort[0]['column'] + ' ' + query.sort[0]['order']
|
||||
for i in range(1, len(query.sort)):
|
||||
sql += ', ' + query.sort[i]['column'] + \
|
||||
' ' + query.sort[i]['order']
|
||||
|
||||
if query and query.limit >= 0:
|
||||
sql += ' limit :limit offset :offset'
|
||||
sql_dict['limit'] = query.limit
|
||||
sql_dict['offset'] = query.offset
|
||||
class MemoryDatabase:
|
||||
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)
|
||||
|
||||
c.execute(sql, sql_dict)
|
||||
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()
|
||||
|
||||
return c.fetchall()
|
||||
|
||||
@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
|
||||
}
|
||||
@@ -1,27 +1,57 @@
|
||||
from .error import ArcError, InputError, DataExist, NoAccess, NoData, UserBan
|
||||
from .constant import Constant
|
||||
from .character import UserCharacter
|
||||
from setting import Config
|
||||
import hashlib
|
||||
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:
|
||||
name = None
|
||||
email = None
|
||||
password = None
|
||||
user_id = None
|
||||
user_code = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
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):
|
||||
hash_pwd = None
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
@@ -33,7 +63,7 @@ class UserRegister(User):
|
||||
if self.c.fetchone() == (0,):
|
||||
self.name = name
|
||||
else:
|
||||
raise DataExist('Username exists.', 101, -203)
|
||||
raise DataExist('Username exists.', 101, -210)
|
||||
|
||||
else:
|
||||
raise InputError('Username is invalid.')
|
||||
@@ -41,22 +71,33 @@ class UserRegister(User):
|
||||
def set_password(self, password: str):
|
||||
if 8 <= len(password) <= 32:
|
||||
self.password = password
|
||||
self.hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
else:
|
||||
raise InputError('Password is invalid.')
|
||||
|
||||
def set_email(self, email: str):
|
||||
# 邮箱格式懒得多判断
|
||||
if 4 <= len(email) <= 32 and '@' in email and '.' in email:
|
||||
if 4 <= len(email) <= 64 and '@' in email and '.' in email:
|
||||
self.c.execute(
|
||||
'''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, -204)
|
||||
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
|
||||
@@ -101,8 +142,10 @@ class UserRegister(User):
|
||||
|
||||
def register(self):
|
||||
now = int(time.time() * 1000)
|
||||
self._build_user_code()
|
||||
self._build_user_id()
|
||||
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,
|
||||
@@ -115,22 +158,21 @@ class UserRegister(User):
|
||||
|
||||
class UserLogin(User):
|
||||
# 密码和token的加密方式为 SHA-256
|
||||
device_id = None
|
||||
ip = None
|
||||
hash_pwd = None
|
||||
token = None
|
||||
now = 0
|
||||
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
|
||||
self.hash_pwd = hashlib.sha256(password.encode("utf8")).hexdigest()
|
||||
|
||||
def set_device_id(self, device_id: str):
|
||||
self.device_id = device_id
|
||||
@@ -194,6 +236,9 @@ class UserLogin(User):
|
||||
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()
|
||||
@@ -230,11 +275,10 @@ class UserLogin(User):
|
||||
|
||||
|
||||
class UserAuth(User):
|
||||
token = None
|
||||
|
||||
def __init__(self, c) -> None:
|
||||
super().__init__()
|
||||
self.c = c
|
||||
self.token = None
|
||||
|
||||
def token_get_id(self):
|
||||
# 用token获取id,没有考虑不同用户token相同情况,说不定会有bug
|
||||
@@ -248,33 +292,474 @@ class UserAuth(User):
|
||||
|
||||
return self.user_id
|
||||
|
||||
def code_get_id(self):
|
||||
# 用user_code获取id
|
||||
|
||||
self.c.execute('''select user_id from user where user_code = :a''',
|
||||
{'a': self.user_code})
|
||||
x = self.c.fetchone()
|
||||
|
||||
if x is not None:
|
||||
self.user_id = x[0]
|
||||
else:
|
||||
raise NoData('No user.', 401, -3)
|
||||
|
||||
return self.user_id
|
||||
|
||||
|
||||
class UserOnline(User):
|
||||
character = None
|
||||
|
||||
class UserInfo(User):
|
||||
def __init__(self, c, user_id=None) -> None:
|
||||
super().__init__()
|
||||
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.character.select_character_uncap_condition(self)
|
||||
'''用户角色改变,包括技能封印的改变'''
|
||||
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 skill_sealed else 0, 'b': self.character.character_id, 'c': self.character.is_uncapped, 'd': self.character.is_uncapped_override, 'e': self.user_id})
|
||||
'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)
|
||||
Binary file not shown.
@@ -1,531 +1,15 @@
|
||||
import sqlite3
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
|
||||
# 数据库初始化文件,删掉arcaea_database.db文件后运行即可,谨慎使用
|
||||
|
||||
ARCAEA_SERVER_VERSION = 'v2.8.5'
|
||||
|
||||
|
||||
def main(path='./'):
|
||||
conn = sqlite3.connect(path+'arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
c.execute('''create table if not exists config(id text primary key,
|
||||
value text
|
||||
);''')
|
||||
c.execute('''insert into config values("version", :a);''',
|
||||
{'a': ARCAEA_SERVER_VERSION})
|
||||
c.execute('''create table if not exists user(user_id int primary key,
|
||||
name text unique,
|
||||
password text not null,
|
||||
join_date char(20),
|
||||
user_code char(10),
|
||||
rating_ptt int,
|
||||
character_id int,
|
||||
is_skill_sealed int,
|
||||
is_char_uncapped int,
|
||||
is_char_uncapped_override int,
|
||||
is_hide_rating int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
score int,
|
||||
shiny_perfect_count int,
|
||||
perfect_count int,
|
||||
near_count int,
|
||||
miss_count int,
|
||||
health int,
|
||||
modifier int,
|
||||
time_played int,
|
||||
clear_type int,
|
||||
rating real,
|
||||
favorite_character int,
|
||||
max_stamina_notification_enabled int,
|
||||
current_map text,
|
||||
ticket int,
|
||||
prog_boost int,
|
||||
email text,
|
||||
world_rank_score int,
|
||||
ban_flag text,
|
||||
next_fragstam_ts int,
|
||||
max_stamina_ts int,
|
||||
stamina int
|
||||
);''')
|
||||
c.execute('''create table if not exists login(access_token text,
|
||||
user_id int,
|
||||
login_time int,
|
||||
login_ip text,
|
||||
login_device text,
|
||||
primary key(access_token, user_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists friend(user_id_me int,
|
||||
user_id_other int,
|
||||
primary key (user_id_me, user_id_other)
|
||||
);''')
|
||||
c.execute('''create table if not exists best_score(user_id int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
score int,
|
||||
shiny_perfect_count int,
|
||||
perfect_count int,
|
||||
near_count int,
|
||||
miss_count int,
|
||||
health int,
|
||||
modifier int,
|
||||
time_played int,
|
||||
best_clear_type int,
|
||||
clear_type int,
|
||||
rating real,
|
||||
primary key(user_id, song_id, difficulty)
|
||||
);''')
|
||||
c.execute('''create table if not exists user_char(user_id int,
|
||||
character_id int,
|
||||
level int,
|
||||
exp real,
|
||||
is_uncapped int,
|
||||
is_uncapped_override int,
|
||||
primary key(user_id, character_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists user_char_full(user_id int,
|
||||
character_id int,
|
||||
level int,
|
||||
exp real,
|
||||
is_uncapped int,
|
||||
is_uncapped_override int,
|
||||
primary key(user_id, character_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists character(character_id int primary key,
|
||||
name text,
|
||||
max_level int,
|
||||
frag1 real,
|
||||
prog1 real,
|
||||
overdrive1 real,
|
||||
frag20 real,
|
||||
prog20 real,
|
||||
overdrive20 real,
|
||||
frag30 real,
|
||||
prog30 real,
|
||||
overdrive30 real,
|
||||
skill_id text,
|
||||
skill_unlock_level int,
|
||||
skill_requires_uncap int,
|
||||
skill_id_uncap text,
|
||||
char_type int,
|
||||
is_uncapped int
|
||||
);''')
|
||||
c.execute('''create table if not exists char_item(character_id int,
|
||||
item_id text,
|
||||
type text,
|
||||
amount int,
|
||||
primary key(character_id, item_id, type)
|
||||
);''')
|
||||
c.execute('''create table if not exists recent30(user_id int primary key,
|
||||
r0 real,
|
||||
song_id0 text,
|
||||
r1 real,
|
||||
song_id1 text,
|
||||
r2 real,
|
||||
song_id2 text,
|
||||
r3 real,
|
||||
song_id3 text,
|
||||
r4 real,
|
||||
song_id4 text,
|
||||
r5 real,
|
||||
song_id5 text,
|
||||
r6 real,
|
||||
song_id6 text,
|
||||
r7 real,
|
||||
song_id7 text,
|
||||
r8 real,
|
||||
song_id8 text,
|
||||
r9 real,
|
||||
song_id9 text,
|
||||
r10 real,
|
||||
song_id10 text,
|
||||
r11 real,
|
||||
song_id11 text,
|
||||
r12 real,
|
||||
song_id12 text,
|
||||
r13 real,
|
||||
song_id13 text,
|
||||
r14 real,
|
||||
song_id14 text,
|
||||
r15 real,
|
||||
song_id15 text,
|
||||
r16 real,
|
||||
song_id16 text,
|
||||
r17 real,
|
||||
song_id17 text,
|
||||
r18 real,
|
||||
song_id18 text,
|
||||
r19 real,
|
||||
song_id19 text,
|
||||
r20 real,
|
||||
song_id20 text,
|
||||
r21 real,
|
||||
song_id21 text,
|
||||
r22 real,
|
||||
song_id22 text,
|
||||
r23 real,
|
||||
song_id23 text,
|
||||
r24 real,
|
||||
song_id24 text,
|
||||
r25 real,
|
||||
song_id25 text,
|
||||
r26 real,
|
||||
song_id26 text,
|
||||
r27 real,
|
||||
song_id27 text,
|
||||
r28 real,
|
||||
song_id28 text,
|
||||
r29 real,
|
||||
song_id29 text
|
||||
);''')
|
||||
c.execute('''create table if not exists user_world(user_id int,
|
||||
map_id text,
|
||||
curr_position int,
|
||||
curr_capture real,
|
||||
is_locked int,
|
||||
primary key(user_id, map_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists world_songplay(user_id int,
|
||||
song_id text,
|
||||
difficulty int,
|
||||
stamina_multiply int,
|
||||
fragment_multiply int,
|
||||
prog_boost_multiply int,
|
||||
primary key(user_id, song_id, difficulty)
|
||||
);''')
|
||||
c.execute('''create table if not exists download_token(user_id int,
|
||||
song_id text,
|
||||
file_name text,
|
||||
token text,
|
||||
time int,
|
||||
primary key(user_id, song_id, file_name)
|
||||
);''')
|
||||
c.execute('''create table if not exists user_download(user_id int,
|
||||
token text,
|
||||
time int,
|
||||
primary key(user_id, token, time)
|
||||
);''')
|
||||
c.execute('''create table if not exists item(item_id text,
|
||||
type text,
|
||||
is_available int,
|
||||
_id text,
|
||||
primary key(item_id, type)
|
||||
);''')
|
||||
c.execute('''create table if not exists user_item(user_id int,
|
||||
item_id text,
|
||||
type text,
|
||||
amount int,
|
||||
primary key(user_id, item_id, type)
|
||||
);''')
|
||||
c.execute('''create table if not exists purchase(purchase_name text primary key,
|
||||
price int,
|
||||
orig_price int,
|
||||
discount_from int,
|
||||
discount_to int,
|
||||
discount_reason text
|
||||
);''')
|
||||
c.execute('''create table if not exists purchase_item(purchase_name text,
|
||||
item_id text,
|
||||
type text,
|
||||
amount int,
|
||||
primary key(purchase_name, item_id, type)
|
||||
);''')
|
||||
c.execute('''create table if not exists user_save(user_id int primary key,
|
||||
scores_data text,
|
||||
clearlamps_data text,
|
||||
clearedsongs_data text,
|
||||
unlocklist_data text,
|
||||
installid_data text,
|
||||
devicemodelname_data text,
|
||||
story_data text,
|
||||
createdAt int
|
||||
);''')
|
||||
c.execute('''create table if not exists present(present_id text primary key,
|
||||
expire_ts int,
|
||||
description text
|
||||
);''')
|
||||
c.execute('''create table if not exists user_present(user_id int,
|
||||
present_id text,
|
||||
primary key(user_id, present_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists present_item(present_id text,
|
||||
item_id text,
|
||||
type text,
|
||||
amount int,
|
||||
primary key(present_id, item_id, type)
|
||||
);''')
|
||||
c.execute('''create table if not exists songfile(song_id text,
|
||||
file_type int,
|
||||
md5 text,
|
||||
primary key(song_id, file_type)
|
||||
);''')
|
||||
c.execute('''create table if not exists redeem(code text primary key,
|
||||
type int
|
||||
);''')
|
||||
c.execute('''create table if not exists user_redeem(user_id int,
|
||||
code text,
|
||||
primary key(user_id, code)
|
||||
);''')
|
||||
c.execute('''create table if not exists redeem_item(code text,
|
||||
item_id text,
|
||||
type text,
|
||||
amount int,
|
||||
primary key(code, item_id, type)
|
||||
);''')
|
||||
|
||||
c.execute('''create table if not exists role(role_id int primary key,
|
||||
role_name text,
|
||||
caption text
|
||||
);''')
|
||||
c.execute('''create table if not exists user_role(user_id int,
|
||||
role_id int,
|
||||
primary key(user_id, role_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists power(power_id int primary key,
|
||||
power_name text,
|
||||
caption text
|
||||
);''')
|
||||
c.execute('''create table if not exists role_power(role_id int,
|
||||
power_id int,
|
||||
primary key(role_id, power_id)
|
||||
);''')
|
||||
c.execute('''create table if not exists api_login(user_id int,
|
||||
token text,
|
||||
login_time int,
|
||||
login_ip text,
|
||||
primary key(user_id, token)
|
||||
);''')
|
||||
|
||||
# 搭档初始化
|
||||
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', '', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
|
||||
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita']
|
||||
|
||||
skill_id = ['gauge_easy', '', '', '', 'note_mirror', '', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita']
|
||||
|
||||
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]
|
||||
|
||||
frag1 = [55, 55, 60, 50, 47, 0, 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]
|
||||
|
||||
prog1 = [35, 55, 47, 50, 60, 0, 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]
|
||||
|
||||
overdrive1 = [35, 55, 25, 50, 47, 0, 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]
|
||||
|
||||
frag20 = [78, 80, 90, 75, 70, 0, 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]
|
||||
|
||||
prog20 = [61, 80, 70, 75, 90, 0, 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]
|
||||
|
||||
overdrive20 = [61, 80, 47, 75, 70, 0, 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]
|
||||
|
||||
frag30 = [88, 90, 100, 75, 80, 0, 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]
|
||||
|
||||
prog30 = [71, 90, 80, 75, 100, 0, 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]
|
||||
|
||||
overdrive30 = [71, 90, 57, 75, 80, 0, 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]
|
||||
|
||||
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]
|
||||
|
||||
char_core = {
|
||||
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||
1: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_desolate', 'amount': 25}],
|
||||
2: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_crimson', 'amount': 25}],
|
||||
4: [{'core_id': 'core_ambivalent', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||
13: [{'core_id': 'core_scarlet', 'amount': 30}],
|
||||
21: [{'core_id': 'core_scarlet', 'amount': 30}],
|
||||
26: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
27: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
28: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
29: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
36: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
42: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
43: [{'core_id': 'core_chunithm', 'amount': 15}],
|
||||
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
|
||||
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
|
||||
19: [{'core_id': 'core_colorful', 'amount': 30}]
|
||||
}
|
||||
|
||||
for i in range(0, 55):
|
||||
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]:
|
||||
sql = '''insert into character values(?,?,30,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1)'''
|
||||
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
|
||||
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
|
||||
else:
|
||||
if i != 5:
|
||||
sql = '''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)'''
|
||||
c.execute(sql, (i, char[i], frag1[i], prog1[i], overdrive1[i], frag20[i], prog20[i], overdrive20[i],
|
||||
frag30[i], prog30[i], overdrive30[i], skill_id[i], skill_unlock_level[i], skill_requires_uncap, skill_id_uncap[i], char_type[i]))
|
||||
|
||||
c.execute('''insert into character values(?,?,20,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0)''', (99,
|
||||
'shirahime', 38, 33, 28, 66, 58, 50, 66, 58, 50, 'frags_preferred_song', 0, 0, '', 0))
|
||||
|
||||
for i in char_core:
|
||||
for j in char_core[i]:
|
||||
c.execute('''insert into char_item values(?,?,'core',?)''',
|
||||
(i, j['core_id'], j['amount']))
|
||||
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
|
||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful']
|
||||
|
||||
for i in cores:
|
||||
c.execute('''insert into item values(?,"core",1,'')''', (i,))
|
||||
|
||||
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
|
||||
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote"]
|
||||
for i in world_songs:
|
||||
c.execute('''insert into item values(?,"world_song",1,'')''', (i,))
|
||||
|
||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6"]
|
||||
for i in world_unlocks:
|
||||
c.execute('''insert into item values(?,"world_unlock",1,'')''', (i,))
|
||||
|
||||
c.execute('''insert into item values(?,?,?,?)''',
|
||||
('fragment', 'fragment', 1, ''))
|
||||
c.execute('''insert into item values(?,?,?,?)''',
|
||||
('memory', 'memory', 1, ''))
|
||||
c.execute('''insert into item values(?,?,?,?)''',
|
||||
('anni5tix', 'anni5tix', 1, ''))
|
||||
|
||||
def insert_items(c, items):
|
||||
# 物品数据导入
|
||||
for i in items:
|
||||
if 'discount_from' not in i:
|
||||
discount_from = -1
|
||||
else:
|
||||
discount_from = i['discount_from']
|
||||
if 'discount_to' not in i:
|
||||
discount_to = -1
|
||||
else:
|
||||
discount_to = i['discount_to']
|
||||
if 'discount_reason' not in i:
|
||||
discount_reason = ''
|
||||
else:
|
||||
discount_reason = i['discount_reason']
|
||||
c.execute('''insert into purchase values(?,?,?,?,?,?)''',
|
||||
(i['name'], i['price'], i['orig_price'], discount_from, discount_to, discount_reason))
|
||||
for j in i['items']:
|
||||
if "_id" not in j:
|
||||
_id = ''
|
||||
else:
|
||||
_id = j['_id']
|
||||
c.execute(
|
||||
'''select exists(select * from item where item_id=?)''', (j['id'],))
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into item values(?,?,?,?)''',
|
||||
(j['id'], j['type'], j['is_available'], _id))
|
||||
if 'amount' in j:
|
||||
amount = j['amount']
|
||||
else:
|
||||
amount = 1
|
||||
c.execute('''insert into purchase_item values(?,?,?,?)''',
|
||||
(i['name'], j['id'], j['type'], amount))
|
||||
|
||||
# item初始化
|
||||
f = open(path+'singles.json', 'r')
|
||||
singles = json.load(f)
|
||||
f.close()
|
||||
insert_items(c, singles)
|
||||
|
||||
f = open(path+'packs.json', 'r')
|
||||
packs = json.load(f)
|
||||
f.close()
|
||||
insert_items(c, packs)
|
||||
|
||||
# api权限与权限组初始化
|
||||
role = ['admin', 'user', 'selecter']
|
||||
role_caption = ['管理员', '用户', '查询接口']
|
||||
|
||||
power = ['select', 'select_me', 'change', 'change_me', 'grant',
|
||||
'grant_inf', 'select_song_rank', 'select_song_info']
|
||||
power_caption = ['总体查询权限', '自我查询权限', '总体修改权限',
|
||||
'自我修改权限', '授权权限', '下级授权权限', '歌曲排行榜查询权限', '歌曲信息查询权限']
|
||||
|
||||
role_power = {'0': [0, 1, 3, 5, 6, 7],
|
||||
'1': [1, 3, 6, 7],
|
||||
'2': [0]
|
||||
}
|
||||
|
||||
for i in range(0, len(role)):
|
||||
c.execute('''insert into role values(:a,:b,:c)''', {
|
||||
'a': i, 'b': role[i], 'c': role_caption[i]})
|
||||
for i in range(0, len(power)):
|
||||
c.execute('''insert into power values(:a,:b,:c)''', {
|
||||
'a': i, 'b': power[i], 'c': power_caption[i]})
|
||||
for i in role_power:
|
||||
for j in role_power[i]:
|
||||
c.execute('''insert into role_power values(:a,:b)''',
|
||||
{'a': int(i), 'b': j})
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def arc_register(name: str):
|
||||
def build_user_code(c):
|
||||
return '123456789'
|
||||
|
||||
def build_user_id(c):
|
||||
return 2000000
|
||||
|
||||
def insert_user_char(c, user_id):
|
||||
# 为用户添加可用角色
|
||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(user_id, 0, 30, 25000, 1, 0))
|
||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(user_id, 1, 30, 25000, 1, 0))
|
||||
|
||||
c.execute(
|
||||
'''select character_id, max_level, is_uncapped from character''')
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
exp = 25000 if i[1] == 30 else 10000
|
||||
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''',
|
||||
(user_id, i[0], i[1], exp, i[2], 0))
|
||||
|
||||
conn = sqlite3.connect(path+'arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
hash_pwd = '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb'
|
||||
c.execute(
|
||||
'''select exists(select * from user where name = :name)''', {'name': name})
|
||||
if c.fetchone() == (0,):
|
||||
user_code = build_user_code(c)
|
||||
user_id = build_user_id(c)
|
||||
now = int(time.time() * 1000)
|
||||
c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt,
|
||||
character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket)
|
||||
values(:user_id, :name, :password, :join_date, :user_code, 1250, 1, 0, 1, 0, 0, -1, 0, '', 114514)
|
||||
''', {'user_code': user_code, 'user_id': user_id, 'join_date': now, 'name': name, 'password': hash_pwd})
|
||||
c.execute('''insert into recent30(user_id) values(:user_id)''', {
|
||||
'user_id': user_id})
|
||||
c.execute(
|
||||
'''insert into best_score values(2000000,'vexaria',3,10000000,100,0,0,0,100,0,1599667200,3,3,10.8)''')
|
||||
insert_user_char(c, user_id)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return None
|
||||
else:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
arc_register('admin')
|
||||
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__':
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -41,8 +41,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "dynamix",
|
||||
@@ -66,8 +66,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "mirai",
|
||||
@@ -91,8 +91,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "yugamu",
|
||||
@@ -111,8 +111,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "lanota",
|
||||
@@ -131,8 +131,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "nijuusei",
|
||||
@@ -171,8 +171,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "tonesphere",
|
||||
@@ -191,8 +191,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "groovecoaster",
|
||||
@@ -211,8 +211,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "zettai",
|
||||
@@ -231,8 +231,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "chunithm",
|
||||
@@ -269,8 +269,8 @@
|
||||
],
|
||||
"price": 400,
|
||||
"orig_price": 400,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "omatsuri",
|
||||
@@ -289,8 +289,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "vs",
|
||||
@@ -309,8 +309,8 @@
|
||||
],
|
||||
"price": 500,
|
||||
"orig_price": 500,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "extend",
|
||||
@@ -329,8 +329,9 @@
|
||||
],
|
||||
"price": 0,
|
||||
"orig_price": 700,
|
||||
"discount_from": 1615248000000,
|
||||
"discount_to": 1615852799000
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000,
|
||||
"discount_reason": "extend"
|
||||
},
|
||||
{
|
||||
"name": "alice",
|
||||
@@ -348,7 +349,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "alice_append_1",
|
||||
@@ -366,7 +369,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "ongeki",
|
||||
@@ -384,7 +389,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
"price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "maimai",
|
||||
@@ -402,7 +409,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400
|
||||
"price": 400,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "chunithm_append_1",
|
||||
@@ -438,7 +447,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "observer",
|
||||
@@ -456,7 +467,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "observer_append_2",
|
||||
@@ -474,7 +487,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 300,
|
||||
"price": 300
|
||||
"price": 300,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "wacca",
|
||||
@@ -492,7 +507,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "nijuusei_append_1",
|
||||
@@ -528,7 +545,9 @@
|
||||
}
|
||||
],
|
||||
"orig_price": 500,
|
||||
"price": 500
|
||||
"price": 500,
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "musedash",
|
||||
@@ -538,20 +557,17 @@
|
||||
"id": "musedash",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "character",
|
||||
"id": "marija",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 5,
|
||||
"amount": 4,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 400,
|
||||
"price": 400,
|
||||
"orig_price": 400
|
||||
"discount_from": 1646784000000,
|
||||
"discount_to": 1647388799000
|
||||
},
|
||||
{
|
||||
"name": "lanota_append_1",
|
||||
@@ -570,5 +586,41 @@
|
||||
],
|
||||
"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
|
||||
}
|
||||
]
|
||||
@@ -1180,5 +1180,77 @@
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "mu",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "mu",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "pupa",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "pupa",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "freemyself",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "freemyself",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
},
|
||||
{
|
||||
"name": "cocorocosmetic",
|
||||
"items": [
|
||||
{
|
||||
"type": "single",
|
||||
"id": "cocorocosmetic",
|
||||
"is_available": true
|
||||
},
|
||||
{
|
||||
"type": "core",
|
||||
"amount": 1,
|
||||
"id": "core_generic",
|
||||
"is_available": true
|
||||
}
|
||||
],
|
||||
"orig_price": 100,
|
||||
"price": 100
|
||||
}
|
||||
]
|
||||
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;
|
||||
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
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
from os import urandom
|
||||
from cryptography.hazmat.primitives.ciphers import (
|
||||
Cipher, algorithms, modes
|
||||
)
|
||||
|
||||
|
||||
def encrypt(key, plaintext, associated_data):
|
||||
iv = os.urandom(12)
|
||||
iv = urandom(12)
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.GCM(iv, min_tag_length=12),
|
||||
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]
|
||||
@@ -1,3 +1,6 @@
|
||||
from .config import Config
|
||||
|
||||
|
||||
def b(value, length=1):
|
||||
return value.to_bytes(length=length, byteorder='little')
|
||||
|
||||
@@ -35,7 +38,7 @@ class Player:
|
||||
self.last_timestamp = 0
|
||||
self.extra_command_queue = []
|
||||
|
||||
self.song_unlock = b'\x00' * 512
|
||||
self.song_unlock: bytes = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.start_command_num = 0
|
||||
|
||||
@@ -58,7 +61,7 @@ class Room:
|
||||
self.song_idx = 0xffff
|
||||
self.last_song_idx = 0xffff
|
||||
|
||||
self.song_unlock = b'\x00' * 512
|
||||
self.song_unlock = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.host_id = 0
|
||||
self.players = [Player(), Player(), Player(), Player()]
|
||||
@@ -120,12 +123,12 @@ class Room:
|
||||
|
||||
def update_song_unlock(self):
|
||||
# 更新房间可用歌曲
|
||||
r = bi(b'\xff' * 512)
|
||||
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, 512)
|
||||
self.song_unlock = b(r, Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
|
||||
def is_ready(self, old_state: int, player_state: int):
|
||||
# 是否全部准备就绪
|
||||
@@ -140,6 +143,9 @@ class Room:
|
||||
|
||||
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
|
||||
@@ -1,9 +1,9 @@
|
||||
from operator import irshift
|
||||
from .udp_sender import CommandSender
|
||||
from .udp_class import bi, Room
|
||||
from .udp_config import Config
|
||||
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:
|
||||
@@ -169,13 +169,14 @@ class CommandParser:
|
||||
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 >= 5000000:
|
||||
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))
|
||||
@@ -183,10 +184,10 @@ class CommandParser:
|
||||
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
|
||||
flag_13 = False
|
||||
|
||||
if player.online == 0:
|
||||
flag_12 = True
|
||||
@@ -229,6 +230,9 @@ class CommandParser:
|
||||
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)
|
||||
@@ -278,8 +282,6 @@ class CommandParser:
|
||||
flag_13 = True
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
if self.room.round_switch == 1:
|
||||
self.room.make_round()
|
||||
|
||||
for i in self.room.players:
|
||||
i.timer = 0
|
||||
@@ -310,7 +312,7 @@ class CommandParser:
|
||||
self.room.command_queue_length += 1
|
||||
self.room.command_queue.append(x.command_12(self.player_index))
|
||||
|
||||
if self.room.state == 3:
|
||||
if self.room.state == 3 or self.room.state == 2:
|
||||
self.room.state = 1
|
||||
self.room.song_idx = 0xffff
|
||||
# self.room.command_queue_length += 1
|
||||
@@ -1,137 +1,60 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from flask import Flask, json, request, jsonify, send_from_directory
|
||||
from logging.config import dictConfig
|
||||
from setting import Config
|
||||
import server
|
||||
import server.auth
|
||||
import server.info
|
||||
import server.setme
|
||||
import server.arcscore
|
||||
import web.login
|
||||
import web.index
|
||||
import api.api_main
|
||||
import server.arcworld
|
||||
import server.arcdownload
|
||||
import server.arcpurchase
|
||||
import server.init
|
||||
import server.character
|
||||
import server.arclinkplay
|
||||
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 multiprocessing import Process, Pipe, set_start_method
|
||||
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
|
||||
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from werkzeug.datastructures import ImmutableMultiDict
|
||||
|
||||
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__)
|
||||
wsgi_app = app.wsgi_app
|
||||
|
||||
if Config.USE_PROXY_FIX:
|
||||
# 代理修复
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
||||
if Config.USE_CORS:
|
||||
# 服务端跨域
|
||||
from flask_cors import CORS
|
||||
CORS(app, supports_credentials=True)
|
||||
|
||||
|
||||
os.chdir(sys.path[0]) # 更改工作路径,以便于愉快使用相对路径
|
||||
|
||||
|
||||
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.api_main.bp)
|
||||
app.register_blueprint(api.bp)
|
||||
app.register_blueprint(server.bp)
|
||||
|
||||
conn1, conn2 = Pipe()
|
||||
|
||||
|
||||
def add_url_prefix(url, strange_flag=False):
|
||||
# 给url加前缀,返回字符串
|
||||
if not url or not Config.GAME_API_PREFIX:
|
||||
return Config.GAME_API_PREFIX + url
|
||||
|
||||
prefix = Config.GAME_API_PREFIX
|
||||
if prefix[0] != '/':
|
||||
prefix = '/' + prefix
|
||||
if prefix[-1] == '/':
|
||||
prefix = prefix[:-1]
|
||||
|
||||
if url[0] != '/':
|
||||
r = '/' + url
|
||||
else:
|
||||
r = url
|
||||
|
||||
if strange_flag and prefix.count('/') >= 1: # 为了方便处理双斜杠
|
||||
t = prefix[::-1]
|
||||
t = t[t.find('/')+1:]
|
||||
prefix = t[::-1]
|
||||
|
||||
return prefix + r
|
||||
|
||||
|
||||
def error_return(error_code, extra={}): # 错误返回
|
||||
# -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
|
||||
# 其它 发生未知错误
|
||||
if extra:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error_code": error_code,
|
||||
"extra": extra
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error_code": error_code
|
||||
})
|
||||
|
||||
|
||||
def success_return(value):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": value
|
||||
})
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
@@ -147,587 +70,62 @@ def favicon():
|
||||
return app.send_static_file('favicon.ico')
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/purchase/bundle/pack'), methods=['GET']) # 曲包信息
|
||||
@server.auth.auth_required(request)
|
||||
def bundle_pack(user_id):
|
||||
return success_return(server.info.get_purchase_pack(user_id))
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/game/info'), methods=['GET']) # 系统信息
|
||||
def game_info():
|
||||
return success_return(server.info.get_game_info())
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/present/me'), methods=['GET']) # 用户奖励信息
|
||||
@server.auth.auth_required(request)
|
||||
def present_info(user_id):
|
||||
return success_return(server.info.get_user_present(user_id))
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/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(108)
|
||||
|
||||
for i in get_list:
|
||||
endpoint = i['endpoint']
|
||||
url = add_url_prefix(endpoint)
|
||||
request.args = ImmutableMultiDict(
|
||||
{key: value[0] for key, value in parse_qs(urlparse(url).query).items()})
|
||||
|
||||
resp_t = map_dict[urlparse(endpoint).path]()
|
||||
|
||||
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': 7, 'extra': {
|
||||
"id": i['id'], 'error_code': resp.get('error_code')}}
|
||||
if "extra" in resp:
|
||||
finally_response['extra']['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(108)
|
||||
|
||||
|
||||
# # 集成式请求,没想到什么好办法处理,就先这样写着
|
||||
# @app.route(add_url_prefix('/compose/aggregate'), methods=['GET'])
|
||||
# @server.auth.auth_required(request)
|
||||
# def aggregate(user_id):
|
||||
# calls = request.args.get('calls')
|
||||
# if calls == '[{ "endpoint": "/user/me", "id": 0 }]': # 极其沙雕的判断,我猜get的参数就两种
|
||||
# r = server.info.arc_aggregate_small(user_id)
|
||||
# else:
|
||||
# r = server.info.arc_aggregate_big(user_id)
|
||||
# return jsonify(r)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/user/me'), methods=['GET']) # 用户信息
|
||||
@server.auth.auth_required(request)
|
||||
def user_me(user_id):
|
||||
r = server.info.get_user_me_c(user_id)
|
||||
if r:
|
||||
return success_return(r)
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
# 角色觉醒
|
||||
@app.route(add_url_prefix('/<path:path>/uncap', True), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def character_first_uncap(user_id, path):
|
||||
character_id = int(path[path.find('character')+10:])
|
||||
r = server.character.char_uncap(user_id, character_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
# 角色使用以太之滴
|
||||
@app.route(add_url_prefix('/<path:path>/exp', True), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def character_exp(user_id, path):
|
||||
character_id = int(path[path.find('character')+10:])
|
||||
amount = int(request.form['amount'])
|
||||
r = server.character.char_use_core(user_id, character_id, amount)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/friend/me/add'), methods=['POST']) # 加好友
|
||||
@server.auth.auth_required(request)
|
||||
def add_friend(user_id):
|
||||
friend_code = request.form['friend_code']
|
||||
friend_id = server.auth.code_get_id(friend_code)
|
||||
if friend_id is not None:
|
||||
r = server.setme.arc_add_friend(user_id, friend_id)
|
||||
if r is not None and r != 602 and r != 604:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": r
|
||||
}
|
||||
})
|
||||
else:
|
||||
if r is not None:
|
||||
return error_return(r)
|
||||
else:
|
||||
return error_return(108)
|
||||
else:
|
||||
return error_return(401)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/friend/me/delete'), methods=['POST']) # 删好友
|
||||
@server.auth.auth_required(request)
|
||||
def delete_friend(user_id):
|
||||
friend_id = int(request.form['friend_id'])
|
||||
if friend_id is not None:
|
||||
r = server.setme.arc_delete_friend(user_id, friend_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"updatedAt": "2020-09-07T07:32:12.740Z",
|
||||
"createdAt": "2020-09-06T10:05:18.471Z",
|
||||
"friends": r
|
||||
}
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
else:
|
||||
return error_return(401)
|
||||
|
||||
|
||||
# 好友排名,默认最多50
|
||||
@app.route(add_url_prefix('/score/song/friend'), methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_friend(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_friend(user_id, song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/score/song/me'), methods=['GET']) # 我的排名,默认最多20
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_me(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_me(user_id, song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/score/song'), methods=['GET']) # TOP20
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_top(user_id):
|
||||
song_id = request.args.get('song_id')
|
||||
difficulty = request.args.get('difficulty')
|
||||
r = server.arcscore.arc_score_top(song_id, difficulty)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/score/song'), methods=['POST']) # 成绩上传
|
||||
@server.auth.auth_required(request)
|
||||
def song_score_post(user_id):
|
||||
song_token = request.form['song_token']
|
||||
song_hash = request.form['song_hash']
|
||||
song_id = request.form['song_id']
|
||||
difficulty = int(request.form['difficulty'])
|
||||
score = int(request.form['score'])
|
||||
shiny_perfect_count = int(request.form['shiny_perfect_count'])
|
||||
perfect_count = int(request.form['perfect_count'])
|
||||
near_count = int(request.form['near_count'])
|
||||
miss_count = int(request.form['miss_count'])
|
||||
health = int(request.form['health'])
|
||||
modifier = int(request.form['modifier'])
|
||||
beyond_gauge = int(request.form['beyond_gauge'])
|
||||
clear_type = int(request.form['clear_type'])
|
||||
submission_hash = request.form['submission_hash']
|
||||
|
||||
# 增加成绩校验
|
||||
if not server.arcscore.arc_score_check(user_id, song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type, song_token, song_hash, submission_hash):
|
||||
return error_return(107)
|
||||
|
||||
r, re = server.arcscore.arc_score_post(user_id, song_id, difficulty, score, shiny_perfect_count,
|
||||
perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type)
|
||||
if re:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": re
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
# 成绩上传所需的token,显然我不想验证
|
||||
@app.route(add_url_prefix('/score/token'), methods=['GET'])
|
||||
def score_token():
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"token": "1145141919810"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
# 世界模式成绩上传所需的token,无验证
|
||||
@app.route(add_url_prefix('/score/token/world'), methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def score_token_world(user_id):
|
||||
args = request.args
|
||||
r = server.arcworld.play_world_song(user_id, args)
|
||||
if r:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/user/me/save'), methods=['GET']) # 从云端同步
|
||||
@server.auth.auth_required(request)
|
||||
def cloud_get(user_id):
|
||||
r = server.arcscore.arc_all_get(user_id)
|
||||
if r is not None:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/user/me/save'), methods=['POST']) # 向云端同步
|
||||
@server.auth.auth_required(request)
|
||||
def cloud_post(user_id):
|
||||
scores_data = request.form['scores_data']
|
||||
clearlamps_data = request.form['clearlamps_data']
|
||||
clearedsongs_data = request.form['clearedsongs_data']
|
||||
unlocklist_data = request.form['unlocklist_data']
|
||||
installid_data = request.form['installid_data']
|
||||
devicemodelname_data = request.form['devicemodelname_data']
|
||||
story_data = request.form['story_data']
|
||||
|
||||
server.arcscore.arc_all_post(user_id, scores_data, clearlamps_data, clearedsongs_data,
|
||||
unlocklist_data, installid_data, devicemodelname_data, story_data)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/purchase/me/redeem'), methods=['POST']) # 兑换码
|
||||
@server.auth.auth_required(request)
|
||||
def redeem(user_id):
|
||||
code = request.form['code']
|
||||
fragment, error_code = server.arcpurchase.claim_user_redeem(
|
||||
user_id, code)
|
||||
if not error_code:
|
||||
if fragment > 0:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {"coupon": "fragment"+str(fragment)}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {"coupon": ""}
|
||||
})
|
||||
else:
|
||||
return error_return(error_code)
|
||||
|
||||
|
||||
# 礼物确认
|
||||
@app.route(add_url_prefix('/present/me/claim/<present_id>'), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def claim_present(user_id, present_id):
|
||||
flag = server.arcpurchase.claim_user_present(user_id, present_id)
|
||||
if flag:
|
||||
return jsonify({
|
||||
"success": True
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
# 购买体力
|
||||
@app.route(add_url_prefix('/purchase/me/stamina/<buy_stamina_type>'), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def purchase_stamina(user_id, buy_stamina_type):
|
||||
|
||||
if buy_stamina_type == 'fragment':
|
||||
r, error_code = server.arcworld.buy_stamina_by_fragment(user_id)
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
if error_code:
|
||||
return error_return(error_code)
|
||||
else:
|
||||
if r:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": r
|
||||
})
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
# 购买,world模式boost
|
||||
@app.route(add_url_prefix('/purchase/me/item'), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def prog_boost(user_id):
|
||||
re = {"success": False}
|
||||
if 'item_id' in request.form:
|
||||
if request.form['item_id'] == 'prog_boost_300':
|
||||
ticket, error_code = server.arcpurchase.get_prog_boost(user_id)
|
||||
if error_code:
|
||||
return error_return(error_code)
|
||||
|
||||
re = {
|
||||
"success": True,
|
||||
"value": {'ticket': ticket}
|
||||
}
|
||||
|
||||
elif request.form['item_id'] == 'stamina6':
|
||||
r, error_code = server.arcworld.buy_stamina_by_ticket(user_id)
|
||||
if error_code:
|
||||
return error_return(error_code)
|
||||
|
||||
re = {
|
||||
"success": True,
|
||||
"value": r
|
||||
}
|
||||
return jsonify(re)
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/purchase/me/pack'), methods=['POST']) # 曲包和单曲购买
|
||||
@server.auth.auth_required(request)
|
||||
def pack(user_id):
|
||||
if 'pack_id' in request.form:
|
||||
return jsonify(server.arcpurchase.buy_thing(user_id, request.form['pack_id']))
|
||||
if 'single_id' in request.form:
|
||||
return jsonify(server.arcpurchase.buy_thing(user_id, request.form['single_id']))
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
# 单曲购买信息获取
|
||||
@app.route(add_url_prefix('/purchase/bundle/single'), methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def single(user_id):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": server.arcpurchase.get_single_purchase(user_id)
|
||||
})
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/world/map/me'), methods=['GET']) # 获得世界模式信息,所有地图
|
||||
@server.auth.auth_required(request)
|
||||
def world_all(user_id):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"current_map": server.arcworld.get_current_map(user_id),
|
||||
"user_id": user_id,
|
||||
"maps": server.arcworld.get_world_all(user_id)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/world/map/me'), methods=['POST']) # 进入地图
|
||||
@server.auth.auth_required(request)
|
||||
def world_in(user_id):
|
||||
map_id = request.form['map_id']
|
||||
flag = server.arcworld.unlock_user_world(user_id, map_id)
|
||||
return jsonify({
|
||||
"success": flag,
|
||||
"value": server.arcworld.get_user_world(user_id, map_id)
|
||||
})
|
||||
|
||||
|
||||
# 获得单个地图完整信息
|
||||
@app.route(add_url_prefix('/world/map/me/<map_id>'), methods=['GET'])
|
||||
@server.auth.auth_required(request)
|
||||
def world_one(user_id, map_id):
|
||||
server.arcworld.change_user_current_map(user_id, map_id)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": {
|
||||
"user_id": user_id,
|
||||
"current_map": map_id,
|
||||
"maps": [server.arcworld.get_user_world_info(user_id, map_id)]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/serve/download/me/song'), methods=['GET']) # 歌曲下载
|
||||
@server.auth.auth_required(request)
|
||||
def download_song(user_id):
|
||||
song_ids = request.args.getlist('sid')
|
||||
url_flag = json.loads(request.args.get('url', 'true'))
|
||||
if server.arcdownload.is_able_download(user_id) or not url_flag:
|
||||
re = {}
|
||||
if not song_ids:
|
||||
re = server.arcdownload.get_all_songs(user_id, url_flag=url_flag)
|
||||
else:
|
||||
re = server.arcdownload.get_some_songs(user_id, song_ids)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": re
|
||||
})
|
||||
else:
|
||||
return error_return(903)
|
||||
|
||||
|
||||
@app.route('/download/<path:file_path>', methods=['GET']) # 下载
|
||||
def download(file_path):
|
||||
try:
|
||||
t = request.args.get('t')
|
||||
message = server.arcdownload.is_token_able_download(t, file_path)
|
||||
if message == 0:
|
||||
path = os.path.join('./database/songs', file_path)
|
||||
if os.path.isfile(path) and not('../' in path or '..\\' in path):
|
||||
return send_from_directory('./database/songs', file_path, as_attachment=True)
|
||||
else:
|
||||
return error_return(109)
|
||||
else:
|
||||
return error_return(message)
|
||||
except:
|
||||
return error_return(108)
|
||||
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()
|
||||
|
||||
|
||||
# 创建房间
|
||||
@app.route(add_url_prefix('/multiplayer/me/room/create'), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def room_create(user_id):
|
||||
if not Config.UDP_PORT or Config.UDP_PORT == '':
|
||||
return error_return(151), 404
|
||||
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
|
||||
|
||||
client_song_map = request.json['clientSongMap']
|
||||
error_code, value = server.arclinkplay.create_room(
|
||||
conn1, user_id, client_song_map)
|
||||
|
||||
if error_code == 0:
|
||||
if Config.LINK_PLAY_HOST == '':
|
||||
value['endPoint'] = request.host.split(':')[0]
|
||||
else:
|
||||
value['endPoint'] = Config.LINK_PLAY_HOST
|
||||
|
||||
value['port'] = int(Config.UDP_PORT)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": value
|
||||
})
|
||||
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:
|
||||
return error_return(error_code), 400
|
||||
|
||||
|
||||
# 加入房间
|
||||
@app.route(add_url_prefix('/multiplayer/me/room/join/<room_code>'), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def room_join(user_id, room_code):
|
||||
if not Config.UDP_PORT or Config.UDP_PORT == '':
|
||||
return error_return(151), 404
|
||||
|
||||
client_song_map = request.json['clientSongMap']
|
||||
error_code, value = server.arclinkplay.join_room(
|
||||
conn1, user_id, client_song_map, room_code)
|
||||
|
||||
if error_code == 0:
|
||||
if Config.LINK_PLAY_HOST == '':
|
||||
value['endPoint'] = request.host.split(':')[0]
|
||||
if Config.SSL_CERT and Config.SSL_KEY:
|
||||
app.run(Config.HOST, Config.PORT, ssl_context=(
|
||||
Config.SSL_CERT, Config.SSL_KEY))
|
||||
else:
|
||||
value['endPoint'] = Config.LINK_PLAY_HOST
|
||||
|
||||
value['port'] = int(Config.UDP_PORT)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": value
|
||||
})
|
||||
else:
|
||||
return error_return(error_code), 400
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/multiplayer/me/update'), methods=['POST']) # 更新房间
|
||||
@server.auth.auth_required(request)
|
||||
def multiplayer_update(user_id):
|
||||
if not Config.UDP_PORT or Config.UDP_PORT == '':
|
||||
return error_return(151), 404
|
||||
|
||||
token = request.json['token']
|
||||
error_code, value = server.arclinkplay.update_room(conn1, user_id, token)
|
||||
|
||||
if error_code == 0:
|
||||
if Config.LINK_PLAY_HOST == '':
|
||||
value['endPoint'] = request.host.split(':')[0]
|
||||
else:
|
||||
value['endPoint'] = Config.LINK_PLAY_HOST
|
||||
|
||||
value['port'] = int(Config.UDP_PORT)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": value
|
||||
})
|
||||
else:
|
||||
return error_return(error_code), 400
|
||||
|
||||
|
||||
@app.route(add_url_prefix('/user/me/request_delete'), methods=['POST']) # 删除账号
|
||||
@server.auth.auth_required(request)
|
||||
def user_delete(user_id):
|
||||
return error_return(151), 404
|
||||
|
||||
|
||||
# 三个设置,写在最后降低优先级
|
||||
@app.route(add_url_prefix('/<path:path>', True), methods=['POST'])
|
||||
@server.auth.auth_required(request)
|
||||
def sys_set(user_id, path):
|
||||
set_arg = path[5:]
|
||||
value = request.form['value']
|
||||
server.setme.arc_sys_set(user_id, value, set_arg)
|
||||
r = server.info.get_user_me_c(user_id)
|
||||
if r:
|
||||
return success_return(r)
|
||||
else:
|
||||
return error_return(108)
|
||||
|
||||
|
||||
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}
|
||||
app.run(Config.HOST, Config.PORT)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -759,8 +157,8 @@ def main():
|
||||
}
|
||||
}
|
||||
}
|
||||
if Config.ALLOW_LOG_INFO:
|
||||
log_dict['root']['handlers'] = ['wsgi', 'info_file', 'error_file']
|
||||
if Config.ALLOW_INFO_LOG:
|
||||
log_dict['root']['handlers'].append('info_file')
|
||||
log_dict['handlers']['info_file'] = {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"maxBytes": 1024 * 1024,
|
||||
@@ -770,42 +168,45 @@ def main():
|
||||
"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 server.init.check_before_run(app):
|
||||
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 data in 'songfile' table...")
|
||||
app.logger.info("Start to initialize song data...")
|
||||
try:
|
||||
error = server.arcdownload.initialize_songfile()
|
||||
except:
|
||||
error = 'Something wrong.'
|
||||
if error:
|
||||
app.logger.warning(error)
|
||||
else:
|
||||
initialize_songfile()
|
||||
app.logger.info('Complete!')
|
||||
except:
|
||||
app.logger.warning('Initialization error!')
|
||||
|
||||
if Config.UDP_PORT and Config.UDP_PORT != '':
|
||||
from udpserver.udp_main import link_play
|
||||
if Config.LINKPLAY_HOST and Config.SET_LINKPLAY_SERVER_AS_SUB_PROCESS:
|
||||
from linkplay_server import link_play
|
||||
process = [Process(target=link_play, args=(
|
||||
conn2, Config.HOST, int(Config.UDP_PORT)))]
|
||||
Config.LINKPLAY_HOST, int(Config.LINKPLAY_UDP_PORT), int(Config.LINKPLAY_TCP_PORT)))]
|
||||
[p.start() for p in process]
|
||||
app.logger.info("UDP server is running...")
|
||||
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)
|
||||
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:
|
||||
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)
|
||||
tcp_server_run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
flask>=2.0
|
||||
cryptography
|
||||
flask>=2.0.2
|
||||
cryptography>=3.0.0
|
||||
limits>=2.7.0
|
||||
|
||||
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()
|
||||
@@ -1,8 +1,17 @@
|
||||
from core.config_manager import Config
|
||||
from flask import Blueprint
|
||||
from setting import Config
|
||||
from . import user
|
||||
from . import auth
|
||||
|
||||
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(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)
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import os
|
||||
import hashlib
|
||||
from flask import url_for
|
||||
import sqlite3
|
||||
from server.sql import Connect
|
||||
import time
|
||||
from setting import Config
|
||||
|
||||
time_limit = Config.DOWNLOAD_TIMES_LIMIT # 每个玩家24小时下载次数限制
|
||||
time_gap_limit = Config.DOWNLOAD_TIME_GAP_LIMIT # 下载链接有效秒数
|
||||
|
||||
|
||||
def get_file_md5(file_path):
|
||||
# 计算文件MD5
|
||||
if not os.path.isfile(file_path):
|
||||
return None
|
||||
myhash = hashlib.md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
b = f.read(8096)
|
||||
if not b:
|
||||
break
|
||||
myhash.update(b)
|
||||
|
||||
return myhash.hexdigest()
|
||||
|
||||
|
||||
def get_url(file_path, **kwargs):
|
||||
# 获取下载地址
|
||||
|
||||
t = ''
|
||||
if 't' in kwargs:
|
||||
t = kwargs['t']
|
||||
|
||||
if Config.DOWNLOAD_LINK_PREFIX:
|
||||
prefix = Config.DOWNLOAD_LINK_PREFIX
|
||||
if prefix[-1] != '/':
|
||||
prefix += '/'
|
||||
|
||||
return prefix + file_path + '?t=' + t
|
||||
else:
|
||||
return url_for('download', file_path=file_path, t=t, _external=True)
|
||||
|
||||
|
||||
def get_one_song(c, user_id, song_id, file_dir='./database/songs', url_flag=True):
|
||||
# 获取一首歌的下载链接,返回字典
|
||||
dir_list = os.listdir(os.path.join(file_dir, song_id))
|
||||
re = {}
|
||||
now = int(time.time())
|
||||
c.execute('''delete from download_token where user_id=:a and song_id=:b''', {
|
||||
'a': user_id, 'b': song_id})
|
||||
|
||||
for i in dir_list:
|
||||
if os.path.isfile(os.path.join(file_dir, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg', '3.ogg']:
|
||||
token = hashlib.md5(
|
||||
(str(user_id) + song_id + i + str(now)).encode(encoding='UTF-8')).hexdigest()
|
||||
|
||||
if i == 'base.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
c.execute(
|
||||
'''select md5 from songfile where song_id=:a and file_type=-1''', {'a': song_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
checksum = x[0]
|
||||
else:
|
||||
checksum = get_file_md5(os.path.join(
|
||||
file_dir, song_id, 'base.ogg'))
|
||||
|
||||
re['audio']["checksum"] = checksum
|
||||
if url_flag:
|
||||
re['audio']["url"] = get_url(
|
||||
file_path=song_id+'/base.ogg', t=token)
|
||||
|
||||
elif i == '3.ogg':
|
||||
if 'audio' not in re:
|
||||
re['audio'] = {}
|
||||
|
||||
c.execute(
|
||||
'''select md5 from songfile where song_id=:a and file_type=-2''', {'a': song_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
checksum = x[0]
|
||||
else:
|
||||
checksum = get_file_md5(os.path.join(
|
||||
file_dir, song_id, '3.ogg'))
|
||||
|
||||
if url_flag:
|
||||
re['audio']['3'] = {"checksum": checksum, "url": get_url(
|
||||
file_path=song_id+'/3.ogg', t=token)}
|
||||
else:
|
||||
re['audio']['3'] = {"checksum": checksum}
|
||||
else:
|
||||
if 'chart' not in re:
|
||||
re['chart'] = {}
|
||||
c.execute(
|
||||
'''select md5 from songfile where song_id=:a and file_type=:b''', {'a': song_id, 'b': int(i[0])})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
checksum = x[0]
|
||||
else:
|
||||
checksum = get_file_md5(os.path.join(file_dir, song_id, i))
|
||||
|
||||
if url_flag:
|
||||
re['chart'][i[0]] = {"checksum": checksum, "url": get_url(
|
||||
file_path=song_id+'/'+i, t=token)}
|
||||
else:
|
||||
re['chart'][i[0]] = {"checksum": checksum}
|
||||
|
||||
if url_flag:
|
||||
c.execute('''insert into download_token values(:a,:b,:c,:d,:e)''', {
|
||||
'a': user_id, 'b': song_id, 'c': i, 'd': token, 'e': now})
|
||||
|
||||
return {song_id: re}
|
||||
|
||||
|
||||
def get_all_songs(user_id, file_dir='./database/songs', url_flag=True):
|
||||
# 获取所有歌的下载链接,返回字典
|
||||
dir_list = os.listdir(file_dir)
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
for i in dir_list:
|
||||
if os.path.isdir(os.path.join(file_dir, i)):
|
||||
re.update(get_one_song(c, user_id, i, file_dir, url_flag))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_some_songs(user_id, song_ids):
|
||||
# 获取一些歌的下载链接,返回字典
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
for song_id in song_ids:
|
||||
re.update(get_one_song(c, user_id, song_id))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def is_token_able_download(t, path):
|
||||
# token是否可以下载,返回错误码,0即可以
|
||||
errorcode = 108
|
||||
with Connect() as c:
|
||||
c.execute('''select * from download_token where token = :t limit 1''',
|
||||
{'t': t})
|
||||
x = c.fetchone()
|
||||
now = int(time.time())
|
||||
if x and now - x[4] <= time_gap_limit and x[1]+'/'+x[2] == path:
|
||||
c.execute(
|
||||
'''select count(*) from user_download where user_id = :a''', {'a': x[0]})
|
||||
y = c.fetchone()
|
||||
if y and y[0] <= time_limit:
|
||||
c.execute('''insert into user_download values(:a,:b,:c)''', {
|
||||
'a': x[0], 'b': x[3], 'c': now})
|
||||
errorcode = 0
|
||||
else:
|
||||
errorcode = 903
|
||||
else:
|
||||
errorcode = 108
|
||||
|
||||
return errorcode
|
||||
|
||||
|
||||
def is_able_download(user_id):
|
||||
# 是否可以下载,返回布尔值
|
||||
f = False
|
||||
with Connect() as c:
|
||||
now = int(time.time())
|
||||
c.execute(
|
||||
'''delete from user_download where user_id = :a and time <= :b''', {'a': user_id, 'b': now - 24*3600})
|
||||
c.execute(
|
||||
'''select count(*) from user_download where user_id = :a''', {'a': user_id})
|
||||
y = c.fetchone()
|
||||
if y and y[0] <= time_limit:
|
||||
f = True
|
||||
else:
|
||||
f = False
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def initialize_one_songfile(c, song_id, file_dir='./database/songs'):
|
||||
# 计算并添加歌曲md5到表中,无返回
|
||||
dir_list = os.listdir(os.path.join(file_dir, song_id))
|
||||
for i in dir_list:
|
||||
if os.path.isfile(os.path.join(file_dir, song_id, i)) and i in ['0.aff', '1.aff', '2.aff', '3.aff', 'base.ogg', '3.ogg']:
|
||||
if i == 'base.ogg':
|
||||
c.execute('''insert into songfile values(:a,-1,:c)''', {
|
||||
'a': song_id, 'c': get_file_md5(os.path.join(file_dir, song_id, 'base.ogg'))})
|
||||
elif i == '3.ogg':
|
||||
c.execute('''insert into songfile values(:a,-2,:c)''', {
|
||||
'a': song_id, 'c': get_file_md5(os.path.join(file_dir, song_id, '3.ogg'))})
|
||||
else:
|
||||
c.execute('''insert into songfile values(:a,:b,:c)''', {
|
||||
'a': song_id, 'b': int(i[0]), 'c': get_file_md5(os.path.join(file_dir, song_id, i))})
|
||||
|
||||
return
|
||||
|
||||
|
||||
def initialize_songfile(file_dir='./database/songs'):
|
||||
# 初始化歌曲数据的md5表,返回错误信息
|
||||
error = None
|
||||
try:
|
||||
dir_list = os.listdir(file_dir)
|
||||
except:
|
||||
error = 'OS error!'
|
||||
return error
|
||||
try:
|
||||
conn = sqlite3.connect('./database/arcaea_database.db')
|
||||
c = conn.cursor()
|
||||
except:
|
||||
error = 'Database error!'
|
||||
return error
|
||||
try:
|
||||
c.execute('''delete from songfile''')
|
||||
for i in dir_list:
|
||||
if os.path.isdir(os.path.join(file_dir, i)):
|
||||
initialize_one_songfile(c, i, file_dir)
|
||||
except:
|
||||
error = 'Initialization error!'
|
||||
finally:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return error
|
||||
@@ -1,125 +0,0 @@
|
||||
from .sql import Connect
|
||||
import base64
|
||||
|
||||
|
||||
def get_song_unlock(client_song_map):
|
||||
# 处理可用歌曲bit,返回bytes
|
||||
|
||||
user_song_unlock = [0] * 512
|
||||
for i in range(0, 1024, 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)
|
||||
|
||||
|
||||
def create_room(conn, user_id, client_song_map):
|
||||
# 创建房间,返回错误码和房间与用户信息
|
||||
error_code = 108
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select name from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
name = x[0]
|
||||
|
||||
song_unlock = get_song_unlock(client_song_map)
|
||||
|
||||
conn.send((1, name, song_unlock))
|
||||
if conn.poll(10):
|
||||
data = conn.recv()
|
||||
else:
|
||||
data = (-1,)
|
||||
|
||||
if data[0] == 0:
|
||||
error_code = 0
|
||||
return error_code, {'roomCode': data[1],
|
||||
'roomId': str(data[2]),
|
||||
'token': str(data[3]),
|
||||
'key': (base64.b64encode(data[4])).decode(),
|
||||
'playerId': str(data[5]),
|
||||
'userId': user_id,
|
||||
'orderedAllowedSongs': (base64.b64encode(song_unlock)).decode()
|
||||
}
|
||||
|
||||
return error_code, None
|
||||
|
||||
|
||||
def join_room(conn, user_id, client_song_map, room_code):
|
||||
# 加入房间,返回错误码和房间与用户信息
|
||||
error_code = 108
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select name from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
name = x[0]
|
||||
|
||||
song_unlock = get_song_unlock(client_song_map)
|
||||
|
||||
conn.send((2, name, song_unlock, room_code))
|
||||
if conn.poll(10):
|
||||
data = conn.recv()
|
||||
else:
|
||||
data = (-1,)
|
||||
|
||||
if data[0] == 0:
|
||||
error_code = 0
|
||||
return error_code, {'roomCode': data[1],
|
||||
'roomId': str(data[2]),
|
||||
'token': str(data[3]),
|
||||
'key': (base64.b64encode(data[4])).decode(),
|
||||
'playerId': str(data[5]),
|
||||
'userId': user_id,
|
||||
'orderedAllowedSongs': (base64.b64encode(data[6])).decode()
|
||||
}
|
||||
else:
|
||||
error_code = data[0]
|
||||
|
||||
return error_code, None
|
||||
|
||||
|
||||
def update_room(conn, user_id, token):
|
||||
# 更新房间,返回错误码和房间与用户信息
|
||||
error_code = 108
|
||||
|
||||
conn.send((3, int(token)))
|
||||
if conn.poll(10):
|
||||
data = conn.recv()
|
||||
else:
|
||||
data = (-1,)
|
||||
|
||||
if data[0] == 0:
|
||||
error_code = 0
|
||||
return error_code, {'roomCode': data[1],
|
||||
'roomId': str(data[2]),
|
||||
'token': token,
|
||||
'key': (base64.b64encode(data[3])).decode(),
|
||||
'playerId': str(data[4]),
|
||||
'userId': user_id,
|
||||
'orderedAllowedSongs': (base64.b64encode(data[5])).decode()
|
||||
}
|
||||
else:
|
||||
error_code = data[0]
|
||||
|
||||
return error_code, None
|
||||
@@ -1,310 +0,0 @@
|
||||
from server.sql import Connect
|
||||
import server.item
|
||||
import server.character
|
||||
import time
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_purchase(c, user_id, type='pack'):
|
||||
# 读取packs内容,返回字典列表
|
||||
c.execute(
|
||||
'''select * from purchase where purchase_name in (select purchase_name from purchase_item where type = :a)''', {'a': type})
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return []
|
||||
|
||||
re = []
|
||||
for i in x:
|
||||
items = []
|
||||
c.execute(
|
||||
'''select a.*, b.amount from item a, purchase_item b where a.item_id=b.item_id and a.type=b.type and b.purchase_name=:name''', {'name': i[0]})
|
||||
y = c.fetchall()
|
||||
t = None
|
||||
if y:
|
||||
for j in y:
|
||||
if j[3]:
|
||||
amount = j[3]
|
||||
else:
|
||||
amount = 1
|
||||
if i[0] == j[0]:
|
||||
# 物品排序,否则客户端报错
|
||||
t = {
|
||||
"type": j[1],
|
||||
"id": j[0],
|
||||
"is_available": int2b(j[2]),
|
||||
'amount': amount
|
||||
}
|
||||
else:
|
||||
items.append({
|
||||
"type": j[1],
|
||||
"id": j[0],
|
||||
"is_available": int2b(j[2]),
|
||||
"amount": amount
|
||||
})
|
||||
|
||||
if t is not None:
|
||||
# 放到列表头
|
||||
items = [t, items]
|
||||
|
||||
r = {"name": i[0],
|
||||
"items": items,
|
||||
"price": i[1],
|
||||
"orig_price": i[2]}
|
||||
|
||||
if i[3] > 0:
|
||||
r['discount_from'] = i[3]
|
||||
if i[4] > 0:
|
||||
r['discount_to'] = i[4]
|
||||
|
||||
if i[5] == 'anni5tix' and i[3] <= int(time.time() * 1000) <= i[4]:
|
||||
c.execute(
|
||||
'''select amount from user_item where user_id=? and item_id="anni5tix"''', (user_id,))
|
||||
z = c.fetchone()
|
||||
if z and z[0] >= 1:
|
||||
r['discount_reason'] = 'anni5tix'
|
||||
r['price'] = 0
|
||||
|
||||
re.append(r)
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_single_purchase(user_id):
|
||||
# main里面没开数据库,这里写一下代替
|
||||
re = []
|
||||
with Connect() as c:
|
||||
re = get_purchase(c, user_id, 'single')
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def buy_item(c, user_id, price):
|
||||
# 购买接口,返回成功与否标识符和剩余源点数
|
||||
c.execute('''select ticket from user where user_id = :a''',
|
||||
{'a': user_id})
|
||||
ticket = c.fetchone()
|
||||
if ticket:
|
||||
ticket = ticket[0]
|
||||
else:
|
||||
ticket = 0
|
||||
|
||||
if ticket < price:
|
||||
return False, ticket
|
||||
|
||||
c.execute('''update user set ticket = :b where user_id = :a''',
|
||||
{'a': user_id, 'b': ticket-price})
|
||||
|
||||
return True, ticket - price
|
||||
|
||||
|
||||
def buy_item_with_anni5tix(c, user_id):
|
||||
# 兑换券购买接口,返回成功与否标志
|
||||
c.execute('''select amount from user_item where user_id = :a and item_id = "anni5tix"''',
|
||||
{'a': user_id})
|
||||
amount = c.fetchone()
|
||||
if amount:
|
||||
amount = amount[0]
|
||||
else:
|
||||
return False
|
||||
|
||||
if amount <= 0:
|
||||
return False
|
||||
|
||||
c.execute('''update user_item set amount = :b where user_id = :a and item_id = "anni5tix"''',
|
||||
{'a': user_id, 'b': amount-1})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def buy_thing(user_id, purchase_id):
|
||||
# 购买物品接口,返回字典
|
||||
success_flag = False
|
||||
ticket = 0
|
||||
packs = []
|
||||
singles = []
|
||||
characters = []
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select price, orig_price, discount_from, discount_to, discount_reason from purchase where purchase_name=:a''',
|
||||
{'a': purchase_id})
|
||||
x = c.fetchone()
|
||||
price = 0
|
||||
flag = False
|
||||
if x:
|
||||
price = x[0]
|
||||
orig_price = x[1]
|
||||
discount_from = x[2]
|
||||
discount_to = x[3]
|
||||
discount_reason = x[4]
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error_code": 501
|
||||
}
|
||||
|
||||
c.execute(
|
||||
'''select item_id, type, amount from purchase_item where purchase_name=:a''', {'a': purchase_id})
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
now = int(time.time() * 1000)
|
||||
if not(discount_from <= now <= discount_to):
|
||||
price = orig_price
|
||||
elif discount_reason == 'anni5tix' and buy_item_with_anni5tix(c, user_id):
|
||||
price = 0
|
||||
|
||||
flag, ticket = buy_item(c, user_id, price)
|
||||
|
||||
if flag:
|
||||
for i in x:
|
||||
if i[2]:
|
||||
amount = i[2]
|
||||
else:
|
||||
amount = 1
|
||||
server.item.claim_user_item(c, user_id, i[0], i[1], amount)
|
||||
|
||||
success_flag = True
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error_code": 501
|
||||
}
|
||||
|
||||
packs = server.item.get_user_items(c, user_id, 'pack')
|
||||
singles = server.item.get_user_items(c, user_id, 'single')
|
||||
characters = server.character.get_user_characters(c, user_id)
|
||||
|
||||
return {
|
||||
"success": success_flag,
|
||||
"value": {'user_id': user_id,
|
||||
'ticket': ticket,
|
||||
'packs': packs,
|
||||
'singles': singles,
|
||||
'characters': characters
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_prog_boost(user_id):
|
||||
# 世界模式源韵强化,扣50源点,返回剩余源点数
|
||||
|
||||
ticket = -1
|
||||
with Connect() as c:
|
||||
flag, ticket = buy_item(c, user_id, 50)
|
||||
|
||||
if flag:
|
||||
c.execute('''update user set prog_boost = 1 where user_id = :a''', {
|
||||
'a': user_id})
|
||||
if ticket >= 0:
|
||||
return ticket, None
|
||||
else:
|
||||
return 0, 108
|
||||
|
||||
|
||||
def get_user_present(c, user_id):
|
||||
# 获取用户奖励,返回字典列表
|
||||
c.execute(
|
||||
'''select * from present where present_id in (select present_id from user_present where user_id=:a)''', {'a': user_id})
|
||||
x = c.fetchall()
|
||||
re = []
|
||||
now = int(time.time() * 1000)
|
||||
if x:
|
||||
for i in x:
|
||||
if now <= int(i[1]):
|
||||
c.execute(
|
||||
'''select * from present_item where present_id=?''', (i[0],))
|
||||
y = c.fetchall()
|
||||
items = []
|
||||
if y:
|
||||
for j in y:
|
||||
if j is not None:
|
||||
items.append({
|
||||
"type": j[2],
|
||||
"id": j[1],
|
||||
"amount": j[3]
|
||||
})
|
||||
re.append({'expire_ts': i[1],
|
||||
'description': i[2],
|
||||
'present_id': i[0],
|
||||
'items': items
|
||||
})
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def claim_user_present(user_id, present_id):
|
||||
# 确认并删除用户奖励,返回成功与否的布尔值
|
||||
flag = False
|
||||
with Connect() as c:
|
||||
c.execute('''select exists(select * from user_present where user_id=:a and present_id=:b)''',
|
||||
{'a': user_id, 'b': present_id})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''delete from user_present where user_id=:a and present_id=:b''',
|
||||
{'a': user_id, 'b': present_id})
|
||||
c.execute('''select * from present where present_id=:b''',
|
||||
{'b': present_id})
|
||||
x = c.fetchone()
|
||||
now = int(time.time() * 1000)
|
||||
if now <= int(x[1]):
|
||||
# 处理memory
|
||||
c.execute(
|
||||
'''select * from present_item where present_id=?''', (x[0],))
|
||||
y = c.fetchall()
|
||||
flag = True
|
||||
if y:
|
||||
for j in y:
|
||||
if j is not None:
|
||||
flag = flag and server.item.claim_user_item(
|
||||
c, user_id, j[1], j[2], j[3])
|
||||
|
||||
else:
|
||||
# 过期
|
||||
flag = False
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
def claim_user_redeem(user_id, code):
|
||||
# 处理兑换码,返回碎片数量和错误码
|
||||
fragment = 0
|
||||
error_code = 108
|
||||
with Connect() as c:
|
||||
c.execute('''select * from redeem where code=:a''', {'a': code})
|
||||
x = c.fetchone()
|
||||
if not x:
|
||||
return 0, 504
|
||||
|
||||
if x[1] == 0: # 一次性
|
||||
c.execute(
|
||||
'''select exists(select * from user_redeem where code=:a)''', {'a': code})
|
||||
if c.fetchone() == (1,):
|
||||
return 0, 505
|
||||
elif x[1] == 1: # 每个玩家一次
|
||||
c.execute('''select exists(select * from user_redeem where code=:a and user_id=:b)''',
|
||||
{'a': code, 'b': user_id})
|
||||
if c.fetchone() == (1,):
|
||||
return 0, 506
|
||||
|
||||
c.execute('''insert into user_redeem values(:b,:a)''',
|
||||
{'a': code, 'b': user_id})
|
||||
|
||||
c.execute('''select * from redeem_item where code=?''', (code,))
|
||||
x = c.fetchall()
|
||||
flag = True
|
||||
if x:
|
||||
for i in x:
|
||||
if i[2] == 'fragment':
|
||||
fragment += i[3]
|
||||
else:
|
||||
flag = flag and server.item.claim_user_item(
|
||||
c, user_id, i[1], i[2], i[3])
|
||||
if flag:
|
||||
error_code = None
|
||||
|
||||
return fragment, error_code
|
||||
@@ -1,202 +1,4 @@
|
||||
from server.config import Constant
|
||||
from server.sql import Connect
|
||||
import time
|
||||
import json
|
||||
import server.arcworld
|
||||
import hashlib
|
||||
from setting import Config
|
||||
|
||||
|
||||
def b2int(x):
|
||||
# int与布尔值转换
|
||||
if x:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def md5(code):
|
||||
# md5加密算法
|
||||
code = code.encode()
|
||||
md5s = hashlib.md5()
|
||||
md5s.update(code)
|
||||
codes = md5s.hexdigest()
|
||||
|
||||
return codes
|
||||
|
||||
|
||||
def get_score(c, user_id, song_id, difficulty):
|
||||
# 根据user_id、song_id、难度得到该曲目最好成绩,返回字典
|
||||
c.execute('''select * from best_score where user_id = :a and song_id = :b and difficulty = :c''',
|
||||
{'a': user_id, 'b': song_id, 'c': difficulty})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
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': user_id})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
character = y[1]
|
||||
is_char_uncapped = int2b(y[3])
|
||||
if y[5] != -1:
|
||||
character = y[5]
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id=:a and character_id=:b''', {
|
||||
'a': user_id, 'b': character})
|
||||
else:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id=:a and character_id=:b''', {
|
||||
'a': user_id, 'b': character})
|
||||
z = c.fetchone()
|
||||
if z:
|
||||
if z[1] == 0:
|
||||
is_char_uncapped = int2b(z[0])
|
||||
else:
|
||||
is_char_uncapped = False
|
||||
else:
|
||||
if y[4] == 1:
|
||||
is_char_uncapped = False
|
||||
|
||||
return {
|
||||
"user_id": x[0],
|
||||
"song_id": x[1],
|
||||
"difficulty": x[2],
|
||||
"score": x[3],
|
||||
"shiny_perfect_count": x[4],
|
||||
"perfect_count": x[5],
|
||||
"near_count": x[6],
|
||||
"miss_count": x[7],
|
||||
"health": x[8],
|
||||
"modifier": x[9],
|
||||
"time_played": x[10],
|
||||
"best_clear_type": x[11],
|
||||
"clear_type": x[12],
|
||||
"name": y[0],
|
||||
"character": character,
|
||||
"is_skill_sealed": int2b(y[2]),
|
||||
"is_char_uncapped": is_char_uncapped
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def arc_score_friend(user_id, song_id, difficulty, limit=50):
|
||||
# 得到用户好友分数表,默认最大50个
|
||||
r = []
|
||||
with Connect() as c:
|
||||
c.execute('''select user_id from best_score where user_id in (select :user_id union select user_id_other from friend where user_id_me = :user_id) and song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
||||
'user_id': user_id, 'song_id': song_id, 'difficulty': difficulty, 'limit': limit})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = 0
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_score_top(song_id, difficulty, limit=20):
|
||||
# 得到top分数表,默认最多20个,如果是负数则全部查询
|
||||
r = []
|
||||
with Connect() as c:
|
||||
if limit >= 0:
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit''', {
|
||||
'song_id': song_id, 'difficulty': difficulty, 'limit': limit})
|
||||
else:
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC''', {
|
||||
'song_id': song_id, 'difficulty': difficulty})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = 0
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_score_me(user_id, song_id, difficulty, limit=20):
|
||||
# 得到用户的排名,默认最大20个
|
||||
r = []
|
||||
with Connect() as c:
|
||||
c.execute('''select exists(select * from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty)''', {
|
||||
'user_id': user_id, 'song_id': song_id, 'difficulty': difficulty})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''select count(*) from best_score where song_id = :song_id and difficulty = :difficulty and (score>(select score from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty) or (score>(select score from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty) and time_played > (select time_played from best_score where user_id = :user_id and song_id = :song_id and difficulty = :difficulty)) )''', {
|
||||
'user_id': user_id, 'song_id': song_id, 'difficulty': difficulty})
|
||||
x = c.fetchone()
|
||||
myrank = int(x[0]) + 1
|
||||
c.execute('''select count(*) from best_score where song_id=:a and difficulty=:b''',
|
||||
{'a': song_id, 'b': difficulty})
|
||||
amount = int(c.fetchone()[0])
|
||||
|
||||
if myrank <= 4: # 排名在前4
|
||||
return arc_score_top(song_id, difficulty, limit)
|
||||
elif myrank >= 5 and myrank <= 9999 - limit + 4 and amount >= 10000: # 万名内,前面有4个人
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||
'song_id': song_id, 'difficulty': difficulty, 'limit': limit, 'offset': myrank - 5})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = myrank - 5
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
|
||||
elif myrank >= 10000: # 万名外
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||
'song_id': song_id, 'difficulty': difficulty, 'limit': limit - 1, 'offset': 9999-limit})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = 9999 - limit
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
y = get_score(c, user_id, song_id, difficulty)
|
||||
y['rank'] = -1
|
||||
r.append(y)
|
||||
elif amount - myrank < limit - 5: # 后方人数不足
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||
'song_id': song_id, 'difficulty': difficulty, 'limit': limit, 'offset': amount - limit})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = amount - limit
|
||||
if rank < 0:
|
||||
rank = 0
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
else:
|
||||
c.execute('''select user_id from best_score where song_id = :song_id and difficulty = :difficulty order by score DESC, time_played DESC limit :limit offset :offset''', {
|
||||
'song_id': song_id, 'difficulty': difficulty, 'limit': limit, 'offset': 9998-limit})
|
||||
x = c.fetchall()
|
||||
if x != []:
|
||||
rank = 9998 - limit
|
||||
for i in x:
|
||||
rank += 1
|
||||
y = get_score(c, i[0], song_id, difficulty)
|
||||
y['rank'] = rank
|
||||
r.append(y)
|
||||
|
||||
return r
|
||||
from core.sql import Connect
|
||||
|
||||
|
||||
def calculate_rating(defnum, score):
|
||||
@@ -213,349 +15,13 @@ def calculate_rating(defnum, score):
|
||||
return ptt
|
||||
|
||||
|
||||
def get_one_ptt(song_id, difficulty, score: int) -> float:
|
||||
# 单曲ptt计算,ptt为负说明没谱面定数数据
|
||||
ptt = -10
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
if difficulty == 0:
|
||||
c.execute('''select rating_pst from songs where sid = :sid;''', {
|
||||
'sid': song_id})
|
||||
elif difficulty == 1:
|
||||
c.execute('''select rating_prs from songs where sid = :sid;''', {
|
||||
'sid': song_id})
|
||||
elif difficulty == 2:
|
||||
c.execute('''select rating_ftr from songs where sid = :sid;''', {
|
||||
'sid': song_id})
|
||||
elif difficulty == 3:
|
||||
c.execute('''select rating_byn from songs where sid = :sid;''', {
|
||||
'sid': song_id})
|
||||
|
||||
x = c.fetchone()
|
||||
defnum = -10 # 没在库里的全部当做定数-10
|
||||
if x is not None and x != '':
|
||||
defnum = float(x[0]) / 10
|
||||
if defnum <= 0:
|
||||
defnum = -10 # 缺少难度的当做定数-10
|
||||
|
||||
ptt = calculate_rating(defnum, score)
|
||||
|
||||
return ptt
|
||||
|
||||
|
||||
def get_song_grade(x):
|
||||
# 成绩转换评级
|
||||
if x >= 9900000: # EX+
|
||||
return 6
|
||||
elif x < 9900000 and x >= 9800000: # EX
|
||||
return 5
|
||||
elif x < 9800000 and x >= 9500000: # AA
|
||||
return 4
|
||||
elif x < 9500000 and x >= 9200000: # A
|
||||
return 3
|
||||
elif x < 9200000 and x >= 8900000: # B
|
||||
return 2
|
||||
elif x < 8900000 and x >= 8600000: # C
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def get_song_state(x):
|
||||
# 返回成绩状态,便于比较
|
||||
if x == 3: # PM
|
||||
return 5
|
||||
elif x == 2: # FC
|
||||
return 4
|
||||
elif x == 5: # Hard Clear
|
||||
return 3
|
||||
elif x == 1: # Clear
|
||||
return 2
|
||||
elif x == 4: # Easy Clear
|
||||
return 1
|
||||
else: # Track Lost
|
||||
return 0
|
||||
|
||||
|
||||
def get_user_ptt_float(c, user_id) -> float:
|
||||
# 总ptt计算,返回浮点数
|
||||
|
||||
sumr = 0
|
||||
c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
|
||||
'a': user_id})
|
||||
x = c.fetchall()
|
||||
if not Config.USE_B10_AS_R10:
|
||||
if x != []:
|
||||
for i in x:
|
||||
sumr += float(i[0])
|
||||
c.execute('''select * from recent30 where user_id = :a''',
|
||||
{'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
r30 = []
|
||||
s30 = []
|
||||
for i in range(1, 61, 2):
|
||||
if x[i] is not None:
|
||||
r30.append(float(x[i]))
|
||||
s30.append(x[i+1])
|
||||
else:
|
||||
r30.append(0)
|
||||
s30.append('')
|
||||
r30, s30 = (list(t)
|
||||
for t in zip(*sorted(zip(r30, s30), reverse=True)))
|
||||
songs = []
|
||||
i = 0
|
||||
while len(songs) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None:
|
||||
if s30[i] not in songs:
|
||||
sumr += r30[i]
|
||||
songs.append(s30[i])
|
||||
i += 1
|
||||
else:
|
||||
if x != []:
|
||||
for i in range(len(x)):
|
||||
t = float(x[i][0])
|
||||
sumr += t
|
||||
if i < 10:
|
||||
sumr += t
|
||||
return sumr/40
|
||||
|
||||
|
||||
def get_user_ptt(c, user_id) -> int:
|
||||
# 总ptt计算,返回4位整数,向下取整
|
||||
|
||||
return int(get_user_ptt_float(c, user_id)*100)
|
||||
|
||||
|
||||
def get_user_world_rank(c, user_id) -> int:
|
||||
# 用户世界排名计算,同时返回排名值,如果超过设定最大值,返回0
|
||||
|
||||
with Connect('./database/arcsong.db') as c2:
|
||||
c2.execute(
|
||||
'''select sid, rating_ftr, rating_byn from songs''')
|
||||
x = c2.fetchall()
|
||||
if x:
|
||||
song_list_ftr = [user_id]
|
||||
song_list_byn = [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])
|
||||
|
||||
if len(song_list_ftr) >= 2:
|
||||
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 = c.fetchone()
|
||||
if x[0] is not None:
|
||||
score_sum = x[0]
|
||||
else:
|
||||
score_sum = 0
|
||||
|
||||
if len(song_list_byn) >= 2:
|
||||
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 = c.fetchone()
|
||||
if x[0] is not None:
|
||||
score_sum += x[0]
|
||||
else:
|
||||
score_sum += 0
|
||||
|
||||
c.execute('''update user set world_rank_score = :b where user_id = :a''', {
|
||||
'a': user_id, 'b': score_sum})
|
||||
|
||||
c.execute(
|
||||
'''select count(*) from user where world_rank_score > ?''', (score_sum,))
|
||||
x = c.fetchone()
|
||||
if x and x[0] + 1 <= Config.WORLD_RANK_MAX:
|
||||
return x[0] + 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def update_recent30(c, user_id, song_id, rating, is_protected):
|
||||
# 刷新r30,这里的判断方法存疑,这里的song_id结尾包含难度数字
|
||||
def insert_r30table(c, user_id, a, b):
|
||||
# 更新r30表
|
||||
c.execute('''delete from recent30 where user_id = :a''',
|
||||
{'a': user_id})
|
||||
sql = 'insert into recent30 values(' + str(user_id)
|
||||
for i in range(0, 30):
|
||||
if a[i] is not None and b[i] is not None:
|
||||
sql = sql + ',' + str(a[i]) + ',"' + b[i] + '"'
|
||||
else:
|
||||
sql = sql + ',0,""'
|
||||
|
||||
sql = sql + ')'
|
||||
c.execute(sql)
|
||||
|
||||
c.execute('''select * from recent30 where user_id = :a''', {'a': user_id})
|
||||
x = c.fetchone()
|
||||
if not x:
|
||||
x = [None] * 61
|
||||
x[0] = user_id
|
||||
for i in range(2, 61, 2):
|
||||
x[i] = ''
|
||||
songs = []
|
||||
flag = True
|
||||
for i in range(2, 61, 2):
|
||||
if x[i] is None or x[i] == '':
|
||||
r30_id = 29
|
||||
flag = False
|
||||
break
|
||||
if x[i] not in songs:
|
||||
songs.append(x[i])
|
||||
if flag:
|
||||
n = len(songs)
|
||||
if n >= 11:
|
||||
r30_id = 29
|
||||
elif song_id not in songs and n == 10:
|
||||
r30_id = 29
|
||||
elif song_id in songs and n == 10:
|
||||
i = 29
|
||||
while x[i*2+2] == song_id:
|
||||
i -= 1
|
||||
r30_id = i
|
||||
elif song_id not in songs and n == 9:
|
||||
i = 29
|
||||
while x[i*2+2] == song_id:
|
||||
i -= 1
|
||||
r30_id = i
|
||||
else:
|
||||
r30_id = 29
|
||||
a = []
|
||||
b = []
|
||||
for i in range(1, 61, 2):
|
||||
a.append(x[i])
|
||||
b.append(x[i+1])
|
||||
|
||||
if is_protected:
|
||||
ptt_pre = get_user_ptt_float(c, user_id)
|
||||
a_pre = [x for x in a]
|
||||
b_pre = [x for x in b]
|
||||
|
||||
for i in range(r30_id, 0, -1):
|
||||
a[i] = a[i-1]
|
||||
b[i] = b[i-1]
|
||||
a[0] = rating
|
||||
b[0] = song_id
|
||||
|
||||
insert_r30table(c, user_id, a, b)
|
||||
|
||||
if is_protected:
|
||||
ptt = get_user_ptt_float(c, user_id)
|
||||
if ptt < ptt_pre:
|
||||
# 触发保护
|
||||
if song_id in b_pre:
|
||||
for i in range(29, -1, -1):
|
||||
if song_id == b_pre[i] and rating > a_pre[i]:
|
||||
# 发现重复歌曲,更新到最高rating
|
||||
a_pre[i] = rating
|
||||
break
|
||||
|
||||
insert_r30table(c, user_id, a_pre, b_pre)
|
||||
return None
|
||||
|
||||
|
||||
def arc_score_post(user_id, song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type):
|
||||
# 分数上传,返回变化后的ptt,和世界模式变化
|
||||
ptt = None
|
||||
re = None
|
||||
with Connect() as c:
|
||||
rating = get_one_ptt(song_id, difficulty, score)
|
||||
if rating < 0: # 没数据不会向recent30里记入
|
||||
unrank_flag = True
|
||||
rating = 0
|
||||
else:
|
||||
unrank_flag = False
|
||||
now = int(time.time() * 1000)
|
||||
# recent 更新
|
||||
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': user_id, 'b': song_id, 'c': difficulty, 'd': score, 'e': shiny_perfect_count, 'f': perfect_count, 'g': near_count, 'h': miss_count, 'i': health, 'j': modifier, 'k': clear_type, 'l': rating, 'm': now})
|
||||
# 成绩录入
|
||||
c.execute('''select score, best_clear_type from best_score where user_id = :a and song_id = :b and difficulty = :c''', {
|
||||
'a': user_id, 'b': song_id, 'c': difficulty})
|
||||
now = int(now // 1000)
|
||||
x = c.fetchone()
|
||||
if x is None:
|
||||
first_protect_flag = True # 初见保护
|
||||
c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', {
|
||||
'a': user_id, 'b': song_id, 'c': difficulty, 'd': score, 'e': shiny_perfect_count, 'f': perfect_count, 'g': near_count, 'h': miss_count, 'i': health, 'j': modifier, 'k': now, 'l': clear_type, 'm': clear_type, 'n': rating})
|
||||
else:
|
||||
first_protect_flag = False
|
||||
if get_song_state(clear_type) > get_song_state(int(x[1])): # 状态更新
|
||||
c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', {
|
||||
'a': clear_type, 'b': user_id, 'c': song_id, 'd': difficulty})
|
||||
if score >= int(x[0]): # 成绩更新
|
||||
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': user_id, 'b': song_id, 'c': difficulty, 'd': score, 'e': shiny_perfect_count, 'f': perfect_count, 'g': near_count, 'h': miss_count, 'i': health, 'j': modifier, 'k': clear_type, 'l': rating, 'm': now})
|
||||
if not unrank_flag:
|
||||
# recent30 更新
|
||||
if health == -1 or int(score) >= 9800000 or first_protect_flag:
|
||||
update_recent30(c, user_id, song_id +
|
||||
str(difficulty), rating, True)
|
||||
else:
|
||||
update_recent30(c, user_id, song_id +
|
||||
str(difficulty), rating, False)
|
||||
# 总PTT更新
|
||||
ptt = get_user_ptt(c, user_id)
|
||||
c.execute('''update user set rating_ptt = :a where user_id = :b''', {
|
||||
'a': ptt, 'b': user_id})
|
||||
|
||||
# 世界模式判断
|
||||
c.execute('''select stamina_multiply,fragment_multiply,prog_boost_multiply from world_songplay where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
'a': user_id, 'b': song_id, 'c': difficulty})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
re = server.arcworld.world_update(
|
||||
c, user_id, song_id, difficulty, rating, clear_type, beyond_gauge, health, x[0], x[1], x[2])
|
||||
re['global_rank'] = get_user_world_rank(c, user_id) # 更新世界排名
|
||||
re["user_rating"] = ptt
|
||||
else:
|
||||
re = {'global_rank': get_user_world_rank(
|
||||
c, user_id), 'user_rating': ptt}
|
||||
|
||||
return ptt, re
|
||||
|
||||
|
||||
def arc_score_check(user_id, song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, beyond_gauge, clear_type, song_token, song_hash, submission_hash):
|
||||
# 分数校验,返回布尔值
|
||||
if shiny_perfect_count < 0 or perfect_count < 0 or near_count < 0 or miss_count < 0 or score < 0:
|
||||
return False
|
||||
if difficulty not in [0, 1, 2, 3]:
|
||||
return False
|
||||
|
||||
all_note = perfect_count + near_count + miss_count
|
||||
ascore = 10000000 / all_note * \
|
||||
(perfect_count + near_count/2) + shiny_perfect_count
|
||||
if abs(ascore - score) >= 5:
|
||||
return False
|
||||
|
||||
with Connect() as c: # 歌曲谱面MD5检查,服务器没有谱面就不管了
|
||||
c.execute('''select md5 from songfile where song_id=:a and file_type=:b''', {
|
||||
'a': song_id, 'b': int(difficulty)})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
if x[0] != song_hash:
|
||||
return False
|
||||
|
||||
x = song_token + song_hash + song_id + str(difficulty) + str(score) + str(shiny_perfect_count) + str(
|
||||
perfect_count) + str(near_count) + str(miss_count) + str(health) + str(modifier) + str(clear_type)
|
||||
y = str(user_id) + song_hash
|
||||
checksum = md5(x+md5(y))
|
||||
if checksum != submission_hash:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def refresh_all_score_rating():
|
||||
# 刷新所有best成绩的rating
|
||||
error = 'Unknown error.'
|
||||
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select sid, rating_pst, rating_prs, rating_ftr, rating_byn from songs''')
|
||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
||||
x = c.fetchall()
|
||||
|
||||
if x:
|
||||
@@ -588,76 +54,3 @@ def refresh_all_score_rating():
|
||||
error = 'No song data.'
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def arc_all_post(user_id, scores_data, clearlamps_data, clearedsongs_data, unlocklist_data, installid_data, devicemodelname_data, story_data):
|
||||
# 向云端同步,无返回
|
||||
with Connect() as c:
|
||||
now = int(time.time() * 1000)
|
||||
c.execute('''delete from user_save where user_id=:a''', {'a': user_id})
|
||||
c.execute('''insert into user_save values(:a,:b,:c,:d,:e,:f,:g,:h,:i)''', {
|
||||
'a': user_id, 'b': scores_data, 'c': clearlamps_data, 'd': clearedsongs_data, 'e': unlocklist_data, 'f': installid_data, 'g': devicemodelname_data, 'h': story_data, 'i': now})
|
||||
return None
|
||||
|
||||
|
||||
def arc_all_get(user_id):
|
||||
# 从云端同步,返回字典
|
||||
|
||||
scores_data = []
|
||||
clearlamps_data = []
|
||||
clearedsongs_data = []
|
||||
unlocklist_data = []
|
||||
installid_data = ''
|
||||
devicemodelname_data = ''
|
||||
story_data = []
|
||||
createdAt = 0
|
||||
|
||||
with Connect() as c:
|
||||
c.execute('''select * from user_save where user_id=:a''',
|
||||
{'a': user_id})
|
||||
x = c.fetchone()
|
||||
|
||||
if x:
|
||||
scores_data = json.loads(x[1])[""]
|
||||
clearlamps_data = json.loads(x[2])[""]
|
||||
clearedsongs_data = json.loads(x[3])[""]
|
||||
unlocklist_data = json.loads(x[4])[""]
|
||||
installid_data = json.loads(x[5])["val"]
|
||||
devicemodelname_data = json.loads(x[6])["val"]
|
||||
story_data = json.loads(x[7])[""]
|
||||
if x[8]:
|
||||
createdAt = int(x[8])
|
||||
|
||||
if Config.SAVE_FULL_UNLOCK:
|
||||
installid_data = "0fcec8ed-7b62-48e2-9d61-55041a22b123"
|
||||
story_data = Constant.story_data
|
||||
unlocklist_data = Constant.unlocklist_data
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"story": {
|
||||
"": story_data
|
||||
},
|
||||
"devicemodelname": {
|
||||
"val": devicemodelname_data
|
||||
},
|
||||
"installid": {
|
||||
"val": installid_data
|
||||
},
|
||||
"unlocklist": {
|
||||
"": unlocklist_data
|
||||
},
|
||||
"clearedsongs": {
|
||||
"": clearedsongs_data
|
||||
},
|
||||
"clearlamps": {
|
||||
"": clearlamps_data
|
||||
},
|
||||
"scores": {
|
||||
"": scores_data
|
||||
},
|
||||
"version": {
|
||||
"val": 1
|
||||
},
|
||||
"createdAt": createdAt
|
||||
}
|
||||
|
||||
@@ -1,672 +0,0 @@
|
||||
import json
|
||||
from server.sql import Connect
|
||||
from .config import Constant
|
||||
from setting import Config
|
||||
import server.item
|
||||
import server.character
|
||||
import server.info
|
||||
import server.arcpurchase
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def calc_stamina(max_stamina_ts, curr_stamina):
|
||||
# 计算体力,返回剩余体力数值
|
||||
|
||||
stamina = int(Constant.MAX_STAMINA - (max_stamina_ts -
|
||||
int(time.time()*1000)) / Constant.STAMINA_RECOVER_TICK)
|
||||
|
||||
if stamina >= Constant.MAX_STAMINA:
|
||||
if curr_stamina >= Constant.MAX_STAMINA:
|
||||
stamina = curr_stamina
|
||||
else:
|
||||
stamina = Constant.MAX_STAMINA
|
||||
if stamina < 0:
|
||||
stamina = 0
|
||||
|
||||
return stamina
|
||||
|
||||
|
||||
def get_world_name(file_dir='./database/map'):
|
||||
# 获取所有地图名称,返回列表
|
||||
L = []
|
||||
for root, dirs, files in os.walk(file_dir):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1] == '.json':
|
||||
L.append(os.path.splitext(file)[0])
|
||||
return L
|
||||
|
||||
|
||||
def get_world_info(map_id):
|
||||
# 读取json文件内容,返回字典
|
||||
world_info = {}
|
||||
with open('./database/map/'+map_id+'.json', 'r') as f:
|
||||
world_info = json.load(f)
|
||||
|
||||
return world_info
|
||||
|
||||
|
||||
def get_user_world_info(user_id, map_id):
|
||||
# 读取json文件内容,加上用户信息,返回字典
|
||||
info = get_world_info(map_id)
|
||||
with Connect() as c:
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
info['curr_position'] = x[2]
|
||||
info['curr_capture'] = x[3]
|
||||
info['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
info['curr_position'] = 0
|
||||
info['curr_capture'] = 0
|
||||
info['is_locked'] = True
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,1)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def get_current_map(user_id):
|
||||
# 得到user的当前图,返回字符串
|
||||
re = ''
|
||||
with Connect() as c:
|
||||
c.execute('''select current_map from user where user_id = :a''',
|
||||
{'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
re = x[0]
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_world_all(user_id):
|
||||
# 读取所有地图信息并处理,返回字典列表
|
||||
re = []
|
||||
worlds = get_world_name()
|
||||
with Connect() as c:
|
||||
for map_id in worlds:
|
||||
info = get_world_info(map_id)
|
||||
steps = info['steps']
|
||||
del info['steps']
|
||||
rewards = []
|
||||
for step in steps:
|
||||
if 'items' in step:
|
||||
rewards.append(
|
||||
{'items': step['items'], 'position': step['position']})
|
||||
info['rewards'] = rewards
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
info['curr_position'] = x[2]
|
||||
info['curr_capture'] = x[3]
|
||||
info['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
info['curr_position'] = 0
|
||||
info['curr_capture'] = 0
|
||||
info['is_locked'] = True
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,1)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
re.append(info)
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_available_maps():
|
||||
# 获取当前可用图(用户设定的),返回字典列表
|
||||
re = []
|
||||
for i in Config.AVAILABLE_MAP:
|
||||
info = get_world_info(i)
|
||||
del info['steps']
|
||||
try:
|
||||
del info['is_locked']
|
||||
del info['curr_position']
|
||||
del info['curr_capture']
|
||||
except:
|
||||
pass
|
||||
re.append(info)
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def get_user_world(user_id, map_id):
|
||||
# 获取用户图信息,返回字典
|
||||
re = {}
|
||||
with Connect() as c:
|
||||
c.execute('''select * from user_world where map_id = :a and user_id = :b''',
|
||||
{'a': map_id, 'b': user_id})
|
||||
x = c.fetchone()
|
||||
re = {
|
||||
"user_id": user_id,
|
||||
"curr_position": 0,
|
||||
"curr_capture": 0,
|
||||
"is_locked": True,
|
||||
"map_id": map_id
|
||||
}
|
||||
if x:
|
||||
re['curr_position'] = x[2]
|
||||
re['curr_capture'] = x[3]
|
||||
re['is_locked'] = int2b(x[4])
|
||||
else:
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,1)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def unlock_user_world(user_id, map_id):
|
||||
# 解锁用户的图,返回成功与否布尔值
|
||||
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select is_locked from user_world where map_id=? and user_id=?''', (map_id, user_id))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
is_locked = x[0]
|
||||
else:
|
||||
is_locked = 1
|
||||
c.execute('''insert into user_world values(:a,:b,0,0,1)''', {
|
||||
'a': user_id, 'b': map_id})
|
||||
if is_locked == 1:
|
||||
map_info = get_world_info(map_id)
|
||||
if 'require_type' in map_info and map_info['require_type'] != '':
|
||||
if map_info['require_type'] in ['pack', 'single']:
|
||||
c.execute('''select exists(select * from user_item where user_id=? and item_id=? and type=?)''',
|
||||
(user_id, map_info['require_id'], map_info['require_type']))
|
||||
if c.fetchone() == (0,):
|
||||
return False
|
||||
|
||||
c.execute(
|
||||
'''update user_world set is_locked=0 where user_id=? and map_id=?''', (user_id, map_id))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def change_user_current_map(user_id, map_id):
|
||||
# 改变用户当前图
|
||||
with Connect() as c:
|
||||
c.execute('''update user set current_map = :a where user_id=:b''', {
|
||||
'a': map_id, 'b': user_id})
|
||||
return None
|
||||
|
||||
|
||||
def play_world_song(user_id, args):
|
||||
# 声明是世界模式的打歌,并且记录加成信息,返回字典
|
||||
r = {}
|
||||
with Connect() as c:
|
||||
stamina_multiply = 1
|
||||
fragment_multiply = 100
|
||||
prog_boost_multiply = 0
|
||||
if 'stamina_multiply' in args:
|
||||
stamina_multiply = int(args['stamina_multiply'])
|
||||
if 'fragment_multiply' in args:
|
||||
fragment_multiply = int(args['fragment_multiply'])
|
||||
if 'prog_boost_multiply' in args:
|
||||
c.execute('''select prog_boost from user where user_id=:a''', {
|
||||
'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x and x[0] == 1:
|
||||
prog_boost_multiply = 300
|
||||
|
||||
c.execute('''delete from world_songplay where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
'a': user_id, 'b': args['song_id'], 'c': args['difficulty']})
|
||||
c.execute('''insert into world_songplay values(:a,:b,:c,:d,:e,:f)''', {
|
||||
'a': user_id, 'b': args['song_id'], 'c': args['difficulty'], 'd': stamina_multiply, 'e': fragment_multiply, 'f': prog_boost_multiply})
|
||||
|
||||
c.execute('''select current_map from user where user_id = :a''', {
|
||||
'a': user_id})
|
||||
map_id = c.fetchone()[0]
|
||||
info = get_world_info(map_id)
|
||||
# 体力计算
|
||||
c.execute(
|
||||
'''select max_stamina_ts, stamina from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
max_stamina_ts = x[0] if x and x[0] is not None else 0
|
||||
stamina = x[1] if x and x[1] is not None else 12
|
||||
now = int(time.time() * 1000)
|
||||
|
||||
# 体力不足
|
||||
if calc_stamina(max_stamina_ts, stamina) < info['stamina_cost']:
|
||||
return {}
|
||||
stamina = calc_stamina(max_stamina_ts, stamina) - \
|
||||
info['stamina_cost'] * stamina_multiply
|
||||
max_stamina_ts = now + Constant.STAMINA_RECOVER_TICK * \
|
||||
(Constant.MAX_STAMINA - stamina)
|
||||
c.execute('''update user set max_stamina_ts=?, stamina=? where user_id=?''',
|
||||
(max_stamina_ts, stamina, user_id))
|
||||
r = {
|
||||
"stamina": stamina,
|
||||
"max_stamina_ts": max_stamina_ts,
|
||||
"token": "13145201919810"
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def climb_step(user_id, map_id, step, prev_capture, prev_position):
|
||||
# 爬梯子,返回奖励列表,台阶列表,当前的位置和坐标,图信息
|
||||
|
||||
info = get_world_info(map_id)
|
||||
step_count = int(info['step_count'])
|
||||
|
||||
restrict_ids = [[]] * step_count
|
||||
capture = [0] * step_count
|
||||
reward_bundle = [""] * step_count # 暂且不用
|
||||
restrict_id = [""] * step_count
|
||||
restrict_type = [""] * step_count
|
||||
items = [[]] * step_count
|
||||
step_type = [[]] * step_count
|
||||
speed_limit_value = [0] * step_count
|
||||
plus_stamina_value = [0] * step_count
|
||||
|
||||
for i in info['steps']:
|
||||
capture[i['position']] = i['capture']
|
||||
if 'items' in i:
|
||||
items[i['position']] = i['items']
|
||||
if 'restrict_id' in i:
|
||||
restrict_id[i['position']] = i['restrict_id']
|
||||
if 'restrict_ids' in i:
|
||||
restrict_ids[i['position']] = i['restrict_ids']
|
||||
if 'restrict_type' in i:
|
||||
restrict_type[i['position']] = i['restrict_type']
|
||||
if 'step_type' in i:
|
||||
step_type[i['position']] = i['step_type']
|
||||
if "speedlimit" in i['step_type']:
|
||||
speed_limit_value[i['position']] = i['speed_limit_value']
|
||||
if "plusstamina" in i['step_type']:
|
||||
plus_stamina_value[i['position']] = i['plus_stamina_value']
|
||||
|
||||
if info['is_beyond']: # beyond判断
|
||||
dt = info['beyond_health'] - prev_capture
|
||||
if dt >= step:
|
||||
curr_capture = prev_capture + step
|
||||
else:
|
||||
curr_capture = info['beyond_health']
|
||||
i = 0
|
||||
t = prev_capture + step
|
||||
while i < step_count and t > 0:
|
||||
dt = capture[i]
|
||||
if dt > t:
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
i += 1
|
||||
if i >= step_count:
|
||||
curr_position = step_count - 1
|
||||
else:
|
||||
curr_position = i
|
||||
|
||||
else:
|
||||
i = prev_position
|
||||
j = prev_capture
|
||||
t = step
|
||||
while t > 0 and i < step_count:
|
||||
dt = capture[i] - j
|
||||
if dt > t:
|
||||
j += t
|
||||
t = 0
|
||||
else:
|
||||
t -= dt
|
||||
j = 0
|
||||
i += 1
|
||||
if i >= step_count:
|
||||
curr_position = step_count - 1
|
||||
curr_capture = 0
|
||||
else:
|
||||
curr_position = i
|
||||
curr_capture = j
|
||||
|
||||
rewards = []
|
||||
steps = []
|
||||
for i in range(prev_position, curr_position+1):
|
||||
if items[i]:
|
||||
rewards.append({'position': i, 'items': items[i]})
|
||||
x = {
|
||||
"map_id": map_id,
|
||||
"position": i,
|
||||
"restrict_ids": restrict_ids[i],
|
||||
"capture": capture[i],
|
||||
"reward_bundle": reward_bundle[i],
|
||||
"restrict_id": restrict_id[i],
|
||||
"restrict_type": restrict_type[i]
|
||||
}
|
||||
if step_type[i]:
|
||||
x['step_type'] = step_type[i]
|
||||
if speed_limit_value[i]:
|
||||
x['speed_limit_value'] = speed_limit_value[i]
|
||||
if plus_stamina_value[i]:
|
||||
x['plus_stamina_value'] = plus_stamina_value[i]
|
||||
steps.append(x)
|
||||
|
||||
return rewards, steps, curr_position, curr_capture, info
|
||||
|
||||
|
||||
def world_update(c, user_id, song_id, difficulty, rating, clear_type, beyond_gauge, health, stamina_multiply=1, fragment_multiply=100, prog_boost_multiply=0):
|
||||
# 成绩上传后世界模式更新,返回字典
|
||||
|
||||
step_times = stamina_multiply * fragment_multiply / \
|
||||
100 * (prog_boost_multiply+100)/100
|
||||
exp_times = stamina_multiply * (prog_boost_multiply+100)/100
|
||||
if prog_boost_multiply != 0:
|
||||
c.execute('''update user set prog_boost = 0 where user_id = :a''', {
|
||||
'a': user_id})
|
||||
c.execute('''delete from world_songplay where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
'a': user_id, 'b': song_id, 'c': difficulty})
|
||||
|
||||
c.execute(
|
||||
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
character_id = x[0] if x and x[0] is not None else 0
|
||||
max_stamina_ts = x[1] if x and x[1] is not None else 0
|
||||
stamina = x[2] if x and x[2] is not None else 12
|
||||
is_skill_sealed = x[3] if x and x[3] is not None else 1
|
||||
skill = False
|
||||
skill_uncap = False
|
||||
level = 1
|
||||
exp = 0
|
||||
frag = 50
|
||||
prog = 50
|
||||
overdrive = 50
|
||||
if not is_skill_sealed:
|
||||
if x:
|
||||
skill = True
|
||||
if x[4] is not None and x[4] == 1:
|
||||
skill_uncap = True
|
||||
if x[5] is not None and x[5] == 1:
|
||||
skill_uncap = False
|
||||
|
||||
c.execute('''select frag1,prog1,overdrive1,frag20,prog20,overdrive20,frag30,prog30,overdrive30,skill_id,skill_id_uncap from character where character_id=?''', (character_id,))
|
||||
x = c.fetchone()
|
||||
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select level, exp from user_char_full where user_id = :a and character_id = :b''', {
|
||||
'a': user_id, 'b': character_id})
|
||||
else:
|
||||
c.execute('''select level, exp from user_char where user_id = :a and character_id = :b''', {
|
||||
'a': user_id, 'b': character_id})
|
||||
y = c.fetchone()
|
||||
if y:
|
||||
level = y[0]
|
||||
exp = y[1]
|
||||
else:
|
||||
level = 1
|
||||
exp = 0
|
||||
|
||||
if x:
|
||||
frag = server.character.calc_char_value(level, x[0], x[3], x[6])
|
||||
prog = server.character.calc_char_value(level, x[1], x[4], x[7])
|
||||
overdrive = server.character.calc_char_value(
|
||||
level, x[2], x[5], x[8])
|
||||
if x[9] is not None and x[9] != '' and skill:
|
||||
skill = x[9]
|
||||
else:
|
||||
skill = None
|
||||
if x[10] is not None and x[9] != '' and skill_uncap:
|
||||
skill_uncap = x[10]
|
||||
else:
|
||||
skill_uncap = None
|
||||
else:
|
||||
frag = 0
|
||||
prog = 0
|
||||
overdrive = 0
|
||||
skill = None
|
||||
skill_uncap = None
|
||||
|
||||
skill_special = ''
|
||||
if skill_uncap is not None and skill_uncap and skill_uncap in ['eto_uncap', 'luna_uncap', 'ayu_uncap', 'skill_vita']:
|
||||
skill_special = skill_uncap
|
||||
elif skill is not None and skill and skill in ['eto_uncap', 'luna_uncap', 'ayu_uncap', 'skill_vita']:
|
||||
skill_special = skill
|
||||
|
||||
c.execute('''select current_map from user where user_id = :a''', {
|
||||
'a': user_id})
|
||||
map_id = c.fetchone()[0]
|
||||
|
||||
if beyond_gauge == 0: # 是否是beyond挑战
|
||||
prog_tempest = 0
|
||||
if not is_skill_sealed and character_id == 35:
|
||||
# 风暴对立
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
prog_tempest = 60
|
||||
else:
|
||||
c.execute(
|
||||
'''select sum(level) from user_char where user_id=?''', (user_id,))
|
||||
prog_tempest = int(x[0]) / 10 if x else 0
|
||||
if prog_tempest > 60:
|
||||
prog_tempest = 60
|
||||
elif prog_tempest < 0:
|
||||
prog_tempest = 0
|
||||
|
||||
base_step = 2.5 + 2.45*rating**0.5
|
||||
step = base_step * (prog + prog_tempest) / 50 * step_times
|
||||
else:
|
||||
info = get_world_info(map_id)
|
||||
if clear_type == 0:
|
||||
base_step = 25/28 + (rating)**0.5 * 0.43
|
||||
else:
|
||||
base_step = 75/28 + (rating)**0.5 * 0.43
|
||||
|
||||
if character_id in info['character_affinity']:
|
||||
affinity_multiplier = info['affinity_multiplier'][info['character_affinity'].index(
|
||||
character_id)]
|
||||
else:
|
||||
affinity_multiplier = 1
|
||||
|
||||
if skill_special == 'skill_vita':
|
||||
# vita技能,overdrive随回忆率提升,提升量最多为10
|
||||
# 此处采用线性函数
|
||||
overdrive_extra = 0
|
||||
if 0 < health <= 100:
|
||||
overdrive_extra = health / 10
|
||||
|
||||
overdrive += overdrive_extra
|
||||
|
||||
step = base_step * overdrive / 50 * \
|
||||
step_times * affinity_multiplier
|
||||
|
||||
c.execute('''select * from user_world where user_id = :a and map_id =:b''',
|
||||
{'a': user_id, 'b': map_id})
|
||||
y = c.fetchone()
|
||||
if y[4] == 1: # 图不可用
|
||||
return {}
|
||||
|
||||
rewards, steps, curr_position, curr_capture, info = climb_step(
|
||||
user_id, map_id, step, y[3], y[2])
|
||||
|
||||
# Eto、Luna、Ayu的技能
|
||||
character_bonus_progress = None
|
||||
if skill_special == 'eto_uncap':
|
||||
# eto觉醒技能,获得残片奖励时世界模式进度加7
|
||||
fragment_flag = False
|
||||
for i in rewards:
|
||||
for j in i['items']:
|
||||
if j['type'] == 'fragment':
|
||||
fragment_flag = True
|
||||
break
|
||||
if fragment_flag:
|
||||
break
|
||||
if fragment_flag:
|
||||
character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS
|
||||
step += character_bonus_progress * step_times
|
||||
rewards, steps, curr_position, curr_capture, info = climb_step(
|
||||
user_id, map_id, step, y[3], y[2]) # 二次爬梯,重新计算
|
||||
|
||||
elif skill_special == 'luna_uncap':
|
||||
# luna觉醒技能,限制格开始时世界模式进度加7
|
||||
if 'restrict_id' in steps[0] and 'restrict_type' in steps[0] and steps[0]['restrict_type'] != '' and steps[0]['restrict_id'] != '':
|
||||
character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
|
||||
step += character_bonus_progress * step_times
|
||||
rewards, steps, curr_position, curr_capture, info = climb_step(
|
||||
user_id, map_id, step, y[3], y[2]) # 二次爬梯,重新计算
|
||||
|
||||
elif skill_special == 'ayu_uncap':
|
||||
# ayu觉醒技能,世界模式进度+5或-5,但不会小于0
|
||||
if random.random() >= 0.5:
|
||||
character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS
|
||||
else:
|
||||
character_bonus_progress = -Constant.AYU_UNCAP_BONUS_PROGRESS
|
||||
|
||||
step += character_bonus_progress * step_times
|
||||
if step < 0:
|
||||
character_bonus_progress += step / step_times
|
||||
step = 0
|
||||
|
||||
rewards, steps, curr_position, curr_capture, info = climb_step(
|
||||
user_id, map_id, step, y[3], y[2]) # 二次爬梯,重新计算
|
||||
|
||||
for i in rewards: # 物品分发
|
||||
for j in i['items']:
|
||||
amount = j['amount'] if 'amount' in j else 1
|
||||
item_id = j['id'] if 'id' in j else ''
|
||||
server.item.claim_user_item(c, user_id, item_id, j['type'], amount)
|
||||
|
||||
if 'step_type' in steps[-1]:
|
||||
if 'plusstamina' in steps[-1]['step_type'] and 'plus_stamina_value' in steps[-1]:
|
||||
# 体力格子
|
||||
max_stamina_ts, stamina = add_stamina(
|
||||
c, user_id, int(steps[-1]['plus_stamina_value']))
|
||||
# 角色升级
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
exp, level = server.character.calc_level_up(
|
||||
c, user_id, character_id, exp, exp_times*rating*6)
|
||||
c.execute('''update user_char set level=?, exp=? where user_id=? and character_id=?''',
|
||||
(level, exp, user_id, character_id))
|
||||
else:
|
||||
exp = Constant.LEVEL_STEPS[level]
|
||||
|
||||
re = {
|
||||
"rewards": rewards,
|
||||
"exp": exp,
|
||||
"level": level,
|
||||
"base_progress": base_step,
|
||||
"progress": step,
|
||||
"user_map": {
|
||||
"user_id": user_id,
|
||||
"curr_position": curr_position,
|
||||
"curr_capture": curr_capture,
|
||||
"is_locked": int2b(y[4]),
|
||||
"map_id": map_id,
|
||||
"prev_capture": y[3],
|
||||
"prev_position": y[2],
|
||||
"beyond_health": info['beyond_health']
|
||||
},
|
||||
"char_stats": {
|
||||
"character_id": character_id,
|
||||
"frag": frag,
|
||||
"prog": prog,
|
||||
"overdrive": overdrive
|
||||
},
|
||||
"current_stamina": calc_stamina(max_stamina_ts, stamina),
|
||||
"max_stamina_ts": max_stamina_ts
|
||||
}
|
||||
|
||||
if beyond_gauge == 0:
|
||||
re["user_map"]["steps"] = steps
|
||||
else:
|
||||
re["user_map"]["steps"] = len(steps)
|
||||
|
||||
if character_id == 35 and not is_skill_sealed:
|
||||
re['char_stats']['prog_tempest'] = prog_tempest
|
||||
re['char_stats']['prog'] += prog_tempest
|
||||
|
||||
if character_bonus_progress is not None:
|
||||
re['character_bonus_progress'] = character_bonus_progress
|
||||
|
||||
if stamina_multiply != 1:
|
||||
re['stamina_multiply'] = stamina_multiply
|
||||
if fragment_multiply != 100:
|
||||
re['fragment_multiply'] = fragment_multiply
|
||||
if prog_boost_multiply != 0:
|
||||
re['prog_boost_multiply'] = prog_boost_multiply
|
||||
|
||||
if curr_position == info['step_count']-1 and info['is_repeatable']: # 循环图判断
|
||||
curr_position = 0
|
||||
c.execute('''update user_world set curr_position=:a, curr_capture=:b where user_id=:c and map_id=:d''', {
|
||||
'a': curr_position, 'b': curr_capture, 'c': user_id, 'd': map_id})
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def add_stamina(c, user_id, add_stamina):
|
||||
# 增添体力,返回max_stamina_ts和stamina
|
||||
|
||||
now = int(time.time() * 1000)
|
||||
c.execute(
|
||||
'''select max_stamina_ts, stamina from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x and x[0] is not None and x[1] is not None:
|
||||
stamina = calc_stamina(x[0], x[1]) + add_stamina
|
||||
max_stamina_ts = now - \
|
||||
(stamina-Constant.MAX_STAMINA) * \
|
||||
Constant.STAMINA_RECOVER_TICK
|
||||
else:
|
||||
max_stamina_ts = now
|
||||
stamina = Constant.MAX_STAMINA
|
||||
|
||||
c.execute('''update user set max_stamina_ts=?, stamina=? where user_id=?''',
|
||||
(max_stamina_ts, stamina, user_id))
|
||||
|
||||
return max_stamina_ts, stamina
|
||||
|
||||
|
||||
def buy_stamina_by_fragment(user_id):
|
||||
# 残片买体力,返回字典和错误码
|
||||
r = {}
|
||||
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select next_fragstam_ts from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
now = int(time.time() * 1000)
|
||||
next_fragstam_ts = x[0] if x[0] else 0
|
||||
|
||||
if now < next_fragstam_ts:
|
||||
return {}, 905
|
||||
|
||||
next_fragstam_ts = now + 24*3600*1000
|
||||
max_stamina_ts, stamina = add_stamina(c, user_id, 6)
|
||||
c.execute('''update user set next_fragstam_ts=?, max_stamina_ts=?, stamina=? where user_id=?''',
|
||||
(next_fragstam_ts, max_stamina_ts, stamina, user_id))
|
||||
|
||||
r = {
|
||||
"user_id": user_id,
|
||||
"stamina": stamina,
|
||||
"max_stamina_ts": max_stamina_ts,
|
||||
"next_fragstam_ts": next_fragstam_ts
|
||||
}
|
||||
|
||||
return r, None
|
||||
|
||||
|
||||
def buy_stamina_by_ticket(user_id):
|
||||
# 源点买体力,返回字典和错误码
|
||||
r = {}
|
||||
|
||||
with Connect() as c:
|
||||
flag, ticket = server.arcpurchase.buy_item(c, user_id, 50)
|
||||
if flag:
|
||||
max_stamina_ts, stamina = add_stamina(c, user_id, 6)
|
||||
c.execute('''update user set max_stamina_ts=?, stamina=? where user_id=?''',
|
||||
(max_stamina_ts, stamina, user_id))
|
||||
r = {
|
||||
"user_id": user_id,
|
||||
"stamina": stamina,
|
||||
"max_stamina_ts": max_stamina_ts,
|
||||
"ticket": ticket
|
||||
}
|
||||
else:
|
||||
return None, 501
|
||||
|
||||
return r, None
|
||||
@@ -1,49 +1,46 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
import functools
|
||||
import base64
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.user import UserAuth, UserLogin
|
||||
from core.sql import Connect
|
||||
from .func import error_return
|
||||
from setting import Config
|
||||
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():
|
||||
if 'AppVersion' in request.headers: # 版本检查
|
||||
if Config.ALLOW_APPVERSION:
|
||||
if request.headers['AppVersion'] not in Config.ALLOW_APPVERSION:
|
||||
return error_return(NoAccess('Wrong app version.', 1203))
|
||||
|
||||
headers = request.headers
|
||||
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:
|
||||
try:
|
||||
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'
|
||||
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)
|
||||
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})
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
|
||||
return error_return()
|
||||
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
|
||||
|
||||
|
||||
def auth_required(request):
|
||||
# arcaea登录验证,写成了修饰器
|
||||
def decorator(view):
|
||||
@functools.wraps(view)
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
||||
headers = request.headers
|
||||
@@ -57,11 +54,10 @@ def auth_required(request):
|
||||
try:
|
||||
user = UserAuth(c)
|
||||
user.token = headers['Authorization'][7:]
|
||||
return view(user.token_get_id(), *args, **kwargs)
|
||||
user_id = user.token_get_id()
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
|
||||
return error_return()
|
||||
return view(user_id, *args, **kwargs)
|
||||
|
||||
return wrapped_view
|
||||
return decorator
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
from setting import Config
|
||||
from server.sql import Connect
|
||||
from .config import Constant
|
||||
import server.info
|
||||
import server.item
|
||||
import server.setme
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_level_steps():
|
||||
# 返回level_steps字典数组
|
||||
return [{'level': i, 'level_exp': Constant.LEVEL_STEPS[i]} for i in Constant.LEVEL_STEPS]
|
||||
|
||||
|
||||
def calc_char_value(level, value1, value20, value30):
|
||||
# 计算搭档数值的核心函数,返回浮点数
|
||||
|
||||
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]
|
||||
|
||||
def calc_char_value_30(level, stata, statb, lva=20, lvb=30):
|
||||
# 计算21~30级搭档数值,返回浮点数
|
||||
|
||||
return (level - lva) * (statb - stata) / (lvb - lva) + stata
|
||||
|
||||
if level < 1 or level > 30:
|
||||
return 0
|
||||
elif 1 <= level <= 20:
|
||||
return calc_char_value_20(level, value1, value20)
|
||||
else:
|
||||
return calc_char_value_30(level, value20, value30)
|
||||
|
||||
|
||||
def get_char_core(c, character_id):
|
||||
# 得到对应角色觉醒所需的核心,返回字典列表
|
||||
r = []
|
||||
c.execute(
|
||||
'''select item_id, amount from char_item where character_id = ? and type="core"''', (character_id,))
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
r.append({'core_type': i[0], 'amount': i[1]})
|
||||
return r
|
||||
|
||||
|
||||
def get_user_characters(c, user_id):
|
||||
# 获取用户所拥有角色,返回列表
|
||||
|
||||
c.execute('''select character_id from user_char where user_id = :user_id''',
|
||||
{'user_id': user_id})
|
||||
|
||||
x = c.fetchall()
|
||||
characters = []
|
||||
|
||||
if x:
|
||||
for i in x:
|
||||
characters.append(i[0])
|
||||
|
||||
return characters
|
||||
|
||||
|
||||
def get_user_character(c, user_id):
|
||||
# 得到用户拥有的角色列表,返回列表
|
||||
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select * from user_char_full a,character b where a.user_id = :user_id and a.character_id=b.character_id''',
|
||||
{'user_id': user_id})
|
||||
else:
|
||||
c.execute('''select * from user_char a,character b where a.user_id = :user_id and a.character_id=b.character_id''',
|
||||
{'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
if not x and not Config.CHARACTER_FULL_UNLOCK:
|
||||
# 添加初始角色
|
||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(user_id, 0, 1, 0, 0, 0))
|
||||
c.execute('''insert into user_char values(?,?,?,?,?,?)''',
|
||||
(user_id, 1, 1, 0, 0, 0))
|
||||
c.execute('''select * from user_char a,character b where a.user_id = :user_id and a.character_id=b.character_id''',
|
||||
{'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
|
||||
if not x:
|
||||
return []
|
||||
r = []
|
||||
for i in x:
|
||||
|
||||
char = {
|
||||
"is_uncapped_override": int2b(i[5]),
|
||||
"is_uncapped": int2b(i[4]),
|
||||
"uncap_cores": get_char_core(c, i[1]),
|
||||
"char_type": i[22],
|
||||
"skill_id_uncap": i[21],
|
||||
"skill_requires_uncap": int2b(i[20]),
|
||||
"skill_unlock_level": i[19],
|
||||
"skill_id": i[18],
|
||||
"overdrive": calc_char_value(i[2], i[11], i[14], i[17]),
|
||||
"prog": calc_char_value(i[2], i[10], i[13], i[16]),
|
||||
"frag": calc_char_value(i[2], i[9], i[12], i[15]),
|
||||
"level_exp": Constant.LEVEL_STEPS[i[2]],
|
||||
"exp": i[3],
|
||||
"level": i[2],
|
||||
"name": i[7],
|
||||
"character_id": i[1]
|
||||
}
|
||||
if i[1] == 21:
|
||||
char["voice"] = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
r.append(char)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def get_one_character(c, user_id, character_id):
|
||||
# 得到用户某个拥有的角色列表,返回字典
|
||||
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select * from user_char_full a,character b where a.user_id = :user_id and a.character_id=b.character_id and a.character_id=:a''',
|
||||
{'user_id': user_id, 'a': character_id})
|
||||
else:
|
||||
c.execute('''select * from user_char a,character b where a.user_id = :user_id and a.character_id=b.character_id and a.character_id=:a''',
|
||||
{'user_id': user_id, 'a': character_id})
|
||||
x = c.fetchone()
|
||||
if not x:
|
||||
return {}
|
||||
r = {
|
||||
"is_uncapped_override": int2b(x[5]),
|
||||
"is_uncapped": int2b(x[4]),
|
||||
"uncap_cores": get_char_core(c, x[1]),
|
||||
"char_type": x[22],
|
||||
"skill_id_uncap": x[21],
|
||||
"skill_requires_uncap": int2b(x[20]),
|
||||
"skill_unlock_level": x[19],
|
||||
"skill_id": x[18],
|
||||
"overdrive": calc_char_value(x[2], x[11], x[14], x[17]),
|
||||
"prog": calc_char_value(x[2], x[10], x[13], x[16]),
|
||||
"frag": calc_char_value(x[2], x[9], x[12], x[15]),
|
||||
"level_exp": Constant.LEVEL_STEPS[x[2]],
|
||||
"exp": x[3],
|
||||
"level": x[2],
|
||||
"name": x[7],
|
||||
"character_id": x[1]
|
||||
}
|
||||
if x[1] == 21 or x[1] == 46:
|
||||
r["voice"] = [0, 1, 2, 3, 100, 1000, 1001]
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def calc_level_up(c, user_id, character_id, exp, exp_addition):
|
||||
# 计算角色升级,返回当前经验和等级
|
||||
|
||||
exp += exp_addition
|
||||
|
||||
if exp >= Constant.LEVEL_STEPS[20]: # 未觉醒溢出
|
||||
c.execute('''select is_uncapped from user_char where user_id=? and character_id=?''',
|
||||
(user_id, character_id))
|
||||
x = c.fetchone()
|
||||
if x and x[0] == 0:
|
||||
return Constant.LEVEL_STEPS[20], 20
|
||||
|
||||
a = []
|
||||
b = []
|
||||
for i in Constant.LEVEL_STEPS:
|
||||
a.append(i)
|
||||
b.append(Constant.LEVEL_STEPS[i])
|
||||
|
||||
if exp >= b[-1]: # 溢出
|
||||
return b[-1], a[-1]
|
||||
|
||||
if exp < b[0]: # 向下溢出,是异常状态
|
||||
return 0, 1
|
||||
|
||||
i = len(a) - 1
|
||||
while exp < b[i]:
|
||||
i -= 1
|
||||
|
||||
return exp, a[i]
|
||||
|
||||
|
||||
def char_use_core(user_id, character_id, amount):
|
||||
# 以太之滴升级,返回user_id,core状态,角色状态的字典
|
||||
r = None
|
||||
with Connect() as c:
|
||||
|
||||
c.execute(
|
||||
'''select amount from user_item where user_id=? and item_id="core_generic" and type="core"''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
pre_amount = x[0]
|
||||
else:
|
||||
pre_amount = 0
|
||||
|
||||
if amount <= pre_amount:
|
||||
c.execute(
|
||||
'''select exp from user_char where user_id=? and character_id=?''', (user_id, character_id))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
exp, level = calc_level_up(
|
||||
c, user_id, character_id, x[0], amount*Constant.CORE_EXP)
|
||||
c.execute('''update user_char set level=?, exp=? where user_id=? and character_id=?''',
|
||||
(level, exp, user_id, character_id))
|
||||
server.item.claim_user_item(
|
||||
c, user_id, 'core_generic', 'core', -amount)
|
||||
|
||||
r = {'character': [get_one_character(c, user_id, character_id)]}
|
||||
r['cores'] = server.item.get_user_cores(c, user_id)
|
||||
r['user_id'] = user_id
|
||||
return r
|
||||
|
||||
|
||||
def char_uncap(user_id, character_id):
|
||||
# 角色觉醒,返回user_id,core状态,角色状态的字典
|
||||
r = None
|
||||
with Connect() as c:
|
||||
c.execute('''select * from char_item where character_id=?''',
|
||||
(character_id,))
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return None
|
||||
|
||||
success = True
|
||||
for i in x:
|
||||
c.execute(
|
||||
'''select amount from user_item where user_id=? and item_id=? and type=?''', (user_id, i[1], i[2]))
|
||||
y = c.fetchone()
|
||||
if not y or i[3] > y[0]:
|
||||
success = False
|
||||
break
|
||||
|
||||
if success:
|
||||
c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
|
||||
(user_id, character_id))
|
||||
for i in x:
|
||||
server.item.claim_user_item(c, user_id, i[1], i[2], -i[3])
|
||||
|
||||
r = {'character': [get_one_character(c, user_id, character_id)]}
|
||||
r['cores'] = server.item.get_user_cores(c, user_id)
|
||||
r['user_id'] = user_id
|
||||
return r
|
||||
File diff suppressed because it is too large
Load Diff
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
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
from flask import jsonify
|
||||
from core.error import ArcError
|
||||
from functools import wraps
|
||||
from traceback import format_exc
|
||||
|
||||
default_error = ArcError('Unknown Error')
|
||||
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): # 错误返回
|
||||
@@ -56,11 +60,29 @@ def error_return(e: ArcError = default_error): # 错误返回
|
||||
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 success_return(value):
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"value": value
|
||||
})
|
||||
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
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
from server.sql import Connect
|
||||
import server.arcworld
|
||||
import server.arcpurchase
|
||||
import server.arcdownload
|
||||
import server.character
|
||||
import server.item
|
||||
import time
|
||||
from setting import Config
|
||||
from .config import Constant
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_recent_score(c, user_id):
|
||||
# 得到用户最近一次的成绩,返回列表
|
||||
c.execute('''select * from user where user_id = :x''', {'x': user_id})
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
if x[11] is not None:
|
||||
c.execute('''select best_clear_type from best_score where user_id=:u and song_id=:s and difficulty=:d''', {
|
||||
'u': user_id, 's': x[11], 'd': x[12]})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
best_clear_type = y[0]
|
||||
else:
|
||||
best_clear_type = x[21]
|
||||
|
||||
return [{
|
||||
"rating": x[22],
|
||||
"modifier": x[19],
|
||||
"time_played": x[20],
|
||||
"health": x[18],
|
||||
"best_clear_type": best_clear_type,
|
||||
"clear_type": x[21],
|
||||
"miss_count": x[17],
|
||||
"near_count": x[16],
|
||||
"perfect_count": x[15],
|
||||
"shiny_perfect_count": x[14],
|
||||
"score": x[13],
|
||||
"difficulty": x[12],
|
||||
"song_id": x[11]
|
||||
}]
|
||||
return []
|
||||
|
||||
|
||||
def get_user_friend(c, user_id):
|
||||
# 得到用户的朋友列表,返回列表
|
||||
c.execute('''select user_id_other from friend where user_id_me = :user_id''', {
|
||||
'user_id': user_id})
|
||||
x = c.fetchall()
|
||||
s = []
|
||||
if x != [] and x[0][0] is not None:
|
||||
|
||||
for i in x:
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': i[0], 'y': user_id})
|
||||
if c.fetchone() == (1,):
|
||||
is_mutual = True
|
||||
else:
|
||||
is_mutual = False
|
||||
|
||||
c.execute('''select * from user where user_id = :x''', {'x': i[0]})
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
character = y[6]
|
||||
is_char_uncapped = int2b(y[8])
|
||||
is_char_uncapped_override = int2b(y[9])
|
||||
if y[23] != -1:
|
||||
character = y[23]
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id=:a and character_id=:b''', {
|
||||
'a': i[0], 'b': character})
|
||||
else:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id=:a and character_id=:b''', {
|
||||
'a': i[0], 'b': character})
|
||||
z = c.fetchone()
|
||||
if z:
|
||||
is_char_uncapped = int2b(z[0])
|
||||
is_char_uncapped_override = int2b(z[1])
|
||||
|
||||
rating = y[5]
|
||||
if int2b(y[10]):
|
||||
rating = -1
|
||||
|
||||
s.append({
|
||||
"is_mutual": is_mutual,
|
||||
"is_char_uncapped_override": is_char_uncapped_override,
|
||||
"is_char_uncapped": is_char_uncapped,
|
||||
"is_skill_sealed": int2b(y[7]),
|
||||
"rating": rating,
|
||||
"join_date": int(y[3]),
|
||||
"character": character,
|
||||
"recent_score": get_recent_score(c, i[0]),
|
||||
"name": y[1],
|
||||
"user_id": i[0]
|
||||
})
|
||||
|
||||
s.sort(key=lambda item: item["recent_score"][0]["time_played"] if len(
|
||||
item["recent_score"]) > 0 else 0, reverse=True)
|
||||
return s
|
||||
|
||||
|
||||
def get_user_me(c, user_id):
|
||||
# 构造user/me的数据,返回字典
|
||||
c.execute('''select * from user where user_id = :x''', {'x': user_id})
|
||||
x = c.fetchone()
|
||||
r = {}
|
||||
if x is not None:
|
||||
user_character = server.character.get_user_character(c, user_id)
|
||||
# 下面没有使用get_user_characters函数是为了节省一次查询
|
||||
characters = []
|
||||
for i in user_character:
|
||||
characters.append(i['character_id'])
|
||||
|
||||
character_id = x[6]
|
||||
if character_id not in characters:
|
||||
character_id = 0
|
||||
c.execute(
|
||||
'''update user set character_id=0 where user_id=?''', (user_id,))
|
||||
|
||||
favorite_character_id = x[23]
|
||||
if favorite_character_id not in characters: # 这是考虑有可能favourite_character设置了用户未拥有的角色
|
||||
favorite_character_id = -1
|
||||
c.execute(
|
||||
'''update user set favorite_character=-1 where user_id=?''', (user_id,))
|
||||
|
||||
# 计算世界排名
|
||||
c.execute(
|
||||
'''select world_rank_score from user where user_id=?''', (user_id,))
|
||||
y = c.fetchone()
|
||||
if y and y[0]:
|
||||
c.execute(
|
||||
'''select count(*) from user where world_rank_score > ?''', (y[0],))
|
||||
y = c.fetchone()
|
||||
if y and y[0] + 1 <= Config.WORLD_RANK_MAX:
|
||||
world_rank = y[0] + 1
|
||||
else:
|
||||
world_rank = 0
|
||||
else:
|
||||
world_rank = 0
|
||||
|
||||
# 源点强化
|
||||
prog_boost = 0
|
||||
if x[27] and x[27] != 0:
|
||||
prog_boost = 300
|
||||
|
||||
# 体力计算
|
||||
next_fragstam_ts = -1
|
||||
max_stamina_ts = 1586274871917
|
||||
stamina = 12
|
||||
if x[31]:
|
||||
next_fragstam_ts = x[31]
|
||||
if x[32]:
|
||||
max_stamina_ts = x[32]
|
||||
if x[33]:
|
||||
stamina = x[33]
|
||||
|
||||
r = {"is_aprilfools": Config.IS_APRILFOOLS,
|
||||
"curr_available_maps": server.arcworld.get_available_maps(),
|
||||
"character_stats": user_character,
|
||||
"friends": get_user_friend(c, user_id),
|
||||
"settings": {
|
||||
"favorite_character": favorite_character_id,
|
||||
"is_hide_rating": int2b(x[10]),
|
||||
"max_stamina_notification_enabled": int2b(x[24])
|
||||
},
|
||||
"user_id": user_id,
|
||||
"name": x[1],
|
||||
"user_code": x[4],
|
||||
"display_name": x[1],
|
||||
"ticket": x[26],
|
||||
"character": character_id,
|
||||
"is_locked_name_duplicate": False,
|
||||
"is_skill_sealed": int2b(x[7]),
|
||||
"current_map": x[25],
|
||||
"prog_boost": prog_boost,
|
||||
"next_fragstam_ts": next_fragstam_ts,
|
||||
"max_stamina_ts": max_stamina_ts,
|
||||
"stamina": server.arcworld.calc_stamina(max_stamina_ts, stamina),
|
||||
"world_unlocks": server.item.get_user_items(c, user_id, 'world_unlock'),
|
||||
"world_songs": server.item.get_user_items(c, user_id, 'world_song'),
|
||||
"singles": server.item.get_user_items(c, user_id, 'single'),
|
||||
"packs": server.item.get_user_items(c, user_id, 'pack'),
|
||||
"characters": characters,
|
||||
"cores": server.item.get_user_cores(c, user_id),
|
||||
"recent_score": get_recent_score(c, user_id),
|
||||
"max_friend": 50,
|
||||
"rating": x[5],
|
||||
"join_date": int(x[3]),
|
||||
"global_rank": world_rank
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def get_user_me_c(user_id):
|
||||
# user/me调用,上边没开数据库这里开一下
|
||||
with Connect() as c:
|
||||
return get_user_me(c, user_id)
|
||||
|
||||
|
||||
def get_purchase_pack(user_id):
|
||||
# 返回曲包数据
|
||||
with Connect() as c:
|
||||
return server.arcpurchase.get_purchase(c, user_id)
|
||||
|
||||
|
||||
def get_game_info():
|
||||
# 返回游戏基本信息
|
||||
r = {
|
||||
"max_stamina": Constant.MAX_STAMINA,
|
||||
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
||||
"core_exp": Constant.CORE_EXP,
|
||||
"curr_ts": int(time.time()*1000),
|
||||
"level_steps": server.character.get_level_steps(),
|
||||
"world_ranking_enabled": True,
|
||||
"is_byd_chapter_unlocked": True
|
||||
}
|
||||
return r
|
||||
|
||||
|
||||
def get_user_present(user_id):
|
||||
# 返回奖励信息
|
||||
with Connect() as c:
|
||||
return server.arcpurchase.get_user_present(c, user_id)
|
||||
|
||||
|
||||
def arc_aggregate_small(user_id):
|
||||
# 返回用户数据
|
||||
r = {"success": False}
|
||||
with Connect() as c:
|
||||
r = {"success": True,
|
||||
"value": [{
|
||||
"id": 0,
|
||||
"value": get_user_me(c, user_id)
|
||||
}]}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_aggregate_big(user_id):
|
||||
# 返回比较全的用户数据
|
||||
r = {"success": False}
|
||||
with Connect() as c:
|
||||
# 防止数据库锁
|
||||
id_2 = server.arcdownload.get_all_songs(user_id, url_flag=False)
|
||||
id_5 = {
|
||||
"current_map": server.arcworld.get_current_map(user_id),
|
||||
"user_id": user_id,
|
||||
"maps": server.arcworld.get_world_all(user_id)
|
||||
}
|
||||
r = {"success": True,
|
||||
"value": [{
|
||||
"id": 0,
|
||||
"value": get_user_me(c, user_id)
|
||||
}, {
|
||||
"id": 1,
|
||||
"value": server.arcpurchase.get_purchase(c, user_id)
|
||||
}, {
|
||||
"id": 2,
|
||||
"value": id_2
|
||||
}, {
|
||||
"id": 3,
|
||||
"value": {
|
||||
"max_stamina": Constant.MAX_STAMINA,
|
||||
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
|
||||
"core_exp": Constant.CORE_EXP,
|
||||
"curr_ts": int(time.time()*1000),
|
||||
"level_steps": server.character.get_level_steps(),
|
||||
"world_ranking_enabled": True,
|
||||
"is_byd_chapter_unlocked": True
|
||||
}
|
||||
}, {
|
||||
"id": 4,
|
||||
"value": server.arcpurchase.get_user_present(c, user_id)
|
||||
}, {
|
||||
"id": 5,
|
||||
"value": id_5
|
||||
}
|
||||
]}
|
||||
|
||||
return r
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
from shutil import copy, copy2
|
||||
from server.sql import Connect
|
||||
from database.database_initialize import main, ARCAEA_SERVER_VERSION
|
||||
|
||||
from core.sql import Connect
|
||||
from database.database_initialize import ARCAEA_SERVER_VERSION, main
|
||||
from web.system import update_database
|
||||
|
||||
|
||||
@@ -25,10 +26,6 @@ def check_before_run(app):
|
||||
|
||||
f = True
|
||||
|
||||
if not os.path.exists('setting.py'):
|
||||
app.logger.warning('File `setting.py` is missing.')
|
||||
f = False
|
||||
|
||||
if not os.path.exists('database'):
|
||||
app.logger.warning('Folder `database` is missing.')
|
||||
f = False
|
||||
@@ -91,8 +88,4 @@ def check_before_run(app):
|
||||
app.logger.warning(
|
||||
'Fail to update the file `database/arcaea_database.db`.')
|
||||
|
||||
if not os.path.exists('database/arcsong.db'):
|
||||
app.logger.warning('File `database/arcsong.db` is missing.')
|
||||
f = False
|
||||
|
||||
return f
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
from setting import Config
|
||||
|
||||
|
||||
def get_user_items(c, user_id, item_type):
|
||||
# 得到用户的物品,返回列表,不包含数量信息
|
||||
|
||||
if item_type == 'world_song' and Config.WORLD_SONG_FULL_UNLOCK or item_type == 'world_unlock' and Config.WORLD_SCENERY_FULL_UNLOCK:
|
||||
c.execute('''select item_id from item where type=?''', (item_type,))
|
||||
else:
|
||||
c.execute('''select item_id from user_item where user_id = :user_id and type = :item_type''',
|
||||
{'user_id': user_id, 'item_type': item_type})
|
||||
x = c.fetchall()
|
||||
if not x:
|
||||
return []
|
||||
|
||||
re = []
|
||||
for i in x:
|
||||
re.append(i[0])
|
||||
return re
|
||||
|
||||
|
||||
def get_user_cores(c, user_id):
|
||||
# 得到用户的core,返回字典列表
|
||||
r = []
|
||||
c.execute(
|
||||
'''select item_id, amount from user_item where user_id = ? and type="core"''', (user_id,))
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
if i[1]:
|
||||
amount = i[1]
|
||||
else:
|
||||
amount = 0
|
||||
r.append({'core_type': i[0], 'amount': amount})
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def claim_user_item(c, user_id, item_id, item_type, amount=1):
|
||||
# 处理用户物品,包括添加和删除操作,返回成功与否布尔值
|
||||
# 这个接口允许memory变成负数,所以不能用来购买
|
||||
|
||||
try:
|
||||
amount = int(amount)
|
||||
except:
|
||||
return False
|
||||
|
||||
if item_type not in ['memory', 'character']:
|
||||
c.execute('''select is_available from item where item_id=? and type=?''',
|
||||
(item_id, item_type))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
if x[0] == 0:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
if item_type in ['core', 'anni5tix']:
|
||||
c.execute(
|
||||
'''select amount from user_item where user_id=? and item_id=? and type=?''', (user_id, item_id, item_type))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
if x[0] + amount < 0: # 数量不足
|
||||
return False
|
||||
c.execute('''update user_item set amount=? where user_id=? and item_id=? and type=?''',
|
||||
(x[0]+amount, user_id, item_id, item_type))
|
||||
else:
|
||||
if amount < 0: # 添加数量错误
|
||||
return False
|
||||
c.execute('''insert into user_item values(?,?,?,?)''',
|
||||
(user_id, item_id, item_type, amount))
|
||||
|
||||
elif item_type == 'memory':
|
||||
c.execute('''select ticket from user where user_id=?''', (user_id,))
|
||||
x = c.fetchone()
|
||||
if x is not None:
|
||||
c.execute('''update user set ticket=? where user_id=?''',
|
||||
(x[0]+amount, user_id))
|
||||
else:
|
||||
return False
|
||||
|
||||
elif item_type == 'character':
|
||||
if not item_id.isdigit():
|
||||
c.execute(
|
||||
'''select character_id from character where name=?''', (item_id,))
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
character_id = x[0]
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
character_id = int(item_id)
|
||||
c.execute(
|
||||
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user_id, character_id))
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into user_char values(?,?,1,0,0,0)''',
|
||||
(user_id, character_id))
|
||||
|
||||
elif item_type in ['world_song', 'world_unlock', 'single', 'pack']:
|
||||
c.execute('''select exists(select * from user_item where user_id=? and item_id=? and type=?)''',
|
||||
(user_id, item_id, item_type))
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into user_item values(:a,:b,:c,1)''',
|
||||
{'a': user_id, 'b': item_id, 'c': item_type})
|
||||
|
||||
return True
|
||||
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())
|
||||
@@ -1,160 +0,0 @@
|
||||
from server.sql import Connect
|
||||
from setting import Config
|
||||
from .config import Constant
|
||||
import server.info
|
||||
import server.character
|
||||
|
||||
|
||||
def b2int(x):
|
||||
# int与布尔值转换
|
||||
if x:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def int2b(x):
|
||||
# int与布尔值转换
|
||||
if x is None or x == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def change_char(user_id, character_id, skill_sealed):
|
||||
# 角色改变,包括技能封印的改变,返回成功与否的布尔值
|
||||
re = False
|
||||
|
||||
with Connect() as c:
|
||||
if Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
else:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
is_uncapped = x[0]
|
||||
is_uncapped_override = x[1]
|
||||
else:
|
||||
return False
|
||||
|
||||
if skill_sealed == 'false':
|
||||
skill_sealed = False
|
||||
else:
|
||||
skill_sealed = True
|
||||
c.execute('''update user set is_skill_sealed = :a, character_id = :b, is_char_uncapped = :c, is_char_uncapped_override = :d where user_id = :e''', {
|
||||
'a': b2int(skill_sealed), 'b': character_id, 'c': is_uncapped, 'd': is_uncapped_override, 'e': user_id})
|
||||
|
||||
re = True
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def change_char_uncap(user_id, character_id):
|
||||
# 角色觉醒改变,返回字典
|
||||
r = None
|
||||
with Connect() as c:
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
else:
|
||||
c.execute('''select is_uncapped, is_uncapped_override from user_char_full where user_id = :a and character_id = :b''',
|
||||
{'a': user_id, 'b': character_id})
|
||||
x = c.fetchone()
|
||||
|
||||
if x is not None and x[0] == 1:
|
||||
c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
|
||||
'a': b2int(x[1] == 0), 'b': user_id})
|
||||
|
||||
if not Config.CHARACTER_FULL_UNLOCK:
|
||||
c.execute('''update user_char set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||
'a': b2int(x[1] == 0), 'b': user_id, 'c': character_id})
|
||||
c.execute(
|
||||
'''select * from user_char a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''', (user_id, character_id))
|
||||
else:
|
||||
c.execute('''update user_char_full set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
|
||||
'a': b2int(x[1] == 0), 'b': user_id, 'c': character_id})
|
||||
c.execute(
|
||||
'''select * from user_char_full a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''', (user_id, character_id))
|
||||
y = c.fetchone()
|
||||
if y is not None:
|
||||
r = {
|
||||
"is_uncapped_override": int2b(y[5]),
|
||||
"is_uncapped": int2b(y[4]),
|
||||
"uncap_cores": server.character.get_char_core(c, y[1]),
|
||||
"char_type": y[22],
|
||||
"skill_id_uncap": y[21],
|
||||
"skill_requires_uncap": int2b(y[20]),
|
||||
"skill_unlock_level": y[19],
|
||||
"skill_id": y[18],
|
||||
"overdrive": server.character.calc_char_value(y[2], y[11], y[14], y[17]),
|
||||
"prog": server.character.calc_char_value(y[2], y[10], y[13], y[16]),
|
||||
"frag": server.character.calc_char_value(y[2], y[9], y[12], y[15]),
|
||||
"level_exp": Constant.LEVEL_STEPS[y[2]],
|
||||
"exp": y[3],
|
||||
"level": y[2],
|
||||
"name": y[7],
|
||||
"character_id": y[1]
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_sys_set(user_id, value, set_arg):
|
||||
# 三个设置,PTT隐藏、体力满通知、最爱角色,无返回
|
||||
with Connect() as c:
|
||||
if 'favorite_character' in set_arg:
|
||||
value = int(value)
|
||||
c.execute('''update user set favorite_character = :a where user_id = :b''', {
|
||||
'a': value, 'b': user_id})
|
||||
|
||||
else:
|
||||
if value == 'false':
|
||||
value = False
|
||||
else:
|
||||
value = True
|
||||
|
||||
if 'is_hide_rating' in set_arg:
|
||||
c.execute('''update user set is_hide_rating = :a where user_id = :b''', {
|
||||
'a': b2int(value), 'b': user_id})
|
||||
if 'max_stamina_notification_enabled' in set_arg:
|
||||
c.execute('''update user set max_stamina_notification_enabled = :a where user_id = :b''', {
|
||||
'a': b2int(value), 'b': user_id})
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def arc_add_friend(user_id, friend_id):
|
||||
# 加好友,返回好友列表,或者是错误码602、604
|
||||
if user_id == friend_id:
|
||||
return 604
|
||||
|
||||
r = None
|
||||
with Connect() as c:
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into friend values(:a, :b)''',
|
||||
{'a': user_id, 'b': friend_id})
|
||||
r = server.info.get_user_friend(c, user_id)
|
||||
else:
|
||||
r = 602
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def arc_delete_friend(user_id, friend_id):
|
||||
# 删好友,返回好友列表
|
||||
r = None
|
||||
with Connect() as c:
|
||||
|
||||
c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''delete from friend where user_id_me = :x and user_id_other = :y''',
|
||||
{'x': user_id, 'y': friend_id})
|
||||
|
||||
r = server.info.get_user_friend(c, user_id)
|
||||
|
||||
return r
|
||||
@@ -1,83 +0,0 @@
|
||||
import sqlite3
|
||||
from flask import current_app
|
||||
import traceback
|
||||
|
||||
|
||||
class Connect():
|
||||
# 数据库连接类,上下文管理
|
||||
|
||||
def __init__(self, file_path='./database/arcaea_database.db'):
|
||||
"""
|
||||
数据库连接,默认连接arcaea_database.db
|
||||
接受:文件路径
|
||||
返回:sqlite3连接操作对象
|
||||
"""
|
||||
self.file_path = file_path
|
||||
|
||||
def __enter__(self):
|
||||
self.conn = sqlite3.connect(self.file_path)
|
||||
self.c = self.conn.cursor()
|
||||
return self.c
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.conn:
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
if exc_type is not None:
|
||||
current_app.logger.error(
|
||||
traceback.format_exception(exc_type, exc_val, exc_tb))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Sql():
|
||||
|
||||
@staticmethod
|
||||
def select(c, table_name, target_column=[], query=None):
|
||||
# 执行查询单句sql语句,返回fetchall数据
|
||||
# 使用准确查询,且在单表内
|
||||
|
||||
sql = 'select '
|
||||
sql_dict = {}
|
||||
if len(target_column) >= 2:
|
||||
sql += target_column[0]
|
||||
for i in range(1, len(target_column)):
|
||||
sql += ',' + target_column[i]
|
||||
sql += ' from ' + table_name
|
||||
elif len(target_column) == 1:
|
||||
sql += target_column[0] + ' from ' + table_name
|
||||
else:
|
||||
sql += '* from ' + table_name
|
||||
|
||||
where_field = []
|
||||
where_value = []
|
||||
if query:
|
||||
for i in query.query:
|
||||
where_field.append(i)
|
||||
where_value.append(query.query[i])
|
||||
|
||||
if where_field and where_value:
|
||||
sql += ' where '
|
||||
sql += where_field[0] + '=:' + where_field[0]
|
||||
sql_dict[where_field[0]] = where_value[0]
|
||||
if len(where_field) >= 2:
|
||||
for i in range(1, len(where_field)):
|
||||
sql_dict[where_field[i]] = where_value[i]
|
||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
||||
|
||||
if query and query.sort:
|
||||
sql += ' order by ' + \
|
||||
query.sort[0]['column'] + ' ' + query.sort[0]['order']
|
||||
for i in range(1, len(query.sort)):
|
||||
sql += ', ' + query.sort[i]['column'] + \
|
||||
' ' + query.sort[i]['order']
|
||||
|
||||
if query and query.limit >= 0:
|
||||
sql += ' limit :limit offset :offset'
|
||||
sql_dict['limit'] = query.limit
|
||||
sql_dict['offset'] = query.offset
|
||||
|
||||
c.execute(sql, sql_dict)
|
||||
|
||||
return c.fetchall()
|
||||
@@ -1,73 +1,167 @@
|
||||
from flask import Blueprint, request
|
||||
from core.error import ArcError, NoAccess
|
||||
from core.sql import Connect
|
||||
from core.user import UserRegister, UserLogin, User, UserOnline
|
||||
from core.character import UserCharacter
|
||||
from .func import error_return, success_return
|
||||
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 setting import Config
|
||||
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:
|
||||
return error_return(NoAccess('Wrong app version.', 1203))
|
||||
raise NoAccess('Wrong app version.', 1203)
|
||||
|
||||
with Connect() as c:
|
||||
try:
|
||||
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 = 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()
|
||||
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})
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
# 注册后自动登录
|
||||
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:
|
||||
try:
|
||||
user = UserOnline(c, user_id)
|
||||
user.change_character(
|
||||
int(request.form['character']), request.form['skill_sealed'] == 'true')
|
||||
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})
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
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:
|
||||
try:
|
||||
user = User()
|
||||
user.user_id = user_id
|
||||
character = UserCharacter(c)
|
||||
character.character_id = character_id
|
||||
character.change_uncap_override(user)
|
||||
character.select_character_info(user)
|
||||
return success_return({'user_id': user.user_id, 'character': [character.to_dict]})
|
||||
except ArcError as e:
|
||||
return error_return(e)
|
||||
return error_return()
|
||||
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)]
|
||||
})
|
||||
@@ -11,7 +11,7 @@
|
||||
<span>Sid: </span>
|
||||
<span class="song-title">{{song['song_id']}}</span>
|
||||
<br />
|
||||
<span>Name_en: </span>
|
||||
<span>Name: </span>
|
||||
<span class="song-title">{{song['name_en']}}</span>
|
||||
<br />
|
||||
<div>铺面定数 Chart const: </div>
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
</form>
|
||||
<div class="content">
|
||||
这里可以将旧版本的数据库同步到新版本的数据库,并刷新用户拥有的全角色列表。<br />
|
||||
可上传文件: arcaea_database.db和arcsong.db<br />
|
||||
可上传文件: arcaea_database.db<br />
|
||||
新数据库不存在的数据会被添加,存在的重复数据也会被改变。<br /><br />
|
||||
Here you can synchronize the old version of the database to the new version of the database and refresh the list of full
|
||||
characters owned by players.<br />
|
||||
Uploadable files: arcaea_database.db & arcsong.db<br />
|
||||
Uploadable files: arcaea_database.db<br />
|
||||
Data that does not exist in the new database will be added and the existing duplicate data will also be changed.
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
class Config:
|
||||
TIME_LIMIT = 3600000
|
||||
|
||||
COMMAND_INTERVAL = 1000000
|
||||
|
||||
COUNTDOWM_TIME = 3999
|
||||
|
||||
PLAYER_TIMEOUT = 30000000
|
||||
@@ -1,223 +0,0 @@
|
||||
import socketserver
|
||||
import threading
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
# import binascii
|
||||
from . import aes
|
||||
from .udp_parser import CommandParser
|
||||
from .udp_class import Room, Player, bi
|
||||
from .udp_config import Config
|
||||
|
||||
|
||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||
link_play_data = {}
|
||||
room_id_dict = {} # 'room_id': Room
|
||||
room_code_dict = {} # 'room_code': Room
|
||||
player_dict = {} # 'player_id' : Player
|
||||
clean_timer = 0
|
||||
|
||||
|
||||
def random_room_code():
|
||||
# 随机生成房间号
|
||||
re = ''
|
||||
for _ in range(4):
|
||||
re += chr(random.randint(65, 90))
|
||||
for _ in range(2):
|
||||
re += str(random.randint(0, 9))
|
||||
|
||||
return re
|
||||
|
||||
|
||||
def clear_player(token):
|
||||
# 清除玩家信息和token
|
||||
del player_dict[link_play_data[token]['player_id']]
|
||||
del link_play_data[token]
|
||||
|
||||
|
||||
def clear_room(room):
|
||||
# 清除房间信息
|
||||
room_id = room.room_id
|
||||
room_code = room.room_code
|
||||
del room_id_dict[room_id]
|
||||
del room_code_dict[room_code]
|
||||
del room
|
||||
|
||||
|
||||
def memory_clean(now):
|
||||
# 内存清理
|
||||
clean_room_list = []
|
||||
clean_player_list = []
|
||||
for token in link_play_data:
|
||||
room = link_play_data[token]['room']
|
||||
if now - room.timestamp >= Config.TIME_LIMIT:
|
||||
clean_room_list.append(room.room_id)
|
||||
|
||||
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
|
||||
clean_player_list.append(token)
|
||||
|
||||
for room_id in room_id_dict:
|
||||
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
|
||||
clean_room_list.append(room_id)
|
||||
|
||||
for room_id in clean_room_list:
|
||||
if room_id in room_id_dict:
|
||||
clear_room(room_id_dict[room_id])
|
||||
|
||||
for token in clean_player_list:
|
||||
clear_player(token)
|
||||
|
||||
|
||||
class UDPhandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
client_msg, server = self.request
|
||||
token = client_msg[:8]
|
||||
iv = client_msg[8:20]
|
||||
tag = client_msg[20:32]
|
||||
ciphertext = client_msg[32:]
|
||||
if int.from_bytes(token, byteorder='little') in link_play_data:
|
||||
user = link_play_data[bi(token)]
|
||||
else:
|
||||
return None
|
||||
|
||||
plaintext = aes.decrypt(user['key'], b'', iv, ciphertext, tag)
|
||||
# print(binascii.b2a_hex(plaintext))
|
||||
|
||||
commands = CommandParser(
|
||||
user['room'], user['player_index']).get_commands(plaintext)
|
||||
|
||||
if user['room'].players[user['player_index']].player_id == 0:
|
||||
clear_player(bi(token))
|
||||
temp = []
|
||||
for i in commands:
|
||||
if i[:3] == b'\x06\x16\x12':
|
||||
temp.append(i)
|
||||
commands = temp
|
||||
# 处理不能正确被踢的问题
|
||||
|
||||
for i in commands:
|
||||
iv, ciphertext, tag = aes.encrypt(user['key'], i, b'')
|
||||
# print(binascii.b2a_hex(i))
|
||||
|
||||
server.sendto(token + iv + tag[:12] +
|
||||
ciphertext, self.client_address)
|
||||
|
||||
|
||||
def server_run(ip, port):
|
||||
server = socketserver.ThreadingUDPServer((ip, port), UDPhandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
def data_swap(conn):
|
||||
clean_timer = 0
|
||||
while True:
|
||||
try:
|
||||
data = conn.recv()
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
now = round(time.time() * 1000)
|
||||
if now - clean_timer >= Config.TIME_LIMIT:
|
||||
clean_timer = now
|
||||
memory_clean(now)
|
||||
|
||||
if data[0] == 1:
|
||||
# 开房
|
||||
key = os.urandom(16)
|
||||
room_id = bi(os.urandom(8))
|
||||
while room_id in room_id_dict and room_id == 0:
|
||||
room_id = bi(os.urandom(8))
|
||||
room = Room()
|
||||
room.room_id = room_id
|
||||
room_id_dict[room_id] = room
|
||||
|
||||
player_id = bi(os.urandom(3))
|
||||
while player_id in player_dict and player_id == 0:
|
||||
player_id = bi(os.urandom(3))
|
||||
player = Player()
|
||||
player.player_id = player_id
|
||||
player.set_player_name(data[1])
|
||||
player_dict[player_id] = player
|
||||
|
||||
player.song_unlock = data[2]
|
||||
room.song_unlock = data[2]
|
||||
room.host_id = player_id
|
||||
room.players[0] = player
|
||||
room.player_num = 1
|
||||
|
||||
room_code = random_room_code()
|
||||
while room_code in room_code_dict:
|
||||
room_code = random_room_code()
|
||||
room.room_code = room_code
|
||||
room_code_dict[room_code] = room
|
||||
|
||||
token = room_id
|
||||
player.token = token
|
||||
|
||||
link_play_data[token] = {'key': key,
|
||||
'room': room,
|
||||
'player_index': 0,
|
||||
'player_id': player_id}
|
||||
|
||||
conn.send((0, room_code, room_id, token, key, player_id))
|
||||
|
||||
elif data[0] == 2:
|
||||
room_code = data[3].upper()
|
||||
if room_code not in room_code_dict:
|
||||
# 房间号错误
|
||||
conn.send((1202, ))
|
||||
else:
|
||||
room = room_code_dict[room_code]
|
||||
if room.player_num == 4:
|
||||
# 满人
|
||||
conn.send((1201, ))
|
||||
elif room.state != 2:
|
||||
# 无法加入
|
||||
conn.send((1205, ))
|
||||
else:
|
||||
key = os.urandom(16)
|
||||
token = bi(os.urandom(8))
|
||||
|
||||
player_id = bi(os.urandom(3))
|
||||
while player_id in player_dict and player_id == 0:
|
||||
player_id = bi(os.urandom(3))
|
||||
player = Player()
|
||||
player.player_id = player_id
|
||||
player.set_player_name(data[1])
|
||||
player.token = token
|
||||
player_dict[player_id] = player
|
||||
|
||||
player.song_unlock = data[2]
|
||||
|
||||
room.update_song_unlock()
|
||||
for i in range(4):
|
||||
if room.players[i].player_id == 0:
|
||||
room.players[i] = player
|
||||
player_index = i
|
||||
break
|
||||
|
||||
link_play_data[token] = {'key': key,
|
||||
'room': room,
|
||||
'player_index': player_index,
|
||||
'player_id': player_id}
|
||||
|
||||
conn.send((0, room_code, room.room_id,
|
||||
token, key, player_id, room.song_unlock))
|
||||
elif data[0] == 3:
|
||||
token = data[1]
|
||||
if token in link_play_data:
|
||||
r = link_play_data[token]
|
||||
conn.send((0, r['room'].room_code, r['room'].room_id, r['key'],
|
||||
r['room'].players[r['player_index']].player_id, r['room'].song_unlock))
|
||||
else:
|
||||
conn.send((108, ))
|
||||
|
||||
|
||||
def link_play(conn, ip: str, port: int):
|
||||
try:
|
||||
server = threading.Thread(target=server_run, args=(ip, port))
|
||||
data_exchange = threading.Thread(target=data_swap, args=(conn,))
|
||||
server.start()
|
||||
data_exchange.start()
|
||||
except:
|
||||
pass
|
||||
@@ -1,16 +1,17 @@
|
||||
from flask import (
|
||||
Blueprint, flash, redirect, render_template, request, url_for
|
||||
)
|
||||
from web.login import login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
from server.sql import Connect
|
||||
import web.webscore
|
||||
import web.system
|
||||
import time
|
||||
import server.arcscore
|
||||
import os
|
||||
import json
|
||||
from server.arcdownload import initialize_songfile
|
||||
import time
|
||||
|
||||
import server.arcscore
|
||||
from core.download import DownloadList, initialize_songfile
|
||||
from core.init import FileChecker
|
||||
from core.rank import RankList
|
||||
from core.sql import Connect
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import web.system
|
||||
import web.webscore
|
||||
from web.login import login_required
|
||||
|
||||
UPLOAD_FOLDER = 'database'
|
||||
ALLOWED_EXTENSIONS = {'db'}
|
||||
@@ -184,21 +185,21 @@ def all_song():
|
||||
return None
|
||||
|
||||
error = None
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
c.execute('''select * from songs''')
|
||||
with Connect() as c:
|
||||
c.execute('''select * from chart''')
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
posts = []
|
||||
for i in x:
|
||||
posts.append({'song_id': i[0],
|
||||
'name_en': i[1],
|
||||
'rating_pst': defnum(i[13]),
|
||||
'rating_prs': defnum(i[14]),
|
||||
'rating_ftr': defnum(i[15]),
|
||||
'rating_byn': defnum(i[16])
|
||||
'rating_pst': defnum(i[2]),
|
||||
'rating_prs': defnum(i[3]),
|
||||
'rating_ftr': defnum(i[4]),
|
||||
'rating_byn': defnum(i[5])
|
||||
})
|
||||
else:
|
||||
error = '没有铺面数据 No song data.'
|
||||
error = '没有谱面数据 No song data.'
|
||||
|
||||
if error:
|
||||
flash(error)
|
||||
@@ -216,25 +217,30 @@ def single_chart_top():
|
||||
difficulty = request.form['difficulty']
|
||||
if difficulty.isdigit():
|
||||
difficulty = int(difficulty)
|
||||
else:
|
||||
difficulty = 0
|
||||
error = None
|
||||
x = None
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
with Connect() as c:
|
||||
song_name = '%'+song_name+'%'
|
||||
c.execute('''select sid, name_en from songs where sid like :a limit 1''',
|
||||
c.execute('''select song_id, name from chart where song_id like :a limit 1''',
|
||||
{'a': song_name})
|
||||
x = c.fetchone()
|
||||
|
||||
if x:
|
||||
song_id = x[0]
|
||||
posts = server.arcscore.arc_score_top(song_id, difficulty, -1)
|
||||
for i in posts:
|
||||
i['time_played'] = time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(i['time_played']))
|
||||
else:
|
||||
error = '查询为空 No song.'
|
||||
if x:
|
||||
y = RankList(c)
|
||||
y.song.set_chart(x[0], difficulty)
|
||||
y.limit = -1
|
||||
y.select_top()
|
||||
posts = y.to_dict_list()
|
||||
for i in posts:
|
||||
i['time_played'] = time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(i['time_played']))
|
||||
else:
|
||||
error = '查询为空 No song.'
|
||||
|
||||
if not error:
|
||||
return render_template('web/singlecharttop.html', posts=posts, song_name_en=x[1], song_id=song_id, difficulty=difficulty)
|
||||
return render_template('web/singlecharttop.html', posts=posts, song_name_en=x[1], song_id=x[0], difficulty=difficulty)
|
||||
else:
|
||||
flash(error)
|
||||
|
||||
@@ -260,12 +266,13 @@ def update_database():
|
||||
flash('未选择文件 No selected file.')
|
||||
return redirect(request.url)
|
||||
|
||||
if file and allowed_file(file.filename) and file.filename in ['arcsong.db', 'arcaea_database.db']:
|
||||
if file and allowed_file(file.filename) and file.filename in ['arcaea_database.db']:
|
||||
filename = 'old_' + secure_filename(file.filename)
|
||||
file.save(os.path.join(UPLOAD_FOLDER, filename))
|
||||
flash('上传成功 Success upload.')
|
||||
try:
|
||||
web.system.update_database()
|
||||
FileChecker.update_database(
|
||||
os.path.join(UPLOAD_FOLDER, filename))
|
||||
flash('数据更新成功 Success update data.')
|
||||
except:
|
||||
flash('数据更新失败 Cannot update data.')
|
||||
@@ -282,11 +289,12 @@ def update_database():
|
||||
@login_required
|
||||
def update_song_hash():
|
||||
# 更新数据库内谱面文件hash值
|
||||
error = initialize_songfile()
|
||||
if error:
|
||||
flash(error)
|
||||
else:
|
||||
try:
|
||||
DownloadList.clear_all_cache()
|
||||
initialize_songfile()
|
||||
flash('数据刷新成功 Success refresh data.')
|
||||
except:
|
||||
flash('Something error!')
|
||||
return render_template('web/updatedatabase.html')
|
||||
|
||||
|
||||
@@ -336,11 +344,11 @@ def add_song():
|
||||
if len(name_en) >= 256:
|
||||
name_en = name_en[:200]
|
||||
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select exists(select * from songs where sid=:a)''', {'a': song_id})
|
||||
'''select exists(select * from chart where song_id=:a)''', {'a': song_id})
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into songs(sid,name_en,rating_pst,rating_prs,rating_ftr,rating_byn) values(:a,:b,:c,:d,:e,:f)''', {
|
||||
c.execute('''insert into chart values(:a,:b,:c,:d,:e,:f)''', {
|
||||
'a': song_id, 'b': name_en, 'c': rating_pst, 'd': rating_prs, 'e': rating_ftr, 'f': rating_byd})
|
||||
flash('歌曲添加成功 Successfully add the song.')
|
||||
else:
|
||||
@@ -359,11 +367,11 @@ def delete_song():
|
||||
|
||||
error = None
|
||||
song_id = request.form['sid']
|
||||
with Connect('./database/arcsong.db') as c:
|
||||
with Connect() as c:
|
||||
c.execute(
|
||||
'''select exists(select * from songs where sid=:a)''', {'a': song_id})
|
||||
'''select exists(select * from chart where song_id=:a)''', {'a': song_id})
|
||||
if c.fetchone() == (1,):
|
||||
c.execute('''delete from songs where sid=:a''', {'a': song_id})
|
||||
c.execute('''delete from chart where song_id=:a''', {'a': song_id})
|
||||
flash('歌曲删除成功 Successfully delete the song.')
|
||||
else:
|
||||
error = "歌曲不存在 The song doesn't exist."
|
||||
@@ -418,7 +426,7 @@ def all_character():
|
||||
def change_character():
|
||||
# 修改角色数据
|
||||
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita']
|
||||
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane']
|
||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||
|
||||
|
||||
@@ -692,7 +700,7 @@ def change_item():
|
||||
c.execute(
|
||||
'''select exists(select * from item where item_id=:a and type=:b)''', {'a': item_id, 'b': item_type})
|
||||
if c.fetchone() == (0,):
|
||||
c.execute('''insert into item values(?,?,?,'')''',
|
||||
c.execute('''insert into item values(?,?,?)''',
|
||||
(item_id, item_type, is_available))
|
||||
flash('物品添加成功 Successfully add the item.')
|
||||
else:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#import sqlite3
|
||||
from flask import (Blueprint, flash, g, redirect,
|
||||
render_template, request, session, url_for)
|
||||
import functools
|
||||
from setting import Config
|
||||
import hashlib
|
||||
|
||||
from core.config_manager import Config
|
||||
from flask import (Blueprint, flash, g, redirect, render_template, request,
|
||||
session, url_for)
|
||||
|
||||
bp = Blueprint('login', __name__, url_prefix='/web')
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import os
|
||||
from server.sql import Connect
|
||||
import time
|
||||
import json
|
||||
import server.arcscore
|
||||
import hashlib
|
||||
import time
|
||||
from random import Random
|
||||
from setting import Config
|
||||
|
||||
from core.sql import Connect
|
||||
|
||||
|
||||
def int2b(x):
|
||||
@@ -22,144 +19,11 @@ def random_str(randomlength=10):
|
||||
chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
|
||||
length = len(chars) - 1
|
||||
random = Random()
|
||||
for i in range(randomlength):
|
||||
for _ in range(randomlength):
|
||||
s += chars[random.randint(0, length)]
|
||||
return s
|
||||
|
||||
|
||||
def get_table_info(c, table_name):
|
||||
# 得到表结构,返回主键列表和字段名列表
|
||||
pk = []
|
||||
name = []
|
||||
|
||||
c.execute('''pragma table_info ("'''+table_name+'''")''')
|
||||
x = c.fetchall()
|
||||
if x:
|
||||
for i in x:
|
||||
name.append(i[1])
|
||||
if i[5] != 0:
|
||||
pk.append(i[1])
|
||||
|
||||
return pk, name
|
||||
|
||||
|
||||
def get_sql_select_table(table_name, get_field, where_field=[], where_value=[]):
|
||||
# sql语句拼接,select ... from ... where ...
|
||||
sql = 'select '
|
||||
sql_dict = {}
|
||||
if len(get_field) >= 2:
|
||||
sql += get_field[0]
|
||||
for i in range(1, len(get_field)):
|
||||
sql += ',' + get_field[i]
|
||||
sql += ' from ' + table_name
|
||||
elif len(get_field) == 1:
|
||||
sql += get_field[0] + ' from ' + table_name
|
||||
else:
|
||||
sql += '* from ' + table_name
|
||||
|
||||
if where_field and where_value:
|
||||
sql += ' where '
|
||||
sql += where_field[0] + '=:' + where_field[0]
|
||||
sql_dict[where_field[0]] = where_value[0]
|
||||
if len(where_field) >= 1:
|
||||
for i in range(1, len(where_field)):
|
||||
sql_dict[where_field[i]] = where_value[i]
|
||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
||||
|
||||
sql += ' order by rowid'
|
||||
return sql, sql_dict
|
||||
|
||||
|
||||
def get_sql_insert_table(table_name, field, value):
|
||||
# sql语句拼接,insert into ...(...) values(...)
|
||||
sql = 'insert into ' + table_name + '('
|
||||
sql_dict = {}
|
||||
sql2 = ''
|
||||
if len(field) >= 2:
|
||||
sql += field[0]
|
||||
sql2 += ':' + field[0]
|
||||
sql_dict[field[0]] = value[0]
|
||||
for i in range(1, len(field)):
|
||||
sql += ',' + field[i]
|
||||
sql2 += ', :' + field[i]
|
||||
sql_dict[field[i]] = value[i]
|
||||
|
||||
sql += ') values('
|
||||
|
||||
elif len(field) == 1:
|
||||
sql += field[0] + ') values('
|
||||
sql2 += ':' + field[0]
|
||||
sql_dict[field[0]] = value[0]
|
||||
|
||||
else:
|
||||
return 'error', {}
|
||||
|
||||
sql += sql2 + ')'
|
||||
|
||||
return sql, sql_dict
|
||||
|
||||
|
||||
def get_sql_delete_table(table_name, where_field=[], where_value=[]):
|
||||
# sql语句拼接,delete from ... where ...
|
||||
sql = 'delete from ' + table_name
|
||||
sql_dict = {}
|
||||
|
||||
if where_field and where_value:
|
||||
sql += ' where '
|
||||
sql += where_field[0] + '=:' + where_field[0]
|
||||
sql_dict[where_field[0]] = where_value[0]
|
||||
if len(where_field) >= 1:
|
||||
for i in range(1, len(where_field)):
|
||||
sql_dict[where_field[i]] = where_value[i]
|
||||
sql += ' and ' + where_field[i] + '=:' + where_field[i]
|
||||
|
||||
return sql, sql_dict
|
||||
|
||||
|
||||
def update_one_table(c1, c2, table_name):
|
||||
# 从c1向c2更新数据表,c1中存在的信息不变
|
||||
c1.execute(
|
||||
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
||||
c2.execute(
|
||||
'''select * from sqlite_master where type = 'table' and name = :a''', {'a': table_name})
|
||||
if not c1.fetchone() or not c2.fetchone():
|
||||
return 'error'
|
||||
|
||||
db1_pk, db1_name = get_table_info(c1, table_name)
|
||||
db2_pk, db2_name = get_table_info(c2, table_name)
|
||||
if db1_pk != db2_pk:
|
||||
return 'error'
|
||||
|
||||
field = []
|
||||
for i in db1_name:
|
||||
if i in db2_name:
|
||||
field.append(i)
|
||||
|
||||
sql, sql_dict = get_sql_select_table(table_name, db1_pk)
|
||||
c1.execute(sql)
|
||||
x = c1.fetchall()
|
||||
sql, sql_dict = get_sql_select_table(table_name, field)
|
||||
c1.execute(sql)
|
||||
y = c1.fetchall()
|
||||
if x:
|
||||
for i in range(0, len(x)):
|
||||
sql, sql_dict = get_sql_select_table(
|
||||
table_name, [], db1_pk, list(x[i]))
|
||||
sql = 'select exists(' + sql + ')'
|
||||
c2.execute(sql, sql_dict)
|
||||
|
||||
if c2.fetchone() == (1,): # 如果c2里存在,先删除
|
||||
sql, sql_dict = get_sql_delete_table(
|
||||
table_name, db1_pk, list(x[i]))
|
||||
c2.execute(sql, sql_dict)
|
||||
|
||||
sql, sql_dict = get_sql_insert_table(
|
||||
table_name, field, list(y[i]))
|
||||
c2.execute(sql, sql_dict)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def update_user_char(c):
|
||||
# 用character数据更新user_char
|
||||
c.execute('''select character_id, max_level, is_uncapped from character''')
|
||||
@@ -176,56 +40,6 @@ def update_user_char(c):
|
||||
(j[0], i[0], i[1], exp, i[2], 0))
|
||||
|
||||
|
||||
def update_database():
|
||||
# 将old数据库不存在数据加入到新数据库上,并删除old数据库
|
||||
# 对于arcaea_datebase.db,更新一些表,并用character数据更新user_char_full
|
||||
# 对于arcsong.db,更新songs
|
||||
if os.path.isfile("database/old_arcaea_database.db") and os.path.isfile("database/arcaea_database.db"):
|
||||
with Connect('./database/old_arcaea_database.db') as c1:
|
||||
with Connect() as c2:
|
||||
|
||||
update_one_table(c1, c2, 'user')
|
||||
update_one_table(c1, c2, 'friend')
|
||||
update_one_table(c1, c2, 'best_score')
|
||||
update_one_table(c1, c2, 'recent30')
|
||||
update_one_table(c1, c2, 'user_world')
|
||||
update_one_table(c1, c2, 'item')
|
||||
update_one_table(c1, c2, 'user_item')
|
||||
update_one_table(c1, c2, 'purchase')
|
||||
update_one_table(c1, c2, 'purchase_item')
|
||||
update_one_table(c1, c2, 'user_save')
|
||||
update_one_table(c1, c2, 'login')
|
||||
update_one_table(c1, c2, 'present')
|
||||
update_one_table(c1, c2, 'user_present')
|
||||
update_one_table(c1, c2, 'present_item')
|
||||
update_one_table(c1, c2, 'redeem')
|
||||
update_one_table(c1, c2, 'user_redeem')
|
||||
update_one_table(c1, c2, 'redeem_item')
|
||||
update_one_table(c1, c2, 'role')
|
||||
update_one_table(c1, c2, 'user_role')
|
||||
update_one_table(c1, c2, 'power')
|
||||
update_one_table(c1, c2, 'role_power')
|
||||
update_one_table(c1, c2, 'api_login')
|
||||
|
||||
update_one_table(c1, c2, 'user_char')
|
||||
|
||||
if not Config.UPDATE_WITH_NEW_CHARACTER_DATA:
|
||||
update_one_table(c1, c2, 'character')
|
||||
|
||||
update_user_char(c2) # 更新user_char_full
|
||||
|
||||
os.remove('database/old_arcaea_database.db')
|
||||
|
||||
# songs
|
||||
if os.path.isfile("database/old_arcsong.db") and os.path.isfile("database/arcsong.db"):
|
||||
with Connect('./database/old_arcsong.db') as c1:
|
||||
with Connect('./database/arcsong.db') as c2:
|
||||
|
||||
update_one_table(c1, c2, 'songs')
|
||||
|
||||
os.remove('database/old_arcsong.db')
|
||||
|
||||
|
||||
def unlock_all_user_item(c):
|
||||
# 解锁所有用户购买
|
||||
|
||||
@@ -321,44 +135,45 @@ def get_all_purchase():
|
||||
def update_one_save(c, user_id):
|
||||
# 同步指定用户存档
|
||||
# 注意,best_score表不比较,直接覆盖
|
||||
|
||||
c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', {
|
||||
'a': user_id})
|
||||
x = c.fetchone()
|
||||
if x:
|
||||
scores = json.loads(x[0])[""]
|
||||
clearlamps = json.loads(x[1])[""]
|
||||
clear_song_id_difficulty = []
|
||||
clear_state = []
|
||||
for i in clearlamps:
|
||||
clear_song_id_difficulty.append(i['song_id']+str(i['difficulty']))
|
||||
clear_state.append(i['clear_type'])
|
||||
|
||||
for i in scores:
|
||||
rating = server.arcscore.get_one_ptt(
|
||||
i['song_id'], i['difficulty'], i['score'])
|
||||
if rating < 0:
|
||||
rating = 0
|
||||
try:
|
||||
index = clear_song_id_difficulty.index(
|
||||
i['song_id'] + str(i['difficulty']))
|
||||
except:
|
||||
index = -1
|
||||
if index != -1:
|
||||
clear_type = clear_state[index]
|
||||
else:
|
||||
clear_type = 0
|
||||
c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
'a': user_id, 'b': i['song_id'], 'c': i['difficulty']})
|
||||
c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', {
|
||||
'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating})
|
||||
|
||||
ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT
|
||||
c.execute('''update user set rating_ptt=:a where user_id=:b''', {
|
||||
'a': ptt, 'b': user_id})
|
||||
|
||||
return
|
||||
|
||||
# c.execute('''select scores_data, clearlamps_data from user_save where user_id=:a''', {
|
||||
# 'a': user_id})
|
||||
# x = c.fetchone()
|
||||
# if x:
|
||||
# scores = json.loads(x[0])[""]
|
||||
# clearlamps = json.loads(x[1])[""]
|
||||
# clear_song_id_difficulty = []
|
||||
# clear_state = []
|
||||
# for i in clearlamps:
|
||||
# clear_song_id_difficulty.append(i['song_id']+str(i['difficulty']))
|
||||
# clear_state.append(i['clear_type'])
|
||||
|
||||
# for i in scores:
|
||||
# rating = server.arcscore.get_one_ptt(
|
||||
# i['song_id'], i['difficulty'], i['score'])
|
||||
# if rating < 0:
|
||||
# rating = 0
|
||||
# try:
|
||||
# index = clear_song_id_difficulty.index(
|
||||
# i['song_id'] + str(i['difficulty']))
|
||||
# except:
|
||||
# index = -1
|
||||
# if index != -1:
|
||||
# clear_type = clear_state[index]
|
||||
# else:
|
||||
# clear_type = 0
|
||||
# c.execute('''delete from best_score where user_id=:a and song_id=:b and difficulty=:c''', {
|
||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty']})
|
||||
# c.execute('''insert into best_score values(:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n)''', {
|
||||
# 'a': user_id, 'b': i['song_id'], 'c': i['difficulty'], 'd': i['score'], 'e': i['shiny_perfect_count'], 'f': i['perfect_count'], 'g': i['near_count'], 'h': i['miss_count'], 'i': i['health'], 'j': i['modifier'], 'k': i['time_played'], 'l': clear_type, 'm': clear_type, 'n': rating})
|
||||
|
||||
# ptt = server.arcscore.get_user_ptt(c, user_id) # 更新PTT
|
||||
# c.execute('''update user set rating_ptt=:a where user_id=:b''', {
|
||||
# 'a': ptt, 'b': user_id})
|
||||
|
||||
# return
|
||||
|
||||
|
||||
def update_all_save(c):
|
||||
# 同步所有用户存档
|
||||
|
||||
114
tools/update_song.py
Normal file
114
tools/update_song.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
|
||||
SONG_DATABASE_PATH = './arcsong.db'
|
||||
SERVER_DATABASE_PATH = './arcaea_database.db'
|
||||
|
||||
|
||||
class Connect():
|
||||
# 数据库连接类,上下文管理
|
||||
|
||||
def __init__(self, file_path=SERVER_DATABASE_PATH):
|
||||
"""
|
||||
数据库连接\
|
||||
接受:文件路径\
|
||||
返回:sqlite3连接操作对象
|
||||
"""
|
||||
self.file_path = file_path
|
||||
|
||||
def __enter__(self):
|
||||
self.conn = sqlite3.connect(self.file_path)
|
||||
self.c = self.conn.cursor()
|
||||
return self.c
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
||||
if exc_type is not None:
|
||||
if self.conn:
|
||||
self.conn.rollback()
|
||||
|
||||
if self.conn:
|
||||
self.conn.commit()
|
||||
self.conn.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def insert(cursor, song_id, name, a, b, c, d, update_type=0):
|
||||
'''Insert a new song into database.'''
|
||||
if update_type == 0 or update_type == 1:
|
||||
cursor.execute(
|
||||
'''select exists(select * from chart where song_id=?)''', (song_id, ))
|
||||
if cursor.fetchone()[0]:
|
||||
if update_type == 0:
|
||||
# 重复则不更新,以服务端数据库数据为准
|
||||
return
|
||||
elif update_type == 1:
|
||||
# 重复则更新,以`arcsong.db`数据为准
|
||||
cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''',
|
||||
(name, a, b, c, d, song_id))
|
||||
return
|
||||
|
||||
cursor.execute(
|
||||
'''insert into chart values (?,?,?,?,?,?)''', (song_id, name, a, b, c, d))
|
||||
|
||||
|
||||
def from_song_datebase():
|
||||
'''Get song data from song database and insert them into server's database.'''
|
||||
with Connect(SONG_DATABASE_PATH) as c:
|
||||
c.execute('''select name from sqlite_master where type="table"''')
|
||||
tables = [x[0] for x in c.fetchall()]
|
||||
if 'songs' in tables:
|
||||
c.execute(
|
||||
'''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''')
|
||||
data = c.fetchall()
|
||||
elif 'charts' in tables:
|
||||
c.execute(
|
||||
'''select song_id, rating_class, name_en, rating from charts''')
|
||||
songs = {}
|
||||
for song_id, rating_class, name_en, rating in c.fetchall():
|
||||
if song_id not in songs:
|
||||
songs[song_id] = [-1, -1, -1, -1, name_en]
|
||||
songs[song_id][rating_class] = rating
|
||||
|
||||
data = [(x, y[-1], y[0], y[1], y[2], y[3])
|
||||
for x, y in songs.items()]
|
||||
else:
|
||||
print('Error: Cannot find table `songs` or `charts` in the database.')
|
||||
return
|
||||
|
||||
# 用户确认更新方式
|
||||
update_type = 0
|
||||
x = input('Type a number to decide the update type:\n0: Do not update if the song already exists in the server database.\n1: Update even if the song already exists in the server database.\n2: Clear chart data in the server database and then update.\nYour choice: ')
|
||||
x = x.strip()
|
||||
if x not in ('0', '1', '2'):
|
||||
print('Error: Invalid input.')
|
||||
return
|
||||
update_type = int(x)
|
||||
|
||||
with Connect() as c:
|
||||
if update_type == 2:
|
||||
# 清空数据表后更新
|
||||
c.execute('''delete from chart''')
|
||||
for x in data:
|
||||
insert(c, x[0], x[1], x[2], x[3], x[4], x[5], update_type)
|
||||
|
||||
print('Seems to be done.')
|
||||
|
||||
|
||||
def check_file():
|
||||
if not os.path.isfile(SONG_DATABASE_PATH) or not os.path.isfile(SERVER_DATABASE_PATH):
|
||||
print('Error: Files cannot be found.')
|
||||
print('Note: Please make sure that both `arcsong.db` and `arcaea_server.db` are in this directory.')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if check_file():
|
||||
from_song_datebase()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
input('Press `Enter` key to exit.')
|
||||
Reference in New Issue
Block a user