29 Commits

Author SHA1 Message Date
Lost-MSth
b6663ac0dc Merge pull request #140 from Lost-MSth/dev
Update to v2.11.3
2023-12-03 17:28:07 +08:00
Lost-MSth
de1d46f96e Update to v2.11.3 2023-12-03 17:23:15 +08:00
Lost-MSth
f3c17cdde7 [Enhance] Link Play Rooms info & API for that
- Add an HTTP API endpoint for getting the information of rooms and players in Link Play
2023-12-03 16:42:53 +08:00
Lost-MSth
150686d9f8 [Refactor] Link Play TCP data transmission
- Code refactor of Link Play TCP data transmission for better security and scalability
2023-12-03 00:38:43 +08:00
Lost-MSth
3e93082a3c Add character Luin 2023-12-01 14:14:17 +08:00
Lost-MSth
04010d9c33 [Enhance] Link Play log & video download
- Add support for downloading `video_720.mp4` & `video_1080.mp4`
- More detailed log for Link Play #135
2023-10-23 22:21:49 +08:00
Lost-MSth
1f389e4073 [Enhance] Save unlock? & songlist parser?
- Support more things in full cloud save unlocking and songlist parser
- For Arcaea 5.0.1

> NOTE: May not work well because this client version is not completed and confusing!!
2023-09-28 21:48:54 +08:00
Lost-MSth
5788972692 [Bug fix]? A small encoding extension
- Use bytes instead of string to pass the data to the `json` module
- For Arcaea 4.7.2
2023-09-14 22:01:15 +08:00
Lost-MSth
f373c7b052 [Enhance] Restrict download by user's purchase
- An option `DOWNLOAD_FORBID_WHEN_NO_ITEM` has been added to the config file to make that users cannot download the songs' files if they has not bought them when the `songlist` file exists. (Experimental)

#128
2023-09-08 11:38:46 +08:00
Lost-MSth
04c6d1a0e0 [Enhance][Bug fix] Support skill_maya & World mode value display bug
- Add support for the skill 'skill_maya'
- Fix a bug that some characters' skill cannot display proper values in world mode progression
- Fix a bug that 'skill_mithra' results in adding `prog` value instead of world mode progress
- For Arcaea 4.7.0
2023-08-18 16:45:27 +08:00
Lost-MSth
41409dee27 [Enhance] Support 'skill_hikari_vanessa'
- Add support for the skill 'skill_hikari_vanessa'
- For Arcaea 4.6.0
2023-07-19 23:27:21 +08:00
Lost-MSth
4527339d78 [Enhance] Support 'skill_ilith_ivy'
- Add support for the skill 'skill_ilith_ivy'
- For Arcaea 4.5.0
2023-06-28 22:34:43 +08:00
Lost-MSth
5a4ff11578 Merge pull request #114 from Lost-MSth/dev
Dev
2023-05-25 22:44:22 +08:00
Lost-MSth
18e79a7799 Update to v2.11.2
- Unlock the character **Nami (Twilight)**
- Update README.md
2023-05-25 22:42:03 +08:00
Lost-MSth
e548d5639f [Enhance] API for getting PTT record
- Add an API endpoint for getting the user's rating records.
- more detailed INFO messages for  login and register
- Add `finale/finale_end` endpoint #110
2023-05-23 21:25:27 +08:00
Lost-MSth
bd74d96250 [Enhance] PTT record & log DB cleaner
- Add support for recording users' potential each day
- Add a log database cleaner tool
- A small change: `/user/<user_id> PUT` API can ban user now.
2023-05-03 00:37:41 +08:00
Lost-MSth
880b66a995 [Enhance] Add background task
- Add background task module for log database
- Update some data for Arcaea v4.4.4
2023-04-28 14:27:11 +08:00
Lost-MSth
8efd11cd02 [Bug fix] Character 5 value error
- Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. #100
2023-04-01 17:11:45 +08:00
Lost-MSth
1c58aeb526 Merge pull request #99 from Lost-MSth/dev
Update to v2.11.1
2023-03-24 19:11:32 +08:00
Lost-MSth
6f82e9b269 Update to v2.11.1 2023-03-24 19:09:47 +08:00
Lost-MSth
f92d8a9642 [Enhance] Skill of Mithra
- Add support for the skill of Mithra
2023-03-23 23:17:02 +08:00
Lost-MSth
1672d337ff [Enhance] [Refactor] Add API about characters
- Add API endpoints about characters
- Some small changes in refactoring
2023-03-22 22:27:21 +08:00
Lost-MSth
70d27be7c7 Merge pull request #94 from Lost-MSth/dev
Update to v2.11.0
2023-03-05 22:58:19 +08:00
Lost-MSth
8d856696ca Update to v2.11.0
- Change characters' value
2023-03-05 22:56:55 +08:00
Lost-MSth
ed8d0aa73c [Enhance] BYD boost & BYD chain maps
- Add support for beyond gauge boost
- Add support for beyond chain maps
- Add support skills of uncapped ilith and mika
- Unlock four chars and uncapped ilith
- Some changes in some values' algorithms
2023-03-02 23:25:38 +08:00
Lost-MSth
a84ec560dd [Enhance] API for redeem
- Add API endpoints for redeem system
- Continue to fix the bug mentioned in 930faf508d
2023-02-28 18:28:09 +08:00
Lost-MSth
930faf508d [Refactor] Refactor for link play
- Refactor simply for link play subprogram
- Fix a logic bug that the room without anyone can be entered
2023-02-27 23:41:32 +08:00
Lost-MSth
7ece2598d1 [Enhance] API for presents & character values
- Add API endpoints for presents
- Change character value algorithm
- Update character values (I forgot in v2.10.3)
2023-02-10 18:15:53 +08:00
Lost-MSth
6f39274b99 [Enhance] API for purchases, items, operations
- Add API endpoints for purchases, items, and operations
- Header checker? :)
2023-02-08 18:18:04 +08:00
65 changed files with 3329 additions and 891 deletions

110
README.md
View File

@@ -17,42 +17,53 @@ This procedure is mainly used for study and research, and shall not be used for
## 特性 Features ## 特性 Features
有以下 We have :x: : 不支持 Not supported
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
:construction: : 建设中 In construction
- 登录、注册 Login and registration - 登录、注册 Login and registration
- 多设备登录 Multi device login - 多设备自动封号 Auto-ban of multiple devices
- :warning: 多设备登录 Multi device login
- 登录频次限制 Login rate limit
- :x: 销号 Destroy account
- 成绩上传 Score upload - 成绩上传 Score upload
- PTT - 成绩校验 Score check
- 世界排名 Global rank - 成绩排名 Score rank
- 排名 Rank - 潜力值机制 Potential
- Best 30
- :warning: Recent Top 10
- :warning: 世界排名 Global rank
- 段位系统 Course system - 段位系统 Course system
- Link Play - :warning: Link Play
- 好友系统 Friends - 好友系统 Friends
- :x: 好友位提升 Max friend number increase
- 云端存档 Cloud save - 云端存档 Cloud save
- 爬梯 Climbing steps - 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
- 自定义世界模式 Customizable World Mode - 世界模式 World mode
- 自定义歌曲下载 Customizable songs download - 体力系统 Stamina system
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless) - :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
- :warning: 角色技能 Character skills
- 歌曲下载 Songs downloading
- :x: 加密下载 Encrypted downloading
- 下载校验 Download check
- 下载频次限制 Download rate limit
- 购买系统 Purchase system
- 单曲和曲包 Single & Pack
- :x: 捆绑包 Bundle
- 折扣 Discount
- 五周年兑换券 5-th anniversary ticket
- :x: Extend 包自动降价 Extend pack automatic price reduction
- 奖励系统 Present system - 奖励系统 Present system
- 兑换码系统 Redeem code system - 兑换码系统 Redeem code system
- 角色系统 Character system - 角色系统 Character system
- 全剧情解锁 Unlock all the stories - 数据记录 Data recording
- 后台查询 Background search - 用户成绩 Users' scores
- 后台自定义信息 Customize some things in the background - 用户每日潜力值 Users' daily potential
- 成绩校验 Score check - :wastebasket: 简单的网页管理后台 Simple web admin backend
- 下载校验 Download check - :construction: API
- 服务器日志 Server log - 服务器日志 Server log
没有以下 We don't have
- 服务器安全性保证 Server security assurance
可能有问题 There may be problems
- Recent 30
- 一些歌曲的解锁 Some songs' unlocking
- 同设备多共存登录 Multiple app logins on the same device
## 说明 Statement ## 说明 Statement
只是很有趣,用处探索中。 只是很有趣,用处探索中。
@@ -69,27 +80,46 @@ It is just so interesting. What it can do is under exploration.
只保留最新版本 Only keep the latest version. 只保留最新版本 Only keep the latest version.
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。 > 提醒:更新时请注意保留原先的数据库,以防数据丢失。
>
> Tips: When updating, please keep the original database in case of data loss. > Tips: When updating, please keep the original database in case of data loss.
>
> 其它小改动请参考各个 commit 信息
> Please refer to the commit messages for other minor changes.
### Version 2.10.3 ### Version 2.11.3
- 适用于Arcaea 4.2.0版本 For Arcaea 4.2.0 > v2.11.2.1 ~ v2.11.2.7 for Arcaea 4.5.0 ~ 5.2.0
- 新搭档 **拉格兰Aria** 已解锁 Unlock the character **Lagrange(Aria)**. (Lack of its values)
- 新搭档 **忘却Apophenia** 已解锁 Unlock the character **Lethe(Apophenia)**. - 适用于 Arcaea 5.2.0 版本
- 新增选项取消歌曲文件哈希预计算 Add an option to disable song file hash pre-calculation. For Arcaea 5.2.0
-增对世界模式中地图本地限制歌曲解锁或挑战解锁以及地图中台阶上限制歌曲难度的支持 Add support for restricting songs' difficulty in the map's steps of world mode and locally restricting unlocking songs or challenges in the map of world mode. -搭档 **Ilith & Ivy**、**Hikari & Vanessa**、**摩耶**、**露恩** 已解锁(注意“ 洞烛(至高:第八探索者)”因客户端因素不可选用)
- 恢复使用云存档覆盖成绩的功能 Restore the feature that cloud save can be used to cover best scores. Unlock the character **Ilith & Ivy**, **Hikari & Vanessa**, **Maya**, and **Luin**. (Note that "Insight(Ascendant - 8th Seeker)" is not available due to the client.)
- 捕获`Authorization`不在请求头导致的报错 Capture error that the request does not have `Authorization` in header. - 为以上角色的技能提供服务端支持
- 修复客户端版本校验中请求头不存在`AppVersion`也能通过校验的逻辑错误 Fix a bug that headers without `AppVersion` are allowed in client version checking. Provide server-side support for the skills of the above characters.
- 新增增删改歌曲信息的API接口 Add some API endpoints, including creating, changing, deleting song info. - 设置中新增可选选项 `DOWNLOAD_FORBID_WHEN_NO_ITEM` 使得当 `songlist` 文件存在时,没有购买的用户无法下载曲目文件(实验性)
An option `DOWNLOAD_FORBID_WHEN_NO_ITEM` has been added to the config file to make that users cannot download the songs' files if they has not bought them when the `songlist` file exists. (Experimental)
- 支持文件 `video_720.mp4` & `video_1080.mp4` 的下载
Add support for downloading `video_720.mp4` & `video_1080.mp4`.
- 在存档全解锁和 `songlist` 解析器中支持更多东西,以适应游戏更新
Support more things in full cloud save unlocking and `songlist` parser, to adapt to game updates.
- Link Play 拥有更详细的控制台日志了
More detailed console log for Link Play.
- 修复一些搭档的技能在世界模式进度中显示不正确的问题
Fix a bug that some characters' skill cannot display proper values in world mode progression.
- 修复技能 "skill_mithra" 导致了 `prog` 值增加而不是世界模式进度增加的问题
Fix a bug that "skill_mithra" results in adding `prog` value instead of world mode progress.
- 重构 Link Play TCP 数据交换部分,以获得更好的安全性和扩展性
Code refactor of Link Play TCP data transmission for better security and scalability.
- 新增一个 HTTP API 用来获取 Link Play 中当前的房间和玩家信息
Add an HTTP API endpoint for getting the information of current rooms and players in Link Play.
## 运行环境与依赖 Running environment and requirements ## 运行环境与依赖 Running environment and requirements
- Windows/Linux/Mac OS/Android... - Windows / Linux / Mac OS / Android...
- Python >= 3.6 - Python >= 3.6
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0 - Flask >= 2.0
- Cryptography >= 3.0.0
- limits >= 2.7.0
- Charles, IDA, proxy app... (optional) - Charles, IDA, proxy app... (optional)
<!-- <!--
@@ -110,7 +140,7 @@ It is just so interesting. What it can do is under exploration.
## Q&A ## Q&A
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A) [中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
## 鸣谢 Thanks ## 鸣谢 Thanks

View File

@@ -1,10 +1,10 @@
from flask import Blueprint from flask import Blueprint
from . import users from . import (users, songs, token, system, items,
from . import songs purchases, presents, redeems, characters, multiplay)
from . import token
bp = Blueprint('api', __name__, url_prefix='/api/v1') bp = Blueprint('api', __name__, url_prefix='/api/v1')
bp.register_blueprint(users.bp) l = [users, songs, token, system, items, purchases,
bp.register_blueprint(songs.bp) presents, redeems, characters, multiplay]
bp.register_blueprint(token.bp) for i in l:
bp.register_blueprint(i.bp)

View File

@@ -1,15 +1,17 @@
from functools import wraps
from traceback import format_exc
from base64 import b64decode from base64 import b64decode
from functools import wraps
from json import loads from json import loads
from traceback import format_exc
from flask import current_app
from core.api_user import APIUser from core.api_user import APIUser
from core.config_manager import Config from core.config_manager import Config
from core.error import ArcError, NoAccess, PostError from core.error import ArcError, InputError, NoAccess, PostError
from core.sql import Connect from core.sql import Connect
from flask import current_app
from .api_code import error_return from .api_code import error_return
from .constant import Constant
def role_required(request, powers=[]): def role_required(request, powers=[]):
@@ -48,13 +50,14 @@ def role_required(request, powers=[]):
return decorator return decorator
def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False): def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False):
''' '''
提取post参数返回dict写成了修饰器\ 提取post参数返回dict写成了修饰器
parameters: \
`request`: `Request` - 当前请求\ parameters:
`required_keys`: `list` - 必须的参数\ `request`: `Request` - 当前请求
`optional_keys`: `list` - 可选的参数\ `required_keys`: `list` - 必须的参数
`optional_keys`: `list` - 可选的参数
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改 `must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
''' '''
@@ -64,7 +67,7 @@ def request_json_handle(request, required_keys: list = [], optional_keys: list =
data = {} data = {}
if request.data: if request.data:
json_data = request.json json_data = request.get_json()
else: else:
if request.method == 'GET' and 'query' in request.args: if request.method == 'GET' and 'query' in request.args:
# 处理axios没法GET传data的问题 # 处理axios没法GET传data的问题
@@ -78,15 +81,24 @@ def request_json_handle(request, required_keys: list = [], optional_keys: list =
for key in required_keys: for key in required_keys:
if key not in json_data: if key not in json_data:
return error_return(PostError(f'Missing parameter: {key}', api_error_code=-100)) return error_return(InputError(f'Missing parameter: {key}', api_error_code=-100))
data[key] = json_data[key] data[key] = json_data[key]
for key in optional_keys: if is_batch:
if key in json_data: for key in Constant.PATCH_KEYS:
data[key] = json_data[key] if key in json_data:
data[key] = json_data[key]
if not isinstance(data[key], list):
return error_return(InputError(f'Parameter {key} must be a list', api_error_code=-100))
if not data:
return error_return(InputError('No change', api_error_code=-100))
else:
for key in optional_keys:
if key in json_data:
data[key] = json_data[key]
if must_change and not data: if must_change and not data:
return error_return(PostError('No change', api_error_code=-100)) return error_return(InputError('No change', api_error_code=-100))
return view(data, *args, **kwargs) return view(data, *args, **kwargs)
@@ -102,8 +114,7 @@ def api_try(view):
data = view(*args, **kwargs) data = view(*args, **kwargs)
if data is None: if data is None:
return error_return() return error_return()
else: return data
return data
except ArcError as e: except ArcError as e:
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
current_app.logger.warning(format_exc()) current_app.logger.warning(format_exc())

View File

@@ -18,6 +18,13 @@ CODE_MSG = {
-104: 'Invalid sort order parameter', -104: 'Invalid sort order parameter',
-105: 'Invalid URL parameter', -105: 'Invalid URL parameter',
-110: 'Invalid user_id', -110: 'Invalid user_id',
-120: 'Invalid item type',
-121: 'No such item',
-122: 'Item already exists',
-123: 'The collection already has this item',
-124: 'The collection does not have this item',
-130: 'No such character',
-131: 'Invalid skill ID',
-200: 'No permission', # 2xx用户相关错误 -200: 'No permission', # 2xx用户相关错误
-201: 'Wrong username or password', -201: 'Wrong username or password',
-202: 'User is banned', -202: 'User is banned',

View File

@@ -0,0 +1,134 @@
from flask import Blueprint, request
from core.error import InputError, NoData
from core.item import ItemFactory
from core.character import Character
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('characters', __name__, url_prefix='/characters')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def characters_get(data, user):
'''查询全角色信息'''
A = ['character_id', 'name', 'skill_id',
'skill_id_uncap', 'char_type', 'is_uncapped']
B = ['name', 'skill_id', 'skill_id_uncap']
C = ['name', 'frag1', 'prog1', 'overdrive1', 'frag20',
'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30']
with Connect() as c:
query = Query(A, B, C).from_dict(data)
x = Sql(c).select('character', query=query)
r = [Character().from_list(i) for i in x]
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict() for x in r])
@bp.route('/<int:character_id>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def characters_character_get(user, character_id: int):
# 包含core
with Connect() as c:
c = Character(c).select(character_id)
c.select_character_core()
return success_return(c.to_dict(has_cores=True))
@bp.route('/<int:character_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['max_level', 'skill_id', 'skill_id_uncap', 'skill_unlock_level', 'skill_requires_uncap', 'char_type', 'is_uncapped', 'frag1', 'prog1', 'overdrive1', 'frag20', 'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30'], must_change=True)
@api_try
def characters_character_put(data, user, character_id: int):
'''修改角色信息'''
if ('skill_id' in data and data['skill_id'] != '' and data['skill_id'] not in Constant.SKILL_IDS) or ('skill_id_uncap' in data and data['skill_id_uncap'] != '' and data['skill_id_uncap'] not in Constant.SKILL_IDS):
raise InputError('Invalid skill_id', api_error_code=-131)
with Connect() as c:
c = Character(c).select(character_id)
try:
if 'max_level' in data:
c.max_level = int(data['max_level'])
if 'skill_id' in data:
c.skill_id = data['skill_id']
if 'skill_id_uncap' in data:
c.skill_id_uncap = data['skill_id_uncap']
if 'skill_unlock_level' in data:
c.skill_unlock_level = int(data['skill_unlock_level'])
if 'skill_requires_uncap' in data:
c.skill_requires_uncap = data['skill_requires_uncap'] == 1
if 'char_type' in data:
c.char_type = int(data['char_type'])
if 'is_uncapped' in data:
c.is_uncapped = data['is_uncapped'] == 1
t = ['frag1', 'prog1', 'overdrive1', 'frag20', 'prog20',
'overdrive20', 'frag30', 'prog30', 'overdrive30']
for i in t:
if i not in data:
continue
if i.endswith('1'):
x = getattr(c, i[:-1])
x.start = float(data[i])
elif i.endswith('20'):
x = getattr(c, i[:-2])
x.mid = float(data[i])
else:
x = getattr(c, i[:-2])
x.end = float(data[i])
except ValueError as e:
raise InputError('Invalid input', api_error_code=-101) from e
c.update()
return success_return(c.to_dict())
@bp.route('/<int:character_id>/cores', methods=['GET'])
@role_required(request, ['select'])
@api_try
def characters_character_cores_get(user, character_id: int):
with Connect() as c:
c = Character(c)
c.character_id = character_id
c.select_character_core()
return success_return(c.uncap_cores_to_dict())
@bp.route('/<int:character_id>/cores', methods=['PATCH'])
@role_required(request, ['change'])
@request_json_handle(request, is_batch=True)
@api_try
def characters_character_cores_patch(data, user, character_id: int):
'''修改角色觉醒cores'''
def force_type_core(x: dict) -> dict:
x['item_type'] = 'core'
x['type'] = 'core'
return x
with Connect() as c:
ch = Character(c)
ch.character_id = character_id
ch.select_character_core()
ch.remove_items([ItemFactory.from_dict(x, c=c)
for x in map(force_type_core, data.get('remove', []))])
ch.add_items([ItemFactory.from_dict(x, c=c)
for x in map(force_type_core, data.get('create', []))])
updates = list(map(force_type_core, data.get('update', [])))
for x in updates:
if 'amount' not in x:
raise InputError('`amount` is required in `update`')
if not isinstance(x['amount'], int) or x['amount'] <= 0:
raise InputError(
'`amount` must be a positive integer', api_error_code=-101)
ch.update_items(
[ItemFactory.from_dict(x, c=c) for x in updates])
return success_return(ch.uncap_cores_to_dict())

View File

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

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

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

View File

@@ -0,0 +1,21 @@
from flask import Blueprint, request
from core.linkplay import RemoteMultiPlayer
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
bp = Blueprint('multiplay', __name__, url_prefix='/multiplay')
@bp.route('/rooms', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=['offset', 'limit'])
@api_try
def rooms_get(data, user):
'''获取房间列表'''
r = RemoteMultiPlayer().get_rooms(offset=data.get(
'offset', 0), limit=data.get('limit', 100))
return success_return(r)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
from base64 import b64decode from base64 import b64decode
from flask import Blueprint, current_app, request
from core.api_user import APIUser from core.api_user import APIUser
from core.error import PostError from core.error import PostError
from core.sql import Connect from core.sql import Connect
from flask import Blueprint, request
from .api_auth import api_try, request_json_handle, role_required from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return from .api_code import success_return
@@ -16,21 +17,23 @@ bp = Blueprint('token', __name__, url_prefix='/token')
@api_try @api_try
def token_post(data): def token_post(data):
''' '''
登录获取token\ 登录获取token
{'auth': base64('<user_id>:<password>')} {'auth': base64('<user_id>:<password>')}
''' '''
try: try:
auth_decode = bytes.decode(b64decode(data['auth'])) auth_decode = bytes.decode(b64decode(data['auth']))
except: except Exception as e:
raise PostError(api_error_code=-100) raise PostError(api_error_code=-100) from e
if not ':' in auth_decode: if ':' not in auth_decode:
raise PostError(api_error_code=-100) raise PostError(api_error_code=-100)
name, password = auth_decode.split(':', 1) name, password = auth_decode.split(':', 1)
with Connect() as c: with Connect() as c:
user = APIUser(c) user = APIUser(c)
user.login(name, password, request.remote_addr) user.login(name, password, request.remote_addr)
return success_return({'token': user.token, 'user_id': user.user_id}) current_app.logger.info(f'API user `{user.user_id}` log in')
return success_return({'token': user.api_token, 'user_id': user.user_id})
@bp.route('', methods=['GET']) @bp.route('', methods=['GET'])

View File

@@ -1,9 +1,12 @@
from flask import Blueprint, request
from core.api_user import APIUser
from core.config_manager import Config
from core.error import InputError, NoAccess, NoData from core.error import InputError, NoAccess, NoData
from core.score import Potential, UserScoreList from core.score import Potential, UserScoreList
from core.sql import Connect, Query, Sql from core.sql import Connect, Query, Sql
from core.user import UserChanger, UserInfo, UserRegister from core.user import UserChanger, UserInfo, UserRegister
from core.api_user import APIUser from core.util import get_today_timestamp
from flask import Blueprint, request
from .api_auth import api_try, request_json_handle, role_required from .api_auth import api_try, request_json_handle, role_required
from .api_code import error_return, success_return from .api_code import error_return, success_return
@@ -90,8 +93,12 @@ def users_user_put(data, user, user_id):
u.set_name(data['name']) u.set_name(data['name'])
r['name'] = u.name r['name'] = u.name
if 'password' in data: if 'password' in data:
u.set_password(data['password']) if data['password'] == '':
r['password'] = u.hash_pwd u.password = ''
r['password'] = ''
else:
u.set_password(data['password'])
r['password'] = u.hash_pwd
if 'email' in data: if 'email' in data:
u.set_email(data['email']) u.set_email(data['email'])
r['email'] = u.email r['email'] = u.email
@@ -127,7 +134,7 @@ def users_user_b30_get(user, user_id):
f'No best30 data of user `{user_id}`', api_error_code=-3) f'No best30 data of user `{user_id}`', api_error_code=-3)
x.select_song_name() x.select_song_name()
r = x.to_dict_list() r = x.to_dict_list()
rating_sum = sum([i.rating for i in x.scores]) rating_sum = sum(i.rating for i in x.scores)
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r}) return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})
@@ -190,3 +197,32 @@ def users_user_role_get(user, user_id):
x = APIUser(c, user_id) x = APIUser(c, user_id)
x.select_role_and_powers() 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]}) return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
@bp.route('/<int:user_id>/rating', methods=['GET'])
@role_required(request, ['select', 'select_me'])
@request_json_handle(request, optional_keys=['start_timestamp', 'end_timestamp', 'duration'])
@api_try
def users_user_rating_get(data, user, user_id):
'''查询用户历史rating`duration`是相对于今天的天数'''
# 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403)
start_timestamp = data.get('start_timestamp', None)
end_timestamp = data.get('end_timestamp', None)
duration = data.get('duration', None)
sql = '''select time, rating_ptt from user_rating where user_id = ?'''
sql_data = [user_id]
if start_timestamp is not None and end_timestamp is not None:
sql += ''' and time between ? and ?'''
sql_data += [start_timestamp, end_timestamp]
elif duration is not None:
sql += ''' and time between ? and ?'''
t = get_today_timestamp()
sql_data += [t - duration * 24 * 3600, t]
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
c.execute(sql, sql_data)
r = c.fetchall()
return success_return({'user_id': user_id, 'data': [{'time': i[0], 'rating_ptt': i[1]} for i in r]})

View File

@@ -55,8 +55,8 @@ class Role:
{'a': self.role_id}) {'a': self.role_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The role `%s` does not exist.' % raise NoData(
self.role_id, api_error_code=-200) f'The role `{self.role_id}` does not exist.', api_error_code=-200)
self.caption = x[0] self.caption = x[0]
return self return self
@@ -127,25 +127,26 @@ class APIUser(UserOnline):
if ip is not None: if ip is not None:
self.ip = ip self.ip = ip
if not self.limiter.hit(name): if not self.limiter.hit(name):
raise RateLimit('Too many login attempts', api_error_code=-205) raise RateLimit(
f'Too many login attempts of username {name}', api_error_code=-205)
self.c.execute('''select user_id, password from user where name = :a''', { self.c.execute('''select user_id, password from user where name = :a''', {
'a': self.name}) 'a': self.name})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The user `%s` does not exist.' % raise NoData(
self.name, api_error_code=-201, status=401) f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
if x[1] == '': if x[1] == '':
raise UserBan('The user `%s` is banned.' % self.name) raise UserBan(f'The user `{x[0]}` is banned.')
if self.hash_pwd != x[1]: if self.hash_pwd != x[1]:
raise NoAccess('The password is incorrect.', raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
api_error_code=-201, status=401) api_error_code=-201, status=401)
self.user_id = x[0] self.user_id = x[0]
now = int(time() * 1000) now = int(time() * 1000)
self.token = sha256( self.api_token = sha256(
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest() (str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
self.logout() self.logout()
self.c.execute('''insert into api_login values(?,?,?,?)''', self.c.execute('''insert into api_login values(?,?,?,?)''',
(self.user_id, self.token, now, self.ip)) (self.user_id, self.api_token, now, self.ip))

View File

@@ -0,0 +1,50 @@
from atexit import register
from concurrent.futures import ThreadPoolExecutor
from .constant import Constant
from .sql import Connect
class BGTask:
executor = ThreadPoolExecutor(max_workers=1)
def __init__(self, func, *args, **kwargs):
self.future = self.executor.submit(func, *args, **kwargs)
def result(self):
return self.future.result()
def cancel(self) -> bool:
return self.future.cancel()
def done(self) -> bool:
return self.future.done()
@staticmethod
def shutdown(wait: bool = True):
BGTask.executor.shutdown(wait)
@register
def atexit():
BGTask.shutdown()
def logdb_execute_func(sql, *args, **kwargs):
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
c.execute(sql, *args, **kwargs)
def logdb_execute_many_func(sql, *args, **kwargs):
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
c.executemany(sql, *args, **kwargs)
def logdb_execute(sql: str, *args, **kwargs):
'''异步执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_func, sql, *args, **kwargs)
def logdb_execute_many(sql: str, *args, **kwargs):
'''异步批量执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_many_func, sql, *args, **kwargs)

View File

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

View File

@@ -12,7 +12,7 @@ class Config:
SONG_FILE_HASH_PRE_CALCULATE = True SONG_FILE_HASH_PRE_CALCULATE = True
GAME_API_PREFIX = '/join/21' GAME_API_PREFIX = '/evolution/23'
ALLOW_APPVERSION = [] # list[str] ALLOW_APPVERSION = [] # list[str]
@@ -23,6 +23,7 @@ class Config:
LINKPLAY_TCP_PORT = 10901 LINKPLAY_TCP_PORT = 10901
LINKPLAY_AUTHENTICATION = 'my_link_play_server' LINKPLAY_AUTHENTICATION = 'my_link_play_server'
LINKPLAY_DISPLAY_HOST = '' LINKPLAY_DISPLAY_HOST = ''
LINKPLAY_TCP_SECRET_KEY = '1145141919810'
SSL_CERT = '' SSL_CERT = ''
SSL_KEY = '' SSL_KEY = ''
@@ -48,6 +49,8 @@ class Config:
DOWNLOAD_TIMES_LIMIT = 3000 DOWNLOAD_TIMES_LIMIT = 3000
DOWNLOAD_TIME_GAP_LIMIT = 1000 DOWNLOAD_TIME_GAP_LIMIT = 1000
DOWNLOAD_FORBID_WHEN_NO_ITEM = False
LOGIN_DEVICE_NUMBER_LIMIT = 1 LOGIN_DEVICE_NUMBER_LIMIT = 1
ALLOW_LOGIN_SAME_DEVICE = False ALLOW_LOGIN_SAME_DEVICE = False
ALLOW_BAN_MULTIDEVICE_USER_AUTO = True ALLOW_BAN_MULTIDEVICE_USER_AUTO = True

View File

@@ -1,6 +1,7 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.10.3' ARCAEA_SERVER_VERSION = 'v2.11.2.7'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
class Constant: class Constant:
@@ -18,10 +19,17 @@ class Constant:
LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100, LEVEL_STEPS = {1: 0, 2: 50, 3: 100, 4: 150, 5: 200, 6: 300, 7: 450, 8: 650, 9: 900, 10: 1200, 11: 1600, 12: 2100, 13: 2700, 14: 3400, 15: 4200, 16: 5100,
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000} 17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
WORLD_VALUE_NAME_ENUM = ['frag', 'prog', 'over']
FREE_PACK_NAME = 'base'
SINGLE_PACK_NAME = 'single'
ETO_UNCAP_BONUS_PROGRESS = 7 ETO_UNCAP_BONUS_PROGRESS = 7
LUNA_UNCAP_BONUS_PROGRESS = 7 LUNA_UNCAP_BONUS_PROGRESS = 7
AYU_UNCAP_BONUS_PROGRESS = 5 AYU_UNCAP_BONUS_PROGRESS = 5
SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000 SKILL_FATALIS_WORLD_LOCKED_TIME = 3600000
SKILL_MIKA_SONGS = ['aprilshowers', 'seventhsense', 'oshamascramble',
'amazingmightyyyy', 'cycles', 'maxrage', 'infinity', 'temptation']
MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT MAX_FRIEND_COUNT = Config.MAX_FRIEND_COUNT
@@ -48,6 +56,8 @@ class Constant:
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
# Well, I can't say a word when I see this. # Well, I can't say a word when I see this.
FINALE_SWITCH = [ FINALE_SWITCH = [
@@ -99,4 +109,6 @@ class Constant:
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save', 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'] '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 LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -96,7 +96,7 @@ class Course:
'''select * from course where course_id = ?''', (self.course_id,)) '''select * from course where course_id = ?''', (self.course_id,))
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The course `%s` is not found.' % self.course_id) raise NoData(f'The course `{self.course_id}` is not found.')
return self.from_list(x) return self.from_list(x)
def select_course_chart(self) -> None: def select_course_chart(self) -> None:
@@ -151,7 +151,8 @@ class Course:
class UserCourse(Course): class UserCourse(Course):
''' '''
用户课题类\ 用户课题类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -200,7 +201,8 @@ class UserCourse(Course):
class UserCourseList: class UserCourseList:
''' '''
用户课题列表类\ 用户课题列表类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -237,8 +239,9 @@ class UserCourseList:
class CoursePlay(UserCourse): class CoursePlay(UserCourse):
''' '''
课题模式打歌类联动UserPlay\ 课题模式打歌类联动UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''

View File

@@ -25,7 +25,9 @@ class SonglistParser:
'''songlist文件解析器''' '''songlist文件解析器'''
FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff', FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff',
'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg'] 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4']
has_songlist = False
songs: dict = {} # {song_id: value, ...} songs: dict = {} # {song_id: value, ...}
# value: bit 76543210 # value: bit 76543210
# 7: video_audio.ogg # 7: video_audio.ogg
@@ -37,6 +39,10 @@ class SonglistParser:
# 1: 1.aff # 1: 1.aff
# 0: 0.aff # 0: 0.aff
pack_info: 'dict[str, set]' = {} # {pack_id: {song_id, ...}, ...}
free_songs: set = set() # {song_id, ...}
world_songs: set = set() # {world_song_id, ...}
def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None: def __init__(self, path=Constant.SONGLIST_FILE_PATH) -> None:
self.path = path self.path = path
self.data: list = [] self.data: list = []
@@ -49,15 +55,37 @@ class SonglistParser:
# songlist没有则只限制文件名 # songlist没有则只限制文件名
return file_name in SonglistParser.FILE_NAMES return file_name in SonglistParser.FILE_NAMES
rule = SonglistParser.songs[song_id] rule = SonglistParser.songs[song_id]
for i in range(8): for i in range(10):
if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0: if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0:
return True return True
return False return False
@staticmethod
def get_user_unlocks(user) -> set:
'''user: UserInfo类或子类的实例'''
x = SonglistParser
if user is None:
return set()
r = set()
for i in user.packs:
if i in x.pack_info:
r.update(x.pack_info[i])
if Constant.SINGLE_PACK_NAME in x.pack_info:
r.update(x.pack_info[Constant.SINGLE_PACK_NAME]
& set(user.singles))
r.update(set(i if i[-1] != '3' else i[:-1]
for i in (x.world_songs & set(user.world_songs))))
r.update(x.free_songs)
return r
def parse_one(self, song: dict) -> dict: def parse_one(self, song: dict) -> dict:
'''解析单个歌曲''' '''解析单个歌曲'''
# TODO: byd_local_unlock ???
if not 'id' in song: if not 'id' in song:
return None return {}
r = 0 r = 0
if 'remote_dl' in song and song['remote_dl']: if 'remote_dl' in song and song['remote_dl']:
r |= 16 r |= 16
@@ -69,27 +97,55 @@ class SonglistParser:
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])): if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
r |= 8 r |= 8
if 'additional_files' in song: for extra_file in song.get('additional_files', []):
if 'video.mp4' in song['additional_files']: x = extra_file['file_name']
if x == SonglistParser.FILE_NAMES[6]:
r |= 64 r |= 64
if 'video_audio.ogg' in song['additional_files']: elif x == SonglistParser.FILE_NAMES[7]:
r |= 128 r |= 128
elif x == SonglistParser.FILE_NAMES[8]:
r |= 256
elif x == SonglistParser.FILE_NAMES[9]:
r |= 512
return {song['id']: r} return {song['id']: r}
def parse_one_unlock(self, song: dict) -> None:
'''解析单个歌曲解锁方式'''
if not 'id' in song or not 'set' in song or not 'purchase' in song:
return {}
x = SonglistParser
if Constant.FREE_PACK_NAME == song['set']:
if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])):
x.world_songs.add(song['id'] + '3')
x.free_songs.add(song['id'])
return None
if song.get('world_unlock', False):
x.world_songs.add(song['id'])
if song['purchase'] == '':
return None
x.pack_info.setdefault(song['set'], set()).add(song['id'])
def parse(self) -> None: def parse(self) -> None:
'''解析songlist文件''' '''解析songlist文件'''
if not os.path.isfile(self.path): if not os.path.isfile(self.path):
return return
with open(self.path, 'r', encoding='utf-8') as f: with open(self.path, 'rb') as f:
self.data = loads(f.read()).get('songs', []) self.data = loads(f.read()).get('songs', [])
self.has_songlist = True
for x in self.data: for x in self.data:
self.songs.update(self.parse_one(x)) self.songs.update(self.parse_one(x))
self.parse_one_unlock(x)
class UserDownload: class UserDownload:
''' '''
用户下载类\ 用户下载类
properties: `user` - `User`类或子类的实例
properties: `user` - `UserInfo`类或子类的实例
''' '''
limiter = ArcLimiter( limiter = ArcLimiter(
@@ -158,8 +214,7 @@ class UserDownload:
if prefix[-1] != '/': if prefix[-1] != '/':
prefix += '/' prefix += '/'
return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}' return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
else: return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
@property @property
def hash(self) -> str: def hash(self) -> str:
@@ -198,6 +253,10 @@ class DownloadList(UserDownload):
DownloadList.get_one_song_file_names.cache_clear() DownloadList.get_one_song_file_names.cache_clear()
DownloadList.get_all_song_ids.cache_clear() DownloadList.get_all_song_ids.cache_clear()
SonglistParser.songs = {} SonglistParser.songs = {}
SonglistParser.pack_info = {}
SonglistParser.free_songs = set()
SonglistParser.world_songs = set()
SonglistParser.has_songlist = False
def clear_download_token(self) -> None: def clear_download_token(self) -> None:
'''清除过期下载链接''' '''清除过期下载链接'''
@@ -243,7 +302,7 @@ class DownloadList(UserDownload):
re['audio']['3'] = {"checksum": x.hash, "url": x.url} re['audio']['3'] = {"checksum": x.hash, "url": x.url}
else: else:
re['audio']['3'] = {"checksum": x.hash} re['audio']['3'] = {"checksum": x.hash}
elif i == 'video.mp4' or i == 'video_audio.ogg': elif i in ('video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4'):
if 'additional_files' not in re: if 'additional_files' not in re:
re['additional_files'] = [] re['additional_files'] = []
@@ -253,6 +312,7 @@ class DownloadList(UserDownload):
else: else:
re['additional_files'].append( re['additional_files'].append(
{"checksum": x.hash, 'file_name': i}) {"checksum": x.hash, 'file_name': i})
# 有参数 requirement 作用未知
else: else:
if 'chart' not in re: if 'chart' not in re:
re['chart'] = {} re['chart'] = {}
@@ -277,9 +337,19 @@ class DownloadList(UserDownload):
if not self.song_ids: if not self.song_ids:
self.song_ids = self.get_all_song_ids() self.song_ids = self.get_all_song_ids()
if Config.DOWNLOAD_FORBID_WHEN_NO_ITEM and SonglistParser.has_songlist:
# 没有歌曲时不允许下载
self.song_ids = list(SonglistParser.get_user_unlocks(
self.user) & set(self.song_ids))
for i in self.song_ids: for i in self.song_ids:
self.add_one_song(i) self.add_one_song(i)
else: else:
if Config.DOWNLOAD_FORBID_WHEN_NO_ITEM and SonglistParser.has_songlist:
# 没有歌曲时不允许下载
self.song_ids = list(SonglistParser.get_user_unlocks(
self.user) & set(self.song_ids))
for i in self.song_ids: for i in self.song_ids:
if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)): if os.path.isdir(os.path.join(Constant.SONG_FILE_FOLDER_PATH, i)):
self.add_one_song(i) self.add_one_song(i)

View File

@@ -7,11 +7,12 @@ from time import time
from traceback import format_exc from traceback import format_exc
from core.config_manager import Config from core.config_manager import Config
from core.constant import ARCAEA_SERVER_VERSION from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
from core.course import Course from core.course import Course
from core.download import DownloadList from core.download import DownloadList
from core.purchase import Purchase from core.purchase import Purchase
from core.sql import Connect, DatabaseMigrator, MemoryDatabase from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
MemoryDatabase)
from core.user import UserRegister from core.user import UserRegister
from core.util import try_rename from core.util import try_rename
@@ -41,16 +42,17 @@ class DatabaseInit:
def table_init(self) -> None: def table_init(self) -> None:
'''初始化数据库结构''' '''初始化数据库结构'''
with open(self.sql_path, 'r') as f: with open(self.sql_path, 'r', encoding='utf-8') as f:
self.c.executescript(f.read()) self.c.executescript(f.read())
self.c.execute('''insert into config values("version", :a);''', { self.c.execute('''insert into config values("version", :a);''', {
'a': ARCAEA_SERVER_VERSION}) 'a': ARCAEA_SERVER_VERSION})
def character_init(self) -> None: def character_init(self) -> None:
'''初始化搭档信息''' '''初始化搭档信息'''
uncapped_characters = self.init_data.char_core.keys()
for i in range(0, len(self.init_data.char)): for i in range(0, len(self.init_data.char)):
skill_requires_uncap = 1 if i == 2 else 0 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]: if i in uncapped_characters:
max_level = 30 max_level = 30
uncapable = 1 uncapable = 1
else: else:
@@ -97,12 +99,6 @@ class DatabaseInit:
with open(self.single_path, 'rb') as f: with open(self.single_path, 'rb') as f:
self.insert_purchase_item(load(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: def course_init(self) -> None:
'''初始化课题信息''' '''初始化课题信息'''
courses = [] courses = []
@@ -135,7 +131,7 @@ class DatabaseInit:
x._insert_user_char() x._insert_user_char()
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt, 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) 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) 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}) ''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email})
@@ -213,6 +209,29 @@ class FileChecker:
self.logger.error( self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}') f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False return False
else:
# 检查更新
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
try:
x = c.execute(
'''select value from cache where key="version"''').fetchone()
except:
x = None
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
self.logger.warning(
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
try:
self.logger.info(
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
self.update_log_database()
self.logger.info(
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH): if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库 # 新建数据库
try: try:
@@ -280,6 +299,12 @@ class FileChecker:
DatabaseMigrator(old_path, new_path).update_database() DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path) os.remove(old_path)
@staticmethod
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
'''直接更新日志数据库'''
if os.path.isfile(old_path):
LogDatabaseMigrator(old_path).update_database()
def check_song_file(self) -> bool: def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存''' '''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH) f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)

View File

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

View File

@@ -1,10 +1,12 @@
import socket import socket
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from json import dumps, loads
from core.error import ArcError, Timeout from core.error import ArcError, Timeout
from .constant import Constant from .constant import Constant
from .user import UserInfo from .user import UserInfo
from .util import aes_gcm_128_decrypt, aes_gcm_128_encrypt
socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT) socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT)
@@ -86,53 +88,77 @@ class Room:
class RemoteMultiPlayer: class RemoteMultiPlayer:
TCP_AES_KEY = Constant.LINKPLAY_TCP_SECRET_KEY.encode(
'utf-8').ljust(16, b'\x00')[:16]
def __init__(self) -> None: def __init__(self) -> None:
self.user: 'Player' = None self.user: 'Player' = None
self.room: 'Room' = None self.room: 'Room' = None
self.data_recv: tuple = None self.data_recv: 'dict | list' = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
return dict(self.room.to_dict(), **self.user.to_dict()) return dict(self.room.to_dict(), **self.user.to_dict())
@staticmethod @staticmethod
def tcp(data: str) -> str: def tcp(data: bytes) -> bytes:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((Constant.LINKPLAY_HOST, sock.connect((Constant.LINKPLAY_HOST,
Constant.LINKPLAY_TCP_PORT)) Constant.LINKPLAY_TCP_PORT))
sock.sendall(bytes(data + "\n", "utf-8"))
sock.sendall(data)
try: try:
received = str(sock.recv(1024), "utf-8").strip() cipher_len = int.from_bytes(sock.recv(8), byteorder='little')
except socket.timeout: if cipher_len > Constant.LINKPLAY_TCP_MAX_LENGTH:
raise ArcError(
'Too long body from link play server', status=400)
iv = sock.recv(12)
tag = sock.recv(12)
ciphertext = sock.recv(cipher_len)
received = aes_gcm_128_decrypt(
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
except socket.timeout as e:
raise Timeout( raise Timeout(
'Timeout when waiting for data from link play server.', status=400) 'Timeout when waiting for data from link play server.', status=400) from e
# print(received) # print(received)
return received return received
def data_swap(self, data: tuple) -> tuple: def data_swap(self, data: dict) -> dict:
received = self.tcp(Constant.LINKPLAY_AUTHENTICATION + iv, ciphertext, tag = aes_gcm_128_encrypt(
'|' + '|'.join([str(x) for x in data])) self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag[:12] + ciphertext
recv_data = self.tcp(send_data)
self.data_recv = loads(recv_data)
self.data_recv = received.split('|') code = self.data_recv['code']
if self.data_recv[0] != '0': if code != 0:
raise ArcError('Link Play error.', raise ArcError(f'Link Play error code: {code}', code, status=400)
int(self.data_recv[0]), status=400)
return self.data_recv
def create_room(self, user: 'Player' = None) -> None: def create_room(self, user: 'Player' = None) -> None:
'''创建房间''' '''创建房间'''
if user is not None: if user is not None:
self.user = user self.user = user
user.select_user_one_column('name') user.select_user_one_column('name')
self.data_swap((1, self.user.name, b64encode( self.data_swap({
self.user.song_unlock).decode('utf-8'))) 'endpoint': 'create_room',
'data': {
'name': self.user.name,
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8')
}
})
self.room = Room() self.room = Room()
self.room.room_code = self.data_recv[1] x = self.data_recv['data']
self.room.room_id = int(self.data_recv[2]) self.room.room_code = x['room_code']
self.room.room_id = int(x['room_id'])
self.room.song_unlock = self.user.song_unlock self.room.song_unlock = self.user.song_unlock
self.user.token = int(self.data_recv[3]) self.user.token = int(x['token'])
self.user.key = b64decode(self.data_recv[4]) self.user.key = b64decode(x['key'])
self.user.player_id = int(self.data_recv[5]) self.user.player_id = int(x['player_id'])
def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None: def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None:
'''加入房间''' '''加入房间'''
@@ -142,23 +168,49 @@ class RemoteMultiPlayer:
self.room = room self.room = room
self.user.select_user_one_column('name') self.user.select_user_one_column('name')
self.data_swap( self.data_swap({
(2, self.user.name, b64encode(self.user.song_unlock).decode('utf-8'), room.room_code)) 'endpoint': 'join_room',
self.room.room_code = self.data_recv[1] 'data': {
self.room.room_id = int(self.data_recv[2]) 'name': self.user.name,
self.room.song_unlock = b64decode(self.data_recv[6]) 'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
self.user.token = int(self.data_recv[3]) 'room_code': self.room.room_code
self.user.key = b64decode(self.data_recv[4]) }
self.user.player_id = int(self.data_recv[5]) })
x = self.data_recv['data']
self.room.room_code = x['room_code']
self.room.room_id = int(x['room_id'])
self.room.song_unlock = b64decode(x['song_unlock'])
self.user.token = int(x['token'])
self.user.key = b64decode(x['key'])
self.user.player_id = int(x['player_id'])
def update_room(self, user: 'Player' = None) -> None: def update_room(self, user: 'Player' = None) -> None:
'''更新房间''' '''更新房间'''
if user is not None: if user is not None:
self.user = user self.user = user
self.data_swap((3, self.user.token)) self.data_swap({
'endpoint': 'update_room',
'data': {
'token': self.user.token
}
})
self.room = Room() self.room = Room()
self.room.room_code = self.data_recv[1] x = self.data_recv['data']
self.room.room_id = int(self.data_recv[2]) self.room.room_code = x['room_code']
self.room.song_unlock = b64decode(self.data_recv[5]) self.room.room_id = int(x['room_id'])
self.user.key = b64decode(self.data_recv[3]) self.room.song_unlock = b64decode(x['song_unlock'])
self.user.player_id = int(self.data_recv[4]) self.user.key = b64decode(x['key'])
self.user.player_id = int(x['player_id'])
def get_rooms(self, offset=0, limit=50) -> dict:
'''获取房间列表'''
self.data_swap({
'endpoint': 'get_rooms',
'data': {
'offset': offset,
'limit': limit
}
})
return self.data_recv['data']

View File

@@ -1,4 +1,5 @@
from .download import DownloadList from .download import DownloadList
from .error import NoData
from .save import SaveData from .save import SaveData
from .score import Score from .score import Score
from .sql import Connect, Sql from .sql import Connect, Sql
@@ -52,8 +53,7 @@ class RefreshAllScoreRating(BaseOperation):
where_values = [] where_values = []
for k in y: for k in y:
ptt = Score.calculate_rating(defnum, k[1]) ptt = Score.calculate_rating(defnum, k[1])
if ptt < 0: ptt = max(ptt, 0)
ptt = 0
values.append((ptt,)) values.append((ptt,))
where_values.append((k[0], i[0], j)) where_values.append((k[0], i[0], j))
if values: if values:
@@ -75,7 +75,7 @@ class RefreshSongFileCache(BaseOperation):
class SaveUpdateScore(BaseOperation): class SaveUpdateScore(BaseOperation):
''' '''
云存档更新成绩,是覆盖式更新\ 云存档更新成绩,是覆盖式更新
提供user参数时只更新该用户的成绩否则更新所有用户的成绩 提供user参数时只更新该用户的成绩否则更新所有用户的成绩
''' '''
_name = 'save_update_score' _name = 'save_update_score'
@@ -124,8 +124,7 @@ class SaveUpdateScore(BaseOperation):
if i['song_id'] in song_chart_const: if i['song_id'] in song_chart_const:
rating = Score.calculate_rating( rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
if rating < 0: rating = max(rating, 0)
rating = 0
y = f'{i["song_id"]}{i["difficulty"]}' y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state: if y in clear_state:
@@ -142,7 +141,7 @@ class SaveUpdateScore(BaseOperation):
def _all_update(self): def _all_update(self):
with Connect() as c: with Connect() as c:
c.execute( c.execute(
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''') '''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]] song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
for i in c.fetchall()} # chart const * 10 for i in c.fetchall()} # chart const * 10
c.execute('''select user_id from user_save''') c.execute('''select user_id from user_save''')
@@ -161,8 +160,7 @@ class SaveUpdateScore(BaseOperation):
if i['song_id'] in song_chart_const: if i['song_id'] in song_chart_const:
rating = Score.calculate_rating( rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
if rating < 0: rating = max(rating, 0)
rating = 0
y = f'{i["song_id"]}{i["difficulty"]}' y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state: if y in clear_state:
@@ -179,7 +177,7 @@ class SaveUpdateScore(BaseOperation):
class UnlockUserItem(BaseOperation): class UnlockUserItem(BaseOperation):
''' '''
全解锁/锁定用户物品\ 全解锁/锁定用户物品
提供user参数时只更新该用户的否则更新所有用户的 提供user参数时只更新该用户的否则更新所有用户的
''' '''
_name = 'unlock_user_item' _name = 'unlock_user_item'
@@ -197,7 +195,7 @@ class UnlockUserItem(BaseOperation):
self.user.user_id = int(user_id) self.user.user_id = int(user_id)
if method in ['unlock', 'lock']: if method in ['unlock', 'lock']:
self.method = method self.method = method
if isinstance(item_types, list) and all([i in self.ALLOW_TYPES for i in item_types]): if isinstance(item_types, list) and all(i in self.ALLOW_TYPES for i in item_types):
self.item_types = item_types self.item_types = item_types
def run(self): def run(self):
@@ -214,6 +212,11 @@ class UnlockUserItem(BaseOperation):
def _one_user_insert(self): def _one_user_insert(self):
with Connect() as c: with Connect() as c:
c.execute(
'''select exists(select * from user where user_id = ?)''', (self.user.user_id,))
if not c.fetchone()[0]:
raise NoData(
f'No such user: `{self.user.user_id}`', api_error_code=-110)
c.execute( c.execute(
f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types) f'''select item_id, type from item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types)
sql_list = [(self.user.user_id, i[0], i[1]) sql_list = [(self.user.user_id, i[0], i[1])

View File

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

View File

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

View File

@@ -7,8 +7,9 @@ from .user import UserInfo
class RankList: class RankList:
''' '''
排行榜类\ 排行榜类
默认limit=20limit<0认为是all\ 默认limit=20limit<0认为是all
property: `user` - `User`类或者子类的实例 property: `user` - `User`类或者子类的实例
''' '''
@@ -85,7 +86,8 @@ class RankList:
@staticmethod @staticmethod
def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION): def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION):
''' '''
计算我的排名中的查询参数\ 计算我的排名中的查询参数
returns: returns:
`sql_limit`: int - 查询limit参数 `sql_limit`: int - 查询limit参数
`sql_offset`: int - 查询offset参数 `sql_offset`: int - 查询offset参数
@@ -103,7 +105,7 @@ class RankList:
need_myself = True need_myself = True
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名 elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
sql_offset = amount - all_limit sql_offset = amount - all_limit
elif my_rank >= max_local_position and my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名 elif max_local_position <= my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
sql_offset = my_rank - max_local_position sql_offset = my_rank - max_local_position
else: # 我已经忘了这是什么了 else: # 我已经忘了这是什么了
sql_offset = max_global_position - all_limit sql_offset = max_global_position - all_limit

View File

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

View File

@@ -11,6 +11,8 @@ class SaveData:
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.user = None
self.scores_data = [] self.scores_data = []
self.clearlamps_data = [] self.clearlamps_data = []
self.clearedsongs_data = [] self.clearedsongs_data = []
@@ -94,13 +96,16 @@ class SaveData:
i['c'] = True i['c'] = True
i['r'] = True i['r'] = True
for i in self.unlocklist_data: for i in self.unlocklist_data:
if i['unlock_key'][-3:] == '101': x = i['unlock_key']
if x[-3:] == '101':
i['complete'] = 100 i['complete'] = 100
elif i['unlock_key'][:16] == 'aegleseeker|2|3|': elif x[:16] == 'aegleseeker|2|3|':
i['complete'] = 10 i['complete'] = 10
elif i['unlock_key'] == 'saikyostronger|2|3|einherjar|2': elif x == 'saikyostronger|2|3|einherjar|2':
i['complete'] = 6 i['complete'] = 6
elif i['unlock_key'] == 'saikyostronger|2|3|laqryma|2': elif x == 'saikyostronger|2|3|laqryma|2':
i['complete'] = 3
elif x[-5:-2] == '109':
i['complete'] = 3 i['complete'] = 3
else: else:
i['complete'] = 1 i['complete'] = 1
@@ -127,7 +132,7 @@ class SaveData:
'Property `%s` is not found in the instance of `SaveData` class.' % key) 'Property `%s` is not found in the instance of `SaveData` class.' % key)
if md5(value) == checksum: if md5(value) == checksum:
if key == 'installid_data' or key == 'devicemodelname_data' or key == 'finalestate_data': if key in ('installid_data', 'devicemodelname_data', 'finalestate_data'):
self.__dict__[key] = json.loads(value)['val'] self.__dict__[key] = json.loads(value)['val']
else: else:
self.__dict__[key] = json.loads(value)[''] self.__dict__[key] = json.loads(value)['']

View File

@@ -2,18 +2,22 @@ from base64 import b64encode
from os import urandom from os import urandom
from time import time from time import time
from .bgtask import BGTask, logdb_execute
from .config_manager import Config
from .constant import Constant from .constant import Constant
from .course import CoursePlay from .course import CoursePlay
from .error import NoData, StaminaNotEnough from .error import NoData, StaminaNotEnough
from .item import ItemCore from .item import ItemCore
from .song import Chart from .song import Chart
from .sql import Connect, Query, Sql from .sql import Connect, Query, Sql
from .util import md5 from .util import get_today_timestamp, md5
from .world import WorldPlay from .world import WorldPlay
class Score: class Score:
def __init__(self) -> None: def __init__(self) -> None:
self.c = None
self.song: 'Chart' = Chart() self.song: 'Chart' = Chart()
self.score: int = None self.score: int = None
self.shiny_perfect_count: int = None self.shiny_perfect_count: int = None
@@ -45,18 +49,17 @@ class Score:
'''分数转换为评级''' '''分数转换为评级'''
if score >= 9900000: # EX+ if score >= 9900000: # EX+
return 6 return 6
elif 9800000 <= score < 9900000: # EX if score >= 9800000: # EX
return 5 return 5
elif 9500000 <= score < 9800000: # AA if score >= 9500000: # AA
return 4 return 4
elif 9200000 <= score < 9500000: # A if score >= 9200000: # A
return 3 return 3
elif 8900000 <= score < 9200000: # B if score >= 8900000: # B
return 2 return 2
elif 8600000 <= score < 8900000: # C if score >= 8600000: # C
return 1 return 1
else: return 0
return 0
@property @property
def song_grade(self) -> int: def song_grade(self) -> int:
@@ -67,21 +70,24 @@ class Score:
'''clear_type转换为成绩状态用数字大小标识便于比较''' '''clear_type转换为成绩状态用数字大小标识便于比较'''
if clear_type == 3: # PM if clear_type == 3: # PM
return 5 return 5
elif clear_type == 2: # FC if clear_type == 2: # FC
return 4 return 4
elif clear_type == 5: # Hard Clear if clear_type == 5: # Hard Clear
return 3 return 3
elif clear_type == 1: # Clear if clear_type == 1: # Clear
return 2 return 2
elif clear_type == 4: # Easy Clear if clear_type == 4: # Easy Clear
return 1 return 1
else: # Track Lost return 0 # Track Lost
return 0
@property @property
def song_state(self) -> int: def song_state(self) -> int:
return self.get_song_state(self.clear_type) return self.get_song_state(self.clear_type)
@property
def all_note_count(self) -> int:
return self.perfect_count + self.near_count + self.miss_count
@property @property
def is_valid(self) -> bool: def is_valid(self) -> bool:
'''分数有效性检查''' '''分数有效性检查'''
@@ -90,7 +96,7 @@ class Score:
if self.song.difficulty not in (0, 1, 2, 3): if self.song.difficulty not in (0, 1, 2, 3):
return False return False
all_note = self.perfect_count + self.near_count + self.miss_count all_note = self.all_note_count
if all_note == 0: if all_note == 0:
return False return False
@@ -112,8 +118,7 @@ class Score:
ptt = defnum + 2 ptt = defnum + 2
elif score < 9800000: elif score < 9800000:
ptt = defnum + (score-9500000) / 300000 ptt = defnum + (score-9500000) / 300000
if ptt < 0: ptt = max(ptt, 0)
ptt = 0
else: else:
ptt = defnum + 1 + (score-9800000) / 200000 ptt = defnum + 1 + (score-9800000) / 200000
@@ -209,6 +214,7 @@ class UserPlay(UserScore):
self.stamina_multiply: int = None self.stamina_multiply: int = None
self.fragment_multiply: int = None self.fragment_multiply: int = None
self.prog_boost_multiply: int = None self.prog_boost_multiply: int = None
self.beyond_boost_gauge_usage: int = None
self.ptt: Potential = None # 临时用来计算用户ptt的 self.ptt: Potential = None # 临时用来计算用户ptt的
self.world_play: 'WorldPlay' = None self.world_play: 'WorldPlay' = None
@@ -216,7 +222,13 @@ class UserPlay(UserScore):
self.course_play_state: int = None self.course_play_state: int = None
self.course_play: 'CoursePlay' = None self.course_play: 'CoursePlay' = None
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
self.skill_cytusii_flag: str = None
self.highest_health: int = None
self.lowest_health: int = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
# 不能super
if self.is_world_mode is None or self.course_play_state is None: if self.is_world_mode is None or self.course_play_state is None:
return {} return {}
if self.course_play_state == 4: if self.course_play_state == 4:
@@ -228,7 +240,7 @@ class UserPlay(UserScore):
r['user_rating'] = self.user.rating_ptt r['user_rating'] = self.user.rating_ptt
r['finale_challenge_higher'] = self.rating > self.ptt.value r['finale_challenge_higher'] = self.rating > self.ptt.value
r['global_rank'] = self.user.global_rank r['global_rank'] = self.user.global_rank
r['finale_play_value'] = self.rating * 5 # emmmm r['finale_play_value'] = 9.065 * self.rating ** 0.5 # by Lost-MSth
return r return r
@property @property
@@ -248,10 +260,15 @@ class UserPlay(UserScore):
if songfile_hash and songfile_hash != self.song_hash: if songfile_hash and songfile_hash != self.song_hash:
return False return False
x = self.song_token + self.song_hash + self.song.song_id + str(self.song.difficulty) + str(self.score) + str(self.shiny_perfect_count) + str( x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
self.perfect_count) + str(self.near_count) + str(self.miss_count) + str(self.health) + str(self.modifier) + str(self.clear_type) if self.combo_interval_bonus is not None:
y = str(self.user.user_id) + self.song_hash if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
return False
x = x + str(self.combo_interval_bonus)
y = f'{self.user.user_id}{self.song_hash}'
checksum = md5(x+md5(y)) checksum = md5(x+md5(y))
if checksum != self.submission_hash: if checksum != self.submission_hash:
return False return False
@@ -285,24 +302,31 @@ class UserPlay(UserScore):
self.stamina_multiply = int(x[8]) self.stamina_multiply = int(x[8])
self.fragment_multiply = int(x[9]) self.fragment_multiply = int(x[9])
self.prog_boost_multiply = int(x[10]) self.prog_boost_multiply = int(x[10])
self.beyond_boost_gauge_usage = int(x[11])
self.skill_cytusii_flag = x[12]
self.is_world_mode = True self.is_world_mode = True
self.course_play_state = -1 self.course_play_state = -1
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0) -> None: def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0, skill_cytusii_flag: str = None) -> None:
self.song_token = b64encode(urandom(64)).decode() self.song_token = b64encode(urandom(64)).decode()
self.stamina_multiply = int(stamina_multiply) self.stamina_multiply = int(stamina_multiply)
self.fragment_multiply = int(fragment_multiply) self.fragment_multiply = int(fragment_multiply)
self.prog_boost_multiply = int(prog_boost_multiply) self.prog_boost_multiply = int(prog_boost_multiply)
if self.prog_boost_multiply != 0: self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
self.c.execute('''select prog_boost from user where user_id=:a''', { self.skill_cytusii_flag = skill_cytusii_flag
if self.prog_boost_multiply != 0 or self.beyond_boost_gauge_usage != 0:
self.c.execute('''select prog_boost, beyond_boost_gauge from user where user_id=:a''', {
'a': self.user.user_id}) 'a': self.user.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if x and x[0] == 300: if x:
self.prog_boost_multiply = 300 self.prog_boost_multiply = 300 if x[0] == 300 else 0
if x[1] < self.beyond_boost_gauge_usage or self.beyond_boost_gauge_usage not in (100, 200):
# 注意偷懒了没判断是否是beyond图
self.beyond_boost_gauge_usage = 0
self.clear_play_state() self.clear_play_state()
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f)''', { self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g,:h)''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 't': self.song_token}) 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 't': self.song_token})
self.user.select_user_about_current_map() self.user.select_user_about_current_map()
self.user.current_map.select_map_info() self.user.current_map.select_map_info()
@@ -334,8 +358,8 @@ class UserPlay(UserScore):
self.course_play.score = 0 self.course_play.score = 0
self.course_play.clear_type = 3 # 设置为PM即最大值 self.course_play.clear_type = 3 # 设置为PM即最大值
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0)''', (self.song_token, self.user.user_id, self.song.song_id, self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,0,"")''', (self.song_token, self.user.user_id, self.song.song_id,
self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type)) self.song.difficulty, self.course_play.course_id, self.course_play_state, self.course_play.score, self.course_play.clear_type))
self.user.select_user_about_stamina() self.user.select_user_about_stamina()
if use_course_skip_purchase: if use_course_skip_purchase:
x = ItemCore(self.c) x = ItemCore(self.c)
@@ -368,8 +392,8 @@ class UserPlay(UserScore):
'''更新此分数对应用户的recent30''' '''更新此分数对应用户的recent30'''
old_recent_10 = self.ptt.recent_10 old_recent_10 = self.ptt.recent_10
if self.is_protected: if self.is_protected:
old_r30 = [x for x in self.ptt.r30] old_r30 = self.ptt.r30.copy()
old_s30 = [x for x in self.ptt.s30] old_s30 = self.ptt.s30.copy()
# 寻找pop位置 # 寻找pop位置
songs = list(set(self.ptt.s30)) songs = list(set(self.ptt.s30))
@@ -410,9 +434,20 @@ class UserPlay(UserScore):
def record_score(self) -> None: def record_score(self) -> None:
'''向log数据库记录分数请注意列名不同''' '''向log数据库记录分数请注意列名不同'''
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2: logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
c2.execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def record_rating_ptt(self, user_rating_ptt: float) -> None:
'''向log数据库记录用户ptt变化'''
today_timestamp = get_today_timestamp()
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
self.user.user_id, today_timestamp)).fetchone()
old_ptt = 0 if old_ptt is None else old_ptt[0]
if old_ptt != user_rating_ptt:
c2.execute('''insert or replace into user_rating values(?,?,?)''',
(self.user.user_id, today_timestamp, user_rating_ptt))
def upload_score(self) -> None: def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算''' '''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
@@ -457,7 +492,9 @@ class UserPlay(UserScore):
self.update_recent30() self.update_recent30()
# 总PTT更新 # 总PTT更新
self.user.rating_ptt = int(self.ptt.value * 100) user_rating_ptt = self.ptt.value
self.user.rating_ptt = int(user_rating_ptt * 100)
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', { self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
'a': self.user.rating_ptt, 'b': self.user.user_id}) 'a': self.user.rating_ptt, 'b': self.user.user_id})
@@ -473,7 +510,8 @@ class UserPlay(UserScore):
class Potential: class Potential:
''' '''
用户潜力值计算处理类\ 用户潜力值计算处理类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''
@@ -481,8 +519,8 @@ class Potential:
self.c = c self.c = c
self.user = user self.user = user
self.r30: list = None self.r30: 'list[float]' = None
self.s30: list = None self.s30: 'list[str]' = None
self.songs_selected: list = None self.songs_selected: list = None
self.b30: list = None self.b30: list = None
@@ -497,7 +535,7 @@ class Potential:
'''获取用户best30的总潜力值''' '''获取用户best30的总潜力值'''
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', { self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
'a': self.user.user_id}) 'a': self.user.user_id})
return sum([x[0] for x in self.c.fetchall()]) return sum(x[0] for x in self.c.fetchall())
def select_recent_30(self) -> None: def select_recent_30(self) -> None:
'''获取用户recent30数据''' '''获取用户recent30数据'''
@@ -572,7 +610,8 @@ class Potential:
class UserScoreList: class UserScoreList:
''' '''
用户分数查询类\ 用户分数查询类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''

View File

@@ -7,6 +7,8 @@ class Chart:
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None: def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
self.c = c self.c = c
self.song_id: str = None
self.difficulty: int = None
self.set_chart(song_id, difficulty) self.set_chart(song_id, difficulty)
self.defnum: int = None self.defnum: int = None
self.song_name: str = None self.song_name: str = None

View File

@@ -1,8 +1,10 @@
import os
import sqlite3 import sqlite3
import traceback import traceback
from atexit import register from atexit import register
from .constant import Constant from .config_manager import Config
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
from .error import ArcError, InputError from .error import ArcError, InputError
@@ -12,8 +14,8 @@ class Connect:
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None: def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
""" """
数据库连接默认连接arcaea_database.db\ 数据库连接默认连接arcaea_database.db
接受:文件路径\ 接受:文件路径
返回sqlite3连接操作对象 返回sqlite3连接操作对象
""" """
self.file_path = file_path self.file_path = file_path
@@ -21,6 +23,9 @@ class Connect:
if logger is not None: if logger is not None:
self.logger = logger self.logger = logger
self.conn: sqlite3.Connection = None
self.c: sqlite3.Cursor = None
def __enter__(self) -> sqlite3.Cursor: def __enter__(self) -> sqlite3.Cursor:
if self.in_memory: if self.in_memory:
self.conn = sqlite3.connect( self.conn = sqlite3.connect(
@@ -144,19 +149,19 @@ class Query:
raise InputError(api_error_code=-104) raise InputError(api_error_code=-104)
self.__sort = sort self.__sort = sort
def set_value(self, limit=-1, offset=0, query={}, fuzzy_query={}, sort=[]) -> None: def set_value(self, limit=-1, offset=0, query=None, fuzzy_query=None, sort=None) -> None:
self.limit = limit self.limit = limit
self.offset = offset self.offset = offset
self.query = query self.query = query if query is not None else {}
self.fuzzy_query = fuzzy_query self.fuzzy_query = fuzzy_query if fuzzy_query is not None else {}
self.sort = sort self.sort = sort if sort is not None else []
def from_dict(self, d: dict) -> 'Query': def from_dict(self, d: dict) -> 'Query':
self.set_value(d.get('limit', -1), d.get('offset', 0), self.set_value(d.get('limit', -1), d.get('offset', 0),
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', [])) d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
return self return self
def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = [], fuzzy_query: dict = {}) -> 'Query': def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = None, fuzzy_query: dict = None) -> 'Query':
self.set_value(limit, offset, query, fuzzy_query, sort) self.set_value(limit, offset, query, fuzzy_query, sort)
return self return self
@@ -170,7 +175,7 @@ class Sql:
self.c = c self.c = c
@staticmethod @staticmethod
def get_select_sql(table_name: str, target_column: list = [], query: 'Query' = None): def get_select_sql(table_name: str, target_column: list = None, query: 'Query' = None):
'''拼接单表内行查询单句sql语句返回语句和参数列表''' '''拼接单表内行查询单句sql语句返回语句和参数列表'''
sql_list = [] sql_list = []
if not target_column: if not target_column:
@@ -210,8 +215,10 @@ class Sql:
return sql, sql_list return sql, sql_list
@staticmethod @staticmethod
def get_insert_sql(table_name: str, key: list = [], value_len: int = None, insert_type: str = None) -> str: def get_insert_sql(table_name: str, key: list = None, value_len: int = None, insert_type: str = None) -> str:
'''拼接insert语句请注意只返回sql语句insert_type为replace或ignore''' '''拼接insert语句请注意只返回sql语句insert_type为replace或ignore'''
if key is None:
key = []
insert_type = 'replace' if insert_type in [ insert_type = 'replace' if insert_type in [
'replace', 'R', 'r', 'REPLACE'] else 'ignore' '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)) + ')' 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)) + ')'
@@ -281,13 +288,13 @@ class Sql:
return sql, sql_list return sql, sql_list
def select(self, table_name: str, target_column: list = [], query: 'Query' = None) -> list: def select(self, table_name: str, target_column: list = None, query: 'Query' = None) -> list:
'''单表内行select单句sql语句返回fetchall数据''' '''单表内行select单句sql语句返回fetchall数据'''
sql, sql_list = self.get_select_sql(table_name, target_column, query) sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute(sql, sql_list) self.c.execute(sql, sql_list)
return self.c.fetchall() return self.c.fetchall()
def select_exists(self, table_name: str, target_column: list = [], query: 'Query' = None) -> bool: def select_exists(self, table_name: str, target_column: list = None, query: 'Query' = None) -> bool:
'''单表内行select exists单句sql语句返回bool值''' '''单表内行select exists单句sql语句返回bool值'''
sql, sql_list = self.get_select_sql(table_name, target_column, query) sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute('select exists(' + sql + ')', sql_list) self.c.execute('select exists(' + sql + ')', sql_list)
@@ -329,7 +336,7 @@ class Sql:
pk = [] pk = []
name = [] name = []
self.c.execute('''pragma table_info ("%s")''' % table_name) # 这里无法参数化 self.c.execute(f'''pragma table_info ("{table_name}")''') # 这里无法参数化
x = self.c.fetchall() x = self.c.fetchall()
if x: if x:
for i in x: for i in x:
@@ -363,8 +370,10 @@ class DatabaseMigrator:
if db1_pk != db2_pk: if db1_pk != db2_pk:
return False return False
sql2.insert_many(table_name, [], sql1.select( public_column = list(filter(lambda x: x in db2_name, db1_name))
table_name, list(filter(lambda x: x in db2_name, db1_name))), insert_type='replace')
sql2.insert_many(table_name, public_column, sql1.select(
table_name, public_column), insert_type='replace')
return True return True
@@ -378,16 +387,9 @@ class DatabaseMigrator:
c.execute('''delete from user_char_full''') c.execute('''delete from user_char_full''')
for i in x: for i in x:
exp = 25000 if i[1] == 30 else 10000 exp = 25000 if i[1] == 30 else 10000
c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [ c.executemany('''insert into user_char_full values(?,?,?,?,?,?,0)''', [
(j[0], i[0], i[1], exp, i[2], 0) for j in y]) (j[0], i[0], i[1], exp, i[2], 0) for j in y])
@staticmethod
def update_user_epilogue(c) -> None:
'''给用户添加epilogue包'''
c.execute('''select user_id from user''')
Sql(c).insert_many('user_item', [], [(i[0], 'epilogue', 'pack', 1)
for i in c.fetchall()], insert_type='ignore')
def update_database(self) -> None: def update_database(self) -> None:
''' '''
将c1数据库不存在数据加入或覆盖到c2数据库上 将c1数据库不存在数据加入或覆盖到c2数据库上
@@ -395,14 +397,38 @@ class DatabaseMigrator:
''' '''
with Connect(self.c2_path) as c2: with Connect(self.c2_path) as c2:
with Connect(self.c1_path) as c1: with Connect(self.c1_path) as c1:
[self.update_one_table(c1, c2, i) for i in Constant.DATABASE_MIGRATE_TABLES:
for i in Constant.DATABASE_MIGRATE_TABLES] self.update_one_table(c1, c2, i)
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA: if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
self.update_one_table(c1, c2, 'character') self.update_one_table(c1, c2, 'character')
self.update_user_char_full(c2) # 更新user_char_full self.update_user_char_full(c2) # 更新user_char_full
self.update_user_epilogue(c2) # 更新user的epilogue
class LogDatabaseMigrator:
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
self.c1_path = c1_path
# self.c2_path = c2_path
self.init_folder_path = Config.DATABASE_INIT_PATH
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_update(self) -> None:
'''直接更新数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
def update_database(self) -> None:
with Connect(self.c1_path) as c:
self.c = c
self.table_update()
class MemoryDatabase: class MemoryDatabase:
@@ -412,7 +438,7 @@ class MemoryDatabase:
self.c = self.conn.cursor() self.c = self.conn.cursor()
self.c.execute('''PRAGMA journal_mode = OFF''') self.c.execute('''PRAGMA journal_mode = OFF''')
self.c.execute('''PRAGMA synchronous = 0''') self.c.execute('''PRAGMA synchronous = 0''')
self.c.execute('''create table if not exists download_token(user_id int, 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));''') song_id text,file_name text,token text,time int,primary key(user_id, song_id, file_name));''')
self.c.execute( self.c.execute(
'''create index if not exists download_token_1 on download_token (song_id, file_name);''') '''create index if not exists download_token_1 on download_token (song_id, file_name);''')

View File

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

View File

@@ -2,6 +2,7 @@ import base64
import hashlib import hashlib
import time import time
from os import urandom from os import urandom
from random import randint
from .character import UserCharacter, UserCharacterList from .character import UserCharacter, UserCharacterList
from .config_manager import Config from .config_manager import Config
@@ -100,7 +101,6 @@ class UserRegister(User):
def _build_user_code(self): def _build_user_code(self):
# 生成9位的user_code用的自然是随机 # 生成9位的user_code用的自然是随机
from random import randint
random_times = 0 random_times = 0
while random_times <= 1000: while random_times <= 1000:
@@ -127,9 +127,9 @@ class UserRegister(User):
def _insert_user_char(self): def _insert_user_char(self):
# 为用户添加初始角色 # 为用户添加初始角色
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''', self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
(self.user_id, 0, 1, 0, 0, 0)) (self.user_id, 0, 1, 0, 0, 0))
self.c.execute('''insert into user_char values(?,?,?,?,?,?)''', self.c.execute('''insert into user_char values(?,?,?,?,?,?,0)''',
(self.user_id, 1, 1, 0, 0, 0)) (self.user_id, 1, 1, 0, 0, 0))
self.c.execute( self.c.execute(
'''select character_id, max_level, is_uncapped from character''') '''select character_id, max_level, is_uncapped from character''')
@@ -137,7 +137,7 @@ class UserRegister(User):
if x: if x:
for i in x: for i in x:
exp = 25000 if i[1] == 30 else 10000 exp = 25000 if i[1] == 30 else 10000
self.c.execute('''insert into user_char_full values(?,?,?,?,?,?)''', self.c.execute('''insert into user_char_full values(?,?,?,?,?,?,0)''',
(self.user_id, i[0], i[1], exp, i[2], 0)) (self.user_id, i[0], i[1], exp, i[2], 0))
def register(self): def register(self):
@@ -148,7 +148,7 @@ class UserRegister(User):
self._build_user_id() self._build_user_id()
self._insert_user_char() self._insert_user_char()
self.c.execute('''insert into user(user_id, name, password, join_date, user_code, rating_ptt, 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) 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) values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email)
''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email}) ''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email})
@@ -237,7 +237,8 @@ class UserLogin(User):
self.set_ip(ip) self.set_ip(ip)
if not self.limiter.hit(name): if not self.limiter.hit(name):
raise RateLimit('Too many login attempts.', 123, -203) raise RateLimit(
f'Too many login attempts of username `{name}`', 123, -203)
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', { self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': self.name}) 'name': self.name})
@@ -251,7 +252,7 @@ class UserLogin(User):
# 自动封号检查 # 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1]) ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > self.now: if ban_timestamp > self.now:
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={ raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
'remaining_ts': ban_timestamp-self.now}) 'remaining_ts': ban_timestamp-self.now})
if x[1] == '': if x[1] == '':
@@ -260,7 +261,7 @@ class UserLogin(User):
f'The account `{self.user_id}` has been banned.', 106) f'The account `{self.user_id}` has been banned.', 106)
if x[1] != self.hash_pwd: if x[1] != self.hash_pwd:
raise NoAccess('Wrong password.', 104) raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
self.token = base64.b64encode(hashlib.sha256( self.token = base64.b64encode(hashlib.sha256(
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode() (str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
@@ -305,9 +306,12 @@ class UserInfo(User):
self.recent_score = Score() self.recent_score = Score()
self.favorite_character = None self.favorite_character = None
self.max_stamina_notification_enabled = False self.max_stamina_notification_enabled = False
self.prog_boost = 0 self.prog_boost: int = 0
self.beyond_boost_gauge: float = 0
self.next_fragstam_ts: int = None self.next_fragstam_ts: int = None
self.world_mode_locked_end_ts: int = None self.world_mode_locked_end_ts: int = None
self.current_map: 'Map' = None
self.stamina: 'UserStamina' = None
self.__cores: list = None self.__cores: list = None
self.__packs: list = None self.__packs: list = None
@@ -405,7 +409,7 @@ class UserInfo(User):
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''', self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
{'x': i[0], 'y': self.user_id}) {'x': i[0], 'y': self.user_id})
is_mutual = True if self.c.fetchone() == (1,) else False is_mutual = self.c.fetchone() == (1,)
you = UserOnline(self.c, i[0]) you = UserOnline(self.c, i[0])
you.select_user() you.select_user()
@@ -496,6 +500,7 @@ class UserInfo(User):
"is_skill_sealed": self.is_skill_sealed, "is_skill_sealed": self.is_skill_sealed,
"current_map": self.current_map.map_id, "current_map": self.current_map.map_id,
"prog_boost": self.prog_boost, "prog_boost": self.prog_boost,
"beyond_boost_gauge": self.beyond_boost_gauge,
"next_fragstam_ts": self.next_fragstam_ts, "next_fragstam_ts": self.next_fragstam_ts,
"max_stamina_ts": self.stamina.max_stamina_ts, "max_stamina_ts": self.stamina.max_stamina_ts,
"stamina": self.stamina.stamina, "stamina": self.stamina.stamina,
@@ -553,6 +558,7 @@ class UserInfo(User):
self.stamina = UserStamina(self.c, self) self.stamina = UserStamina(self.c, self)
self.stamina.set_value(x[32], x[33]) self.stamina.set_value(x[32], x[33])
self.world_mode_locked_end_ts = x[34] if x[34] else -1 self.world_mode_locked_end_ts = x[34] if x[34] else -1
self.beyond_boost_gauge = x[35] if x[35] else 0
return self return self
@@ -610,7 +616,7 @@ class UserInfo(User):
查询user表有关世界模式打歌的信息 查询user表有关世界模式打歌的信息
''' '''
self.c.execute( self.c.execute(
'''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts from user where user_id=?''', (self.user_id,)) '''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge from user where user_id=?''', (self.user_id,))
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -623,6 +629,7 @@ class UserInfo(User):
self.character.is_uncapped_override = x[5] == 1 self.character.is_uncapped_override = x[5] == 1
self.current_map = UserMap(self.c, x[6], self) self.current_map = UserMap(self.c, x[6], self)
self.world_mode_locked_end_ts = x[7] if x[7] else -1 self.world_mode_locked_end_ts = x[7] if x[7] else -1
self.beyond_boost_gauge = x[8] if x[8] else 0
@property @property
def global_rank(self) -> int: def global_rank(self) -> int:
@@ -656,16 +663,16 @@ class UserInfo(User):
score_sum = 0 score_sum = 0
if len(song_list_ftr) >= 2: if len(song_list_ftr) >= 2:
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({0})'''.format( self.c.execute(
','.join(['?']*(len(song_list_ftr)-1))), tuple(song_list_ftr)) f'''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({','.join(['?']*(len(song_list_ftr)-1))})''', tuple(song_list_ftr))
x = self.c.fetchone() x = self.c.fetchone()
if x[0] is not None: if x[0] is not None:
score_sum += x[0] score_sum += x[0]
if len(song_list_byn) >= 2: if len(song_list_byn) >= 2:
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({0})'''.format( self.c.execute(
','.join(['?']*(len(song_list_byn)-1))), tuple(song_list_byn)) f'''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({','.join(['?']*(len(song_list_byn)-1))})''', tuple(song_list_byn))
x = self.c.fetchone() x = self.c.fetchone()
if x[0] is not None: if x[0] is not None:
@@ -678,13 +685,13 @@ class UserInfo(User):
def select_user_one_column(self, column_name: str, default_value=None) -> None: def select_user_one_column(self, column_name: str, default_value=None) -> None:
''' '''
查询user表的某个属性\ 查询user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
self.c.execute('''select %s from user where user_id = :a''' % self.c.execute(f'''select {column_name} from user where user_id = :a''', {
column_name, {'a': self.user_id}) 'a': self.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -693,15 +700,15 @@ class UserInfo(User):
def update_user_one_column(self, column_name: str, value=None) -> None: def update_user_one_column(self, column_name: str, value=None) -> None:
''' '''
更新user表的某个属性\ 更新user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
if value is not None: if value is not None:
self.__dict__[column_name] = value self.__dict__[column_name] = value
self.c.execute('''update user set %s = :a where user_id = :b''' % self.c.execute(f'''update user set {column_name} = :a where user_id = :b''', {
column_name, {'a': self.__dict__[column_name], 'b': self.user_id}) 'a': self.__dict__[column_name], 'b': self.user_id})
class UserOnline(UserInfo): class UserOnline(UserInfo):
@@ -757,7 +764,7 @@ class UserChanger(UserInfo, UserRegister):
if columns is not None: if columns is not None:
d = {} d = {}
for column in columns: for column in columns:
if column == 'password': if column == 'password' and self.password != '':
d[column] = self.hash_pwd d[column] = self.hash_pwd
else: else:
d[column] = self.__dict__[column] d[column] = self.__dict__[column]

View File

@@ -1,5 +1,28 @@
import hashlib import hashlib
import os import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from datetime import date
from time import mktime
def aes_gcm_128_encrypt(key, plaintext, associated_data):
iv = os.urandom(12)
encryptor = Cipher(
algorithms.AES(key),
modes.GCM(iv, min_tag_length=12),
).encryptor()
encryptor.authenticate_additional_data(associated_data)
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return (iv, ciphertext, encryptor.tag)
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(iv, tag, min_tag_length=12),
).decryptor()
decryptor.authenticate_additional_data(associated_data)
return decryptor.update(ciphertext) + decryptor.finalize()
def md5(code: str) -> str: def md5(code: str) -> str:
@@ -37,3 +60,8 @@ def try_rename(path: str, new_path: str) -> str:
os.rename(path, final_path) os.rename(path, final_path)
return final_path return final_path
def get_today_timestamp():
'''相对于本机本地时间的今天0点的时间戳'''
return int(mktime(date.today().timetuple()))

View File

@@ -1,6 +1,6 @@
import json
import os import os
from functools import lru_cache from functools import lru_cache
from json import load
from random import random from random import random
from time import time from time import time
@@ -25,15 +25,15 @@ def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list:
def get_world_info(map_id: str) -> dict: def get_world_info(map_id: str) -> dict:
'''读取json文件内容返回字典''' '''读取json文件内容返回字典'''
world_info = {} world_info = {}
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, map_id+'.json'), 'r') as f: with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, f'{map_id}.json'), 'rb') as f:
world_info = json.load(f) world_info = load(f)
return world_info return world_info
def get_world_all(c, user) -> list: def get_world_all(c, user) -> list:
''' '''
读取所有地图信息,返回列表\ 读取所有地图信息,返回列表
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
worlds = get_world_name() worlds = get_world_name()
@@ -44,7 +44,7 @@ class Step:
'''台阶类''' '''台阶类'''
def __init__(self) -> None: def __init__(self) -> None:
self.postion: int = None self.position: int = None
self.capture: int = None self.capture: int = None
self.items: list = [] self.items: list = []
self.restrict_id: str = None self.restrict_id: str = None
@@ -117,6 +117,7 @@ class Map:
self.require_localunlock_songid: str = None self.require_localunlock_songid: str = None
self.require_localunlock_challengeid: str = None self.require_localunlock_challengeid: str = None
self.chain_info: dict = None
@property @property
def rewards(self) -> list: def rewards(self) -> list:
@@ -140,7 +141,7 @@ class Map:
def to_dict(self) -> dict: def to_dict(self) -> dict:
if self.chapter is None: if self.chapter is None:
self.select_map_info() self.select_map_info()
return { r = {
'map_id': self.map_id, 'map_id': self.map_id,
'is_legacy': self.is_legacy, 'is_legacy': self.is_legacy,
'is_beyond': self.is_beyond, 'is_beyond': self.is_beyond,
@@ -162,6 +163,9 @@ class Map:
'require_localunlock_challengeid': self.require_localunlock_challengeid, 'require_localunlock_challengeid': self.require_localunlock_challengeid,
'steps': [s.to_dict() for s in self.steps], 'steps': [s.to_dict() for s in self.steps],
} }
if self.chain_info is not None:
r['chain_info'] = self.chain_info
return r
def from_dict(self, raw_dict: dict) -> 'Map': def from_dict(self, raw_dict: dict) -> 'Map':
self.is_legacy = raw_dict.get('is_legacy') self.is_legacy = raw_dict.get('is_legacy')
@@ -179,8 +183,11 @@ class Map:
self.coordinate = raw_dict.get('coordinate') self.coordinate = raw_dict.get('coordinate')
self.custom_bg = raw_dict.get('custom_bg', '') self.custom_bg = raw_dict.get('custom_bg', '')
self.stamina_cost = raw_dict.get('stamina_cost') self.stamina_cost = raw_dict.get('stamina_cost')
self.require_localunlock_songid = raw_dict.get('require_localunlock_songid', '') self.require_localunlock_songid = raw_dict.get(
self.require_localunlock_challengeid = raw_dict.get('require_localunlock_challengeid', '') 'require_localunlock_songid', '')
self.require_localunlock_challengeid = raw_dict.get(
'require_localunlock_challengeid', '')
self.chain_info = raw_dict.get('chain_info')
self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')] self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')]
return self return self
@@ -191,7 +198,7 @@ class Map:
class UserMap(Map): class UserMap(Map):
''' '''
用户地图类\ 用户地图类
parameters: `user` - `User`类或者子类的实例 parameters: `user` - `User`类或者子类的实例
''' '''
@@ -301,7 +308,7 @@ class UserMap(Map):
if self.require_type in ['pack', 'single']: if self.require_type in ['pack', 'single']:
item = ItemFactory(self.c).get_item(self.require_type) item = ItemFactory(self.c).get_item(self.require_type)
item.item_id = self.require_id item.item_id = self.require_id
item.select(self.user) item.select_user_item(self.user)
if not item.amount: if not item.amount:
self.is_locked = True self.is_locked = True
@@ -406,7 +413,8 @@ class Stamina:
class UserStamina(Stamina): class UserStamina(Stamina):
''' '''
用户体力类\ 用户体力类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -432,8 +440,9 @@ class UserStamina(Stamina):
class WorldPlay: class WorldPlay:
''' '''
世界模式打歌类处理特殊角色技能联动UserMap和UserPlay\ 世界模式打歌类处理特殊角色技能联动UserMap和UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''
@@ -447,8 +456,9 @@ class WorldPlay:
self.step_value: float = None self.step_value: float = None
self.prog_tempest: float = None self.prog_tempest: float = None
self.overdrive_extra: float = None
self.character_bonus_progress: float = None self.character_bonus_progress: float = None
self.prog_skill_increase: float = None
self.over_skill_increase: float = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
arcmap: 'UserMap' = self.user.current_map arcmap: 'UserMap' = self.user.current_map
@@ -476,21 +486,27 @@ class WorldPlay:
}, },
"current_stamina": self.user.stamina.stamina, "current_stamina": self.user.stamina.stamina,
"max_stamina_ts": self.user.stamina.max_stamina_ts, "max_stamina_ts": self.user.stamina.max_stamina_ts,
'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts,
'beyond_boost_gauge': self.user.beyond_boost_gauge
} }
if self.overdrive_extra is not None: if self.over_skill_increase is not None:
r['char_stats']['overdrive'] += self.overdrive_extra r['char_stats']['over_skill_increase'] = self.over_skill_increase
if self.prog_skill_increase is not None:
r['char_stats']['prog_skill_increase'] = self.prog_skill_increase
if self.prog_tempest is not None: if self.prog_tempest is not None:
r['char_stats']['prog'] += self.prog_tempest r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样
r['char_stats']['prog_tempest'] = self.prog_tempest r['char_stats']['prog_tempest'] = self.prog_tempest
if self.character_bonus_progress is not None: if self.character_bonus_progress is not None:
# 猜的,为了让客户端正确显示,当然结果是没问题的 # 猜的,为了让客户端正确显示
# r['base_progress'] += self.character_bonus_progress # 肯定不是这样的 r['progress'] -= self.character_bonus_progress
r['character_bonus_progress'] = self.character_bonus_progress r['character_bonus_progress'] = self.character_bonus_progress
if self.character_used.skill_id_displayed == 'skill_maya':
r['char_stats']['skill_state'] = self.character_used.skill_state
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
r["user_map"]["steps"] = [ r["user_map"]["steps"] = [
x.to_dict() for x in arcmap.steps_for_climbing] x.to_dict() for x in arcmap.steps_for_climbing]
@@ -503,16 +519,45 @@ class WorldPlay:
r['fragment_multiply'] = self.user_play.fragment_multiply r['fragment_multiply'] = self.user_play.fragment_multiply
if self.user_play.prog_boost_multiply != 0: if self.user_play.prog_boost_multiply != 0:
r['prog_boost_multiply'] = self.user_play.prog_boost_multiply r['prog_boost_multiply'] = self.user_play.prog_boost_multiply
if self.user_play.beyond_boost_gauge_usage != 0:
r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage
return r return r
@property
def beyond_boost_gauge_addition(self) -> float:
# guessed by Lost-MSth
return 2.45 * self.user_play.rating ** 0.5 + 27
@property @property
def step_times(self) -> float: def step_times(self) -> float:
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * prog_boost_multiply / 100 * beyond_boost_times
@property @property
def exp_times(self) -> float: def exp_times(self) -> float:
return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply+100) / 100 prog_boost_multiply = self.user_play.prog_boost_multiply + 100
beyond_boost_times = 1
if self.user_play.beyond_gauge == 1:
if prog_boost_multiply > 100:
prog_boost_multiply -= 100
if self.user_play.beyond_boost_gauge_usage == 100:
beyond_boost_times = 2
elif self.user_play.beyond_boost_gauge_usage == 200:
beyond_boost_times = 3
return self.user_play.stamina_multiply * prog_boost_multiply / 100 * beyond_boost_times
def get_step(self) -> None: def get_step(self) -> None:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
@@ -521,6 +566,8 @@ class WorldPlay:
self.character_used.level) self.character_used.level)
if self.prog_tempest: if self.prog_tempest:
prog += self.prog_tempest prog += self.prog_tempest
if self.prog_skill_increase:
prog += self.prog_skill_increase
self.step_value = self.base_step_value * prog / 50 * self.step_times self.step_value = self.base_step_value * prog / 50 * self.step_times
else: else:
@@ -539,8 +586,8 @@ class WorldPlay:
overdrive = self.character_used.overdrive.get_value( overdrive = self.character_used.overdrive.get_value(
self.character_used.level) self.character_used.level)
if self.overdrive_extra: if self.over_skill_increase:
overdrive += self.overdrive_extra overdrive += self.over_skill_increase
self.step_value = self.base_step_value * overdrive / \ self.step_value = self.base_step_value * overdrive / \
50 * self.step_times * affinity_multiplier 50 * self.step_times * affinity_multiplier
@@ -553,6 +600,20 @@ class WorldPlay:
self.user_play.clear_play_state() self.user_play.clear_play_state()
self.user.select_user_about_world_play() self.user.select_user_about_world_play()
if self.user_play.beyond_gauge == 0:
# 更新byd大招蓄力条
self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition
self.user.beyond_boost_gauge = min(
self.user.beyond_boost_gauge, 200)
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge:
self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage
if abs(self.user.beyond_boost_gauge) <= 1e-5:
self.user.beyond_boost_gauge = 0
self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge)
self.character_used = Character() self.character_used = Character()
self.user.character.select_character_info() self.user.character.select_character_info()
@@ -595,16 +656,17 @@ class WorldPlay:
self.user.current_map.update() self.user.current_map.update()
def before_calculate(self) -> None: def before_calculate(self) -> None:
if self.user_play.beyond_gauge == 0: factory_dict = {'skill_vita': self._skill_vita, 'skill_mika': self._skill_mika, 'skill_ilith_ivy': self._skill_ilith_ivy,
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed: 'ilith_awakened_skill': self._ilith_awakened_skill, 'skill_hikari_vanessa': self._skill_hikari_vanessa}
self._special_tempest() if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
else: self._special_tempest()
if self.character_used.skill_id_displayed == 'skill_vita':
self._skill_vita() if self.character_used.skill_id_displayed in factory_dict:
factory_dict[self.character_used.skill_id_displayed]()
def after_climb(self) -> None: def after_climb(self) -> None:
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap, factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane} 'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane, 'skill_maya': self._skill_maya, 'skill_mithra': self._skill_mithra}
if self.character_used.skill_id_displayed in factory_dict: if self.character_used.skill_id_displayed in factory_dict:
factory_dict[self.character_used.skill_id_displayed]() factory_dict[self.character_used.skill_id_displayed]()
@@ -624,12 +686,12 @@ class WorldPlay:
def _skill_vita(self) -> None: def _skill_vita(self) -> None:
''' '''
vita技能overdrive随回忆率提升提升量最多为10\ vita技能overdrive随回忆率提升提升量最多为10
此处采用线性函数 此处采用线性函数
''' '''
self.overdrive_extra = 0 self.over_skill_increase = 0
if 0 < self.user_play.health <= 100: if 0 < self.user_play.health <= 100:
self.overdrive_extra = self.user_play.health / 10 self.over_skill_increase = self.user_play.health / 10
def _eto_uncap(self) -> None: def _eto_uncap(self) -> None:
'''eto觉醒技能获得残片奖励时世界模式进度加7''' '''eto觉醒技能获得残片奖励时世界模式进度加7'''
@@ -645,7 +707,7 @@ class WorldPlay:
if fragment_flag: if fragment_flag:
self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS
self.step_value += self.character_bonus_progress * self.step_times self.step_value += self.character_bonus_progress
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
@@ -654,7 +716,7 @@ class WorldPlay:
x: 'Step' = self.user.current_map.steps_for_climbing[0] x: 'Step' = self.user.current_map.steps_for_climbing[0]
if x.restrict_id and x.restrict_type: if x.restrict_id and x.restrict_type:
self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
self.step_value += self.character_bonus_progress * self.step_times self.step_value += self.character_bonus_progress
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
@@ -664,9 +726,9 @@ class WorldPlay:
self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random( self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random(
) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS ) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS
self.step_value += self.character_bonus_progress * self.step_times self.step_value += self.character_bonus_progress
if self.step_value < 0: if self.step_value < 0:
self.character_bonus_progress += self.step_value / self.step_times self.character_bonus_progress += self.step_value
self.step_value = 0 self.step_value = 0
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
@@ -685,6 +747,64 @@ class WorldPlay:
''' '''
x: 'Step' = self.user.current_map.steps_for_climbing[0] 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: 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.character_bonus_progress = -self.step_value / 2
self.step_value = self.step_value / 2 self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
def _ilith_awakened_skill(self) -> None:
'''
ilith 觉醒技能,曲目通关时步数+6wiki 说是 prog 值+6
'''
if self.user_play.health > 0:
self.prog_skill_increase = 6
def _skill_mika(self) -> None:
'''
mika 技能,通关特定曲目能力值翻倍
'''
if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0:
self.over_skill_increase = self.character_used.overdrive.get_value(
self.character_used.level)
self.prog_skill_increase = self.character_used.prog.get_value(
self.character_used.level)
def _skill_mithra(self) -> None:
'''
mithra 技能,每 150 combo 增加世界模式进度+1
'''
if self.user_play.combo_interval_bonus:
self.character_bonus_progress = self.user_play.combo_interval_bonus
self.step_value += self.character_bonus_progress
self.user.current_map.reclimb(self.step_value)
def _skill_ilith_ivy(self) -> None:
'''
ilith & ivy 技能,根据 skill_cytusii_flag 来增加三个数值,最高生命每过 20 就对应数值 +10
'''
if not self.user_play.skill_cytusii_flag:
return
x = self.user_play.skill_cytusii_flag[:
self.user_play.highest_health // 20]
self.over_skill_increase = x.count('2') * 10
self.prog_skill_increase = x.count('1') * 10
def _skill_hikari_vanessa(self) -> None:
'''
hikari & vanessa 技能,根据 skill_cytusii_flag 来减少三个数值,最高生命每过 20 就对应数值 -10
'''
if not self.user_play.skill_cytusii_flag:
return
x = self.user_play.skill_cytusii_flag[:5 -
self.user_play.lowest_health // 20]
self.over_skill_increase = -x.count('2') * 10
self.prog_skill_increase = -x.count('1') * 10
def _skill_maya(self) -> None:
'''
maya 技能skill_flag 为 1 时,世界模式进度翻倍
'''
if self.character_used.skill_flag:
self.character_bonus_progress = self.step_value
self.step_value += self.character_bonus_progress
self.user.current_map.reclimb(self.step_value)
self.character_used.change_skill_state()

View File

@@ -1,45 +1,45 @@
class InitData: 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', char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)'] 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin']
skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror'] 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin']
skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', '', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''] '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap']
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0,
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0] 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32,
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 0, 57] 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60]
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52,
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 0, 70] 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50]
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18,
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 0, 57] 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34]
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52,
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50] 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70]
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73,
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80] 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80]
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50] 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50] 65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 105, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80] 80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50] 56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64]
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0] 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2]
char_core = { char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
@@ -57,14 +57,18 @@ class InitData:
43: [{'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}], 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}], 12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}] 19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', 'amount': 30}],
66: [{'core_id': 'core_chunithm', 'amount': 15}],
5: [{'core_id': 'core_hollow', 'amount': 0}],
73: [{'core_id': 'core_wacca', 'amount': 15}]
} }
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson',
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase'] 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca']
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle'] "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn']
world_unlocks = ["scenery_chap1", "scenery_chap2", world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]

View File

@@ -17,6 +17,11 @@ clear_type int,
rating real, rating real,
primary key(user_id, song_id, difficulty, time_played) primary key(user_id, song_id, difficulty, time_played)
); );
create table if not exists user_rating(user_id int,
time int,
rating_ptt real,
primary key(user_id, time)
);
create index if not exists user_score_1 on user_score (song_id, difficulty); create index if not exists user_score_1 on user_score (song_id, difficulty);
create index if not exists user_score_2 on user_score (time_played); create index if not exists user_score_2 on user_score (time_played);

View File

@@ -646,5 +646,149 @@
"price": 700, "price": 700,
"discount_from": 1646784000000, "discount_from": 1646784000000,
"discount_to": 1647388799000 "discount_to": 1647388799000
},
{
"name": "maimai_append_1",
"items": [
{
"type": "pack",
"id": "maimai_append_1",
"is_available": true
},
{
"type": "core",
"amount": 4,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 400,
"price": 400
},
{
"name": "chunithm_append_2",
"items": [
{
"type": "pack",
"id": "chunithm_append_2",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "cytusii",
"items": [
{
"type": "pack",
"id": "cytusii",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "cytusii_append_1",
"items": [
{
"type": "pack",
"id": "cytusii_append_1",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "eden",
"items": [
{
"type": "pack",
"id": "eden",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "eden_append_1",
"items": [
{
"type": "pack",
"id": "eden_append_1",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
},
{
"name": "eden_append_2",
"items": [
{
"type": "pack",
"id": "eden_append_2",
"is_available": true
},
{
"type": "core",
"amount": 3,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 300,
"price": 300
},
{
"name": "wacca_append_1",
"items": [
{
"type": "pack",
"id": "wacca_append_1",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
} }
] ]

View File

@@ -1306,5 +1306,257 @@
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100
},
{
"name": "hiirogekka",
"items": [
{
"type": "single",
"id": "hiirogekka",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "manicjeer",
"items": [
{
"type": "single",
"id": "manicjeer",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "crimsonthrone",
"items": [
{
"type": "single",
"id": "crimsonthrone",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "primitivelights",
"items": [
{
"type": "single",
"id": "primitivelights",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "thesurvivor",
"items": [
{
"type": "single",
"id": "thesurvivor",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "newyorkbackraise",
"items": [
{
"type": "single",
"id": "newyorkbackraise",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "lostintheabyss",
"items": [
{
"type": "single",
"id": "lostintheabyss",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "galacticlove",
"items": [
{
"type": "single",
"id": "galacticlove",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "tothemilkyway",
"items": [
{
"type": "single",
"id": "tothemilkyway",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "internetyamero",
"items": [
{
"type": "single",
"id": "internetyamero",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "sacrifice",
"items": [
{
"type": "single",
"id": "sacrifice",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "tothefurthestdream",
"items": [
{
"type": "single",
"id": "tothefurthestdream",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "bbkkbkk",
"items": [
{
"type": "single",
"id": "bbkkbkk",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "wishuponasnow",
"items": [
{
"type": "single",
"id": "wishuponasnow",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
} }
] ]

View File

@@ -33,7 +33,8 @@ ban_flag text,
next_fragstam_ts int, next_fragstam_ts int,
max_stamina_ts int, max_stamina_ts int,
stamina int, stamina int,
world_mode_locked_end_ts int world_mode_locked_end_ts int,
beyond_boost_gauge real default 0
); );
create table if not exists login(access_token text, create table if not exists login(access_token text,
user_id int, user_id int,
@@ -68,6 +69,7 @@ level int,
exp real, exp real,
is_uncapped int, is_uncapped int,
is_uncapped_override int, is_uncapped_override int,
skill_flag int,
primary key(user_id, character_id) primary key(user_id, character_id)
); );
create table if not exists user_char_full(user_id int, create table if not exists user_char_full(user_id int,
@@ -76,6 +78,7 @@ level int,
exp real, exp real,
is_uncapped int, is_uncapped int,
is_uncapped_override int, is_uncapped_override int,
skill_flag int,
primary key(user_id, character_id) primary key(user_id, character_id)
); );
create table if not exists character(character_id int primary key, create table if not exists character(character_id int primary key,
@@ -182,7 +185,9 @@ course_score int,
course_clear_type int, course_clear_type int,
stamina_multiply int, stamina_multiply int,
fragment_multiply int, fragment_multiply int,
prog_boost_multiply int prog_boost_multiply int,
beyond_boost_gauge_usage int,
skill_cytusii_flag text
); );
create table if not exists item(item_id text, create table if not exists item(item_id text,
type text, type text,

View File

@@ -1,12 +1,27 @@
{ {
"songs": [ "songs": [
{
"id": "dement",
"difficulties": [
{ {
"ratingClass": 3 "id": "dement",
"set": "base",
"purchase": "",
"difficulties": [
{
"ratingClass": 0,
"rating": 3
},
{
"ratingClass": 1,
"rating": 6
},
{
"ratingClass": 2,
"rating": 7
},
{
"ratingClass": 3,
"rating": 9
}
]
} }
] ]
}
]
} }

View File

@@ -4,17 +4,22 @@ class Config:
''' '''
''' '''
服务器地址、端口号、校验码 服务器地址、端口号、校验码、传输加密密钥
Server address, port and verification code Server address, port, verification code, and encryption key
''' '''
HOST = '0.0.0.0' HOST = '0.0.0.0'
UDP_PORT = 10900 UDP_PORT = 10900
TCP_PORT = 10901 TCP_PORT = 10901
AUTHENTICATION = 'my_link_play_server' AUTHENTICATION = 'my_link_play_server'
TCP_SECRET_KEY = '1145141919810'
''' '''
-------------------------------------------------- --------------------------------------------------
''' '''
DEBUG = False
TCP_MAX_LENGTH = 0x0FFFFFFF
TIME_LIMIT = 3600000 TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000 COMMAND_INTERVAL = 1000000

View File

@@ -1,257 +1,107 @@
import base64 # import binascii
import logging import logging
import random
import socketserver import socketserver
import threading import threading
import time from json import dumps, loads
from os import urandom
# import binascii
from .aes import decrypt, encrypt from .aes import decrypt, encrypt
from .config import Config from .config import Config
from .udp_class import Player, Room, bi from .store import Store, TCPRouter, clear_player, clear_room
from .udp_class import bi
from .udp_parser import CommandParser from .udp_parser import CommandParser
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
link_play_data = {}
room_id_dict = {} # 'room_id': Room
room_code_dict = {} # 'room_code': Room
player_dict = {} # 'player_id' : Player
clean_timer = 0
lock = threading.RLock()
logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s', logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
level=logging.INFO) level=logging.INFO)
def random_room_code():
# 随机生成房间号
re = ''
for _ in range(4):
re += chr(random.randint(65, 90))
for _ in range(2):
re += str(random.randint(0, 9))
return re
def unique_random(dataset, length=8, random_func=None):
'''无重复随机且默认非0'''
if random_func is None:
x = bi(urandom(length))
while x in dataset or x == 0:
x = bi(urandom(length))
else:
x = random_func()
while x in dataset:
x = random_func()
return x
def clear_player(token):
# 清除玩家信息和token
del player_dict[link_play_data[token]['player_id']]
del link_play_data[token]
def clear_room(room):
# 清除房间信息
room_id = room.room_id
room_code = room.room_code
del room_id_dict[room_id]
del room_code_dict[room_code]
del room
def memory_clean(now):
# 内存清理
with lock:
clean_room_list = []
clean_player_list = []
for token in link_play_data:
room = link_play_data[token]['room']
if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id)
if now - room.players[link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token)
for room_id in room_id_dict:
if now - room_id_dict[room_id].timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id)
for room_id in clean_room_list:
if room_id in room_id_dict:
clear_room(room_id_dict[room_id])
for token in clean_player_list:
clear_player(token)
class UDP_handler(socketserver.BaseRequestHandler): class UDP_handler(socketserver.BaseRequestHandler):
def handle(self): def handle(self):
client_msg, server = self.request client_msg, server = self.request
# print(client_msg)
try: try:
token = client_msg[:8] token = client_msg[:8]
iv = client_msg[8:20] iv = client_msg[8:20]
tag = client_msg[20:32] tag = client_msg[20:32]
ciphertext = client_msg[32:] ciphertext = client_msg[32:]
if int.from_bytes(token, byteorder='little') in link_play_data: if bi(token) not in Store.link_play_data:
user = link_play_data[bi(token)]
else:
return None return None
user = Store.link_play_data[bi(token)]
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag) plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
return None return None
# print(binascii.b2a_hex(plaintext))
# if Config.DEBUG:
# logging.info(
# f'UDP-From-{self.client_address[0]}-{binascii.b2a_hex(plaintext)}')
commands = CommandParser( commands = CommandParser(
user['room'], user['player_index']).get_commands(plaintext) user['room'], user['player_index']).get_commands(plaintext)
if user['room'].players[user['player_index']].player_id == 0: if user['room'].players[user['player_index']].player_id == 0:
clear_player(bi(token)) clear_player(bi(token))
temp = [] if user['room'].player_num == 0:
for i in commands: clear_room(user['room'])
if i[:3] == b'\x06\x16\x12': commands = [i for i in commands if i[2] == 0x12]
temp.append(i)
commands = temp
# 处理不能正确被踢的问题 # 处理不能正确被踢的问题
for i in commands: for i in commands:
iv, ciphertext, tag = encrypt(user['key'], i, b'') iv, ciphertext, tag = encrypt(user['key'], i, b'')
# print(binascii.b2a_hex(i)) # if Config.DEBUG:
# logging.info(
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
server.sendto(token + iv + tag[:12] + server.sendto(token + iv + tag[:12] +
ciphertext, self.client_address) ciphertext, self.client_address)
class TCP_handler(socketserver.StreamRequestHandler): AUTH_LEN = len(Config.AUTHENTICATION)
def handle(self): TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16]
self.data = self.rfile.readline().strip()
message = self.data.decode('utf-8')
# print(message) class TCP_handler(socketserver.StreamRequestHandler):
data = message.split('|')
if data[0] != Config.AUTHENTICATION: def handle(self):
self.wfile.write(b'No authentication') try:
logging.warning('TCP-%s-No authentication' % if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
self.client_address[0]) self.wfile.write(b'No authentication')
logging.warning(
f'TCP-{self.client_address[0]}-No authentication')
return None
cipher_len = int.from_bytes(self.rfile.read(8), byteorder='little')
if cipher_len > Config.TCP_MAX_LENGTH:
self.wfile.write(b'Body too long')
logging.warning(f'TCP-{self.client_address[0]}-Body too long')
return None
iv = self.rfile.read(12)
tag = self.rfile.read(12)
ciphertext = self.rfile.read(cipher_len)
self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag)
message = self.data.decode('utf-8')
data = loads(message)
except Exception as e:
logging.error(e)
return None return None
global clean_timer if Config.DEBUG:
now = round(time.time() * 1000) logging.info(f'TCP-From-{self.client_address[0]}-{message}')
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')) r = TCPRouter(data).handle()
try:
r = dumps(r)
def data_swap(data: list) -> str: if Config.DEBUG:
# data: list[str] = [command, ...] logging.info(f'TCP-To-{self.client_address[0]}-{r}')
if data[0] == '1': iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
# 开房 r = len(ciphertext).to_bytes(8, byteorder='little') + \
# data = ['1', name, song_unlock, ] iv + tag[:12] + ciphertext
# song_unlock: base64 str except Exception as e:
name = data[1] logging.error(e)
song_unlock = base64.b64decode(data[2]) return None
self.wfile.write(r)
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): def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):

View File

@@ -0,0 +1,302 @@
import logging
from base64 import b64decode, b64encode
from os import urandom
from random import randint
from threading import RLock
from time import time
from .config import Config
from .udp_class import Player, Room, bi
class Store:
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
link_play_data = {}
room_id_dict = {} # 'room_id': Room
room_code_dict = {} # 'room_code': Room
player_dict = {} # 'player_id' : Player
lock = RLock()
def random_room_code():
re = ''
for _ in range(4):
re += chr(randint(65, 90))
for _ in range(2):
re += str(randint(0, 9))
return re
def unique_random(dataset, length=8, random_func=None):
'''无重复随机且默认非0没处理可能的死循环'''
if random_func is None:
x = bi(urandom(length))
while x in dataset or x == 0:
x = bi(urandom(length))
else:
x = random_func()
while x in dataset:
x = random_func()
return x
def clear_player(token):
# 清除玩家信息和token
player_id = Store.link_play_data[token]['player_id']
logging.info(f'Clean player `{Store.player_dict[player_id].name}`')
del Store.player_dict[player_id]
del Store.link_play_data[token]
def clear_room(room):
# 清除房间信息
room_id = room.room_id
room_code = room.room_code
logging.info(f'Clean room `{room_code}`')
del Store.room_id_dict[room_id]
del Store.room_code_dict[room_code]
del room
def memory_clean(now):
# 内存清理,应对玩家不正常退出
with Store.lock:
clean_room_list = []
clean_player_list = []
for token, v in Store.link_play_data.items():
room = v['room']
if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id)
if now - room.players[v['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token)
for room_id, v in Store.room_id_dict.items():
if now - v.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id)
for room_id in clean_room_list:
if room_id in Store.room_id_dict:
clear_room(Store.room_id_dict[room_id])
for token in clean_player_list:
clear_player(token)
class TCPRouter:
clean_timer = 0
router = {
'debug',
'create_room',
'join_room',
'update_room',
'get_rooms',
}
def __init__(self, raw_data: 'dict | list'):
self.raw_data = raw_data # data: dict {endpoint: str, data: dict}
self.data = raw_data['data']
self.endpoint = raw_data['endpoint']
def debug(self) -> dict:
if Config.DEBUG:
return {'result': eval(self.data['code'])}
return {'hello_world': 'ok'}
@staticmethod
def clean_check():
now = round(time() * 1000)
if now - TCPRouter.clean_timer >= Config.TIME_LIMIT:
logging.info('Start cleaning memory...')
TCPRouter.clean_timer = now
memory_clean(now)
def handle(self) -> dict:
self.clean_check()
if self.endpoint not in self.router:
return None
try:
r = getattr(self, self.endpoint)()
except Exception as e:
logging.error(e)
return 999
if isinstance(r, int):
return {'code': r}
return {
'code': 0,
'data': r
}
@staticmethod
def generate_player(name: str) -> Player:
player_id = unique_random(Store.player_dict, 3)
player = Player()
player.player_id = player_id
player.set_player_name(name)
Store.player_dict[player_id] = player
return player
@staticmethod
def generate_room() -> Room:
room_id = unique_random(Store.room_id_dict)
room = Room()
room.room_id = room_id
room.timestamp = round(time() * 1000)
Store.room_id_dict[room_id] = room
room_code = unique_random(
Store.room_code_dict, random_func=random_room_code)
room.room_code = room_code
Store.room_code_dict[room_code] = room
return room
def create_room(self) -> dict:
# 开房
# data = ['1', name, song_unlock, ]
# song_unlock: base64 str
name = self.data['name']
song_unlock = b64decode(self.data['song_unlock'])
key = urandom(16)
with Store.lock:
room = self.generate_room()
player = self.generate_player(name)
player.song_unlock = song_unlock
room.song_unlock = song_unlock
room.host_id = player.player_id
room.players[0] = player
token = room.room_id
player.token = token
Store.link_play_data[token] = {
'key': key,
'room': room,
'player_index': 0,
'player_id': player.player_id
}
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`')
return {
'room_code': room.room_code,
'room_id': room.room_id,
'token': token,
'key': b64encode(key).decode('utf-8'),
'player_id': player.player_id
}
def join_room(self) -> 'dict | int':
# 入房
# data = ['2', name, song_unlock, room_code]
# song_unlock: base64 str
room_code = self.data['room_code'].upper()
key = urandom(16)
name = self.data['name']
song_unlock = b64decode(self.data['song_unlock'])
with Store.lock:
if room_code not in Store.room_code_dict:
# 房间号错误 / 房间不存在
return 1202
room: Room = Store.room_code_dict[room_code]
player_num = room.player_num
if player_num == 4:
# 满人
return 1201
if player_num == 0:
# 房间不存在
return 1202
if room.state != 2:
# 无法加入
return 1205
token = unique_random(Store.link_play_data)
player = self.generate_player(name)
player.token = token
player.song_unlock = song_unlock
room.update_song_unlock()
for i in range(4):
if room.players[i].player_id == 0:
room.players[i] = player
player_index = i
break
Store.link_play_data[token] = {
'key': key,
'room': room,
'player_index': player_index,
'player_id': player.player_id
}
logging.info(f'TCP-Player `{name}` joins room `{room_code}`')
return {
'room_code': room_code,
'room_id': room.room_id,
'token': token,
'key': b64encode(key).decode('utf-8'),
'player_id': player.player_id,
'song_unlock': b64encode(room.song_unlock).decode('utf-8')
}
def update_room(self) -> dict:
# 房间信息更新
# data = ['3', token]
token = int(self.data['token'])
with Store.lock:
if token not in Store.link_play_data:
return 108
r = Store.link_play_data[token]
room = r['room']
logging.info(f'TCP-Room `{room.room_code}` info update')
return {
'room_code': room.room_code,
'room_id': room.room_id,
'key': b64encode(r['key']).decode('utf-8'),
# changed from room.players[r['player_index']].player_id,
'player_id': r['player_id'],
'song_unlock': b64encode(room.song_unlock).decode('utf-8')
}
def get_rooms(self) -> dict:
# 获取房间列表与详细信息
offset = int(self.data.get('offset', 0))
if offset < 0:
offset = 0
limit = min(int(self.data.get('limit', 100)), 100)
if limit < 0:
limit = 100
n = 0
m = 0
rooms = []
f = False
f2 = False
for room in Store.room_id_dict.values():
if room.player_num == 0:
continue
if m < offset:
m += 1
continue
if f:
# 处理刚好有 limit 个房间的情况
f2 = True
break
n += 1
rooms.append(room.to_dict())
if n >= limit:
f = True
return {
'amount': n,
'offset': offset,
'limit': limit,
'has_more': f2,
'rooms': rooms
}

View File

@@ -1,3 +1,6 @@
import logging
from time import time
from .config import Config from .config import Config
@@ -42,6 +45,31 @@ class Player:
self.start_command_num = 0 self.start_command_num = 0
@property
def name(self) -> str:
return self.player_name.decode('ascii').rstrip('\x00')
def to_dict(self) -> dict:
return {
'multiplay_player_id': self.player_id,
'name': self.name,
'is_online': self.online == 1,
'character_id': self.character_id,
'is_uncapped': self.is_uncapped == 1,
'last_song': {
'difficulty': self.last_difficulty,
'score': self.last_score,
'cleartype': self.last_cleartype,
},
'song': {
'difficulty': self.difficulty,
'score': self.score,
'cleartype': self.cleartype,
},
'player_state': self.player_state,
'last_timestamp': self.last_timestamp,
}
def set_player_name(self, player_name: str): def set_player_name(self, player_name: str):
self.player_name = player_name.encode('ascii') self.player_name = player_name.encode('ascii')
if len(self.player_name) > 16: if len(self.player_name) > 16:
@@ -61,11 +89,10 @@ class Room:
self.song_idx = 0xffff self.song_idx = 0xffff
self.last_song_idx = 0xffff self.last_song_idx = 0xffff
self.song_unlock = b'\x00' * Config.LINK_PLAY_UNLOCK_LENGTH self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
self.host_id = 0 self.host_id = 0
self.players = [Player(), Player(), Player(), Player()] self.players = [Player(), Player(), Player(), Player()]
self.player_num = 0
self.interval = 1000 self.interval = 1000
self.times = 100 self.times = 100
@@ -73,7 +100,59 @@ class Room:
self.round_switch = 0 self.round_switch = 0
self.command_queue = [] self.command_queue = []
self.command_queue_length = 0
def to_dict(self) -> dict:
p = [i.to_dict() for i in self.players if i.player_id != 0]
for i in p:
i['is_host'] = i['player_id'] == self.host_id
return {
'room_id': self.room_id,
'room_code': self.room_code,
'state': self.state,
'song_idx': self.song_idx,
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
'host_id': self.host_id,
'players': p,
'round_switch': self.round_switch == 1,
'last_timestamp': self.timestamp,
'is_enterable': self.is_enterable,
'is_playing': self.is_playing,
}
@property
def is_enterable(self) -> bool:
return 0 < self.player_num < 4 and self.state == 2
@property
def is_playing(self) -> bool:
return self.state in (4, 5, 6, 7)
@property
def command_queue_length(self) -> int:
return len(self.command_queue)
@property
def player_num(self) -> int:
self.check_player_online()
return sum(i.player_id != 0 for i in self.players)
def check_player_online(self, now: int = None):
# 检测玩家是否被自动踢出房间 / 离线判断
now = round(time() * 1000000) if now is None else now
flag = False
player_index_list = []
for i, x in enumerate(self.players):
if x.player_id == 0 or x.last_timestamp == 0:
continue
if now - x.last_timestamp >= Config.PLAYER_TIMEOUT:
self.delete_player(i)
flag = True
player_index_list.append(i)
elif x.online == 1 and now - x.last_timestamp >= Config.PLAYER_PRE_TIMEOUT:
x.online = 0
player_index_list.append(i)
return flag, player_index_list
def get_players_info(self): def get_players_info(self):
# 获取所有玩家信息 # 获取所有玩家信息
@@ -106,17 +185,23 @@ class Room:
for i in range(4): for i in range(4):
if self.players[i].player_id == self.host_id: if self.players[i].player_id == self.host_id:
for j in range(1, 4): for j in range(1, 4):
if self.players[(i + j) % 4].player_id != 0: player = self.players[(i + j) % 4]
self.host_id = self.players[(i + j) % 4].player_id if player.player_id != 0:
self.host_id = player.player_id
logging.info(
f'Player `{player.name}` becomes the host of room `{self.room_code}`')
break break
break break
def delete_player(self, player_index: int): def delete_player(self, player_index: int):
# 删除某个玩家 # 删除某个玩家
self.player_num -= 1 player = self.players[player_index]
if self.players[player_index].player_id == self.host_id: if player.player_id == self.host_id:
self.make_round() self.make_round()
logging.info(
f'Player `{player.name}` leaves room `{self.room_code}`')
self.players[player_index].online = 0 self.players[player_index].online = 0
self.players[player_index] = Player() self.players[player_index] = Player()
self.update_song_unlock() self.update_song_unlock()
@@ -138,8 +223,7 @@ class Room:
return False return False
return True return True
else: return False
return False
def is_finish(self): def is_finish(self):
# 是否全部进入结算 # 是否全部进入结算
@@ -178,3 +262,10 @@ class Room:
for i in max_score_i: for i in max_score_i:
self.players[i].best_player_flag = 1 self.players[i].best_player_flag = 1
logging.info(
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
for i in self.players:
if i.player_id != 0:
logging.info(
f'- Player `{i.name}` - Score: {i.last_score} Cleartype: {i.last_cleartype} Difficulty: {i.last_difficulty}')

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import api
import server import server
import web.index import web.index
import web.login import web.login
# import webapi
from core.constant import Constant from core.constant import Constant
from core.download import UserDownload from core.download import UserDownload
from core.error import ArcError, NoAccess, RateLimit from core.error import ArcError, NoAccess, RateLimit
@@ -54,6 +55,7 @@ app.register_blueprint(web.login.bp)
app.register_blueprint(web.index.bp) app.register_blueprint(web.index.bp)
app.register_blueprint(api.bp) app.register_blueprint(api.bp)
app.register_blueprint(server.bp) app.register_blueprint(server.bp)
# app.register_blueprint(webapi.bp)
@app.route('/') @app.route('/')

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
from flask import Blueprint, request
from core.constant import Constant from core.constant import Constant
from core.course import UserCourseList from core.course import UserCourseList
from core.item import ItemCore from core.item import ItemCore
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return
@@ -19,7 +20,7 @@ def course_me(user_id):
user = UserOnline(c, user_id) user = UserOnline(c, user_id)
core = ItemCore(c) core = ItemCore(c)
core.item_id = 'core_course_skip_purchase' core.item_id = 'core_course_skip_purchase'
core.select(user) core.select_user_item(user)
x = UserCourseList(c, user) x = UserCourseList(c, user)
x.select_all() x.select_all()
return success_return({ return success_return({

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline, code_get_id from core.user import UserOnline, code_get_id
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,10 +1,18 @@
from functools import wraps from functools import wraps
from traceback import format_exc from traceback import format_exc
from core.config_manager import Config
from core.error import ArcError
from flask import current_app, g, jsonify from flask import current_app, g, jsonify
from core.config_manager import Config
from core.error import ArcError, NoAccess
has_arc_hash = False
try:
from core.arc_crypto import ArcHashChecker # type: ignore
has_arc_hash = True
except ModuleNotFoundError:
pass
default_error = ArcError('Unknown Error', status=500) default_error = ArcError('Unknown Error', status=500)
@@ -32,6 +40,8 @@ def error_return(e: ArcError = default_error): # 错误返回
# 124 你今天不能再使用这个IP地址创建新的账号 # 124 你今天不能再使用这个IP地址创建新的账号
# 150 非常抱歉您已被限制使用此功能 # 150 非常抱歉您已被限制使用此功能
# 151 目前无法使用此功能 # 151 目前无法使用此功能
# 160 账户未邮箱认证,请检查邮箱
# 161 账户认证过期,请重新注册
# 401 用户不存在 # 401 用户不存在
# 403 无法连接至服务器 # 403 无法连接至服务器
# 501 502 -6 此物品目前无法获取 # 501 502 -6 此物品目前无法获取
@@ -43,6 +53,7 @@ def error_return(e: ArcError = default_error): # 错误返回
# 604 你不能加自己为好友 # 604 你不能加自己为好友
# 903 下载量超过了限制请24小时后重试 # 903 下载量超过了限制请24小时后重试
# 905 请在再次使用此功能前等待24小时 # 905 请在再次使用此功能前等待24小时
# 910 重新请求验证邮件前需等待x分钟 extra: retry_at
# 1001 设备数量达到上限 # 1001 设备数量达到上限
# 1002 此设备已使用过此功能 # 1002 此设备已使用过此功能
# 1201 房间已满 # 1201 房间已满
@@ -78,8 +89,7 @@ def arc_try(view):
data = view(*args, **kwargs) data = view(*args, **kwargs)
if data is None: if data is None:
return error_return() return error_return()
else: return data
return data
except ArcError as e: except ArcError as e:
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
current_app.logger.warning(format_exc()) current_app.logger.warning(format_exc())
@@ -89,3 +99,16 @@ def arc_try(view):
return error_return(e) return error_return(e)
return wrapped_view return wrapped_view
def header_check(request) -> ArcError:
'''检查请求头是否合法'''
headers = request.headers
if Config.ALLOW_APPVERSION: # 版本检查
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION:
return NoAccess('Invalid app version', 1203)
if has_arc_hash and not ArcHashChecker(request).check():
return NoAccess('Invalid request')
return None

View File

@@ -1,8 +1,9 @@
from flask import Blueprint, request
from core.config_manager import Config from core.config_manager import Config
from core.error import ArcError from core.error import ArcError
from core.linkplay import Player, RemoteMultiPlayer, Room from core.linkplay import Player, RemoteMultiPlayer, Room
from core.sql import Connect from core.sql import Connect
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,13 +1,14 @@
import json import json
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from flask import Blueprint, jsonify, request
from werkzeug.datastructures import ImmutableMultiDict
from core.download import DownloadList from core.download import DownloadList
from core.error import RateLimit from core.error import RateLimit
from core.sql import Connect from core.sql import Connect
from core.system import GameInfo from core.system import GameInfo
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, jsonify, request
from werkzeug.datastructures import ImmutableMultiDict
from .auth import auth_required from .auth import auth_required
from .func import arc_try, error_return, success_return from .func import arc_try, error_return, success_return
@@ -29,15 +30,16 @@ def game_info():
@auth_required(request) @auth_required(request)
@arc_try @arc_try
def download_song(user_id): def download_song(user_id):
with Connect(in_memory=True) as c: with Connect(in_memory=True) as c_m:
x = DownloadList(c, UserOnline(None, user_id)) with Connect() as c:
x.song_ids = request.args.getlist('sid') x = DownloadList(c_m, UserOnline(c, user_id))
x.url_flag = json.loads(request.args.get('url', 'true')) x.song_ids = request.args.getlist('sid')
if x.url_flag and x.is_limited: x.url_flag = json.loads(request.args.get('url', 'true'))
raise RateLimit('You have reached the download limit.', 903) if x.url_flag and x.is_limited:
raise RateLimit('You have reached the download limit.', 903)
x.add_songs() x.add_songs()
return success_return(x.urls) return success_return(x.urls)
@bp.route('/finale/progress', methods=['GET']) @bp.route('/finale/progress', methods=['GET'])
@@ -53,6 +55,11 @@ def finale_start():
return success_return({}) return success_return({})
@bp.route('/finale/finale_end', methods=['POST'])
def finale_end():
return success_return({})
map_dict = {'/user/me': user_me, map_dict = {'/user/me': user_me,
'/purchase/bundle/pack': bundle_pack, '/purchase/bundle/pack': bundle_pack,
'/serve/download/me/song': download_song, '/serve/download/me/song': download_song,

View File

@@ -1,7 +1,8 @@
from flask import Blueprint, request
from core.present import UserPresent, UserPresentList from core.present import UserPresent, UserPresentList
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,12 +1,13 @@
from time import time from time import time
from flask import Blueprint, request
from core.error import InputError, ItemUnavailable, PostError from core.error import InputError, ItemUnavailable, PostError
from core.item import ItemFactory, Stamina6 from core.item import ItemFactory, Stamina6
from core.purchase import Purchase, PurchaseList from core.purchase import Purchase, PurchaseList
from core.redeem import UserRedeem from core.redeem import UserRedeem
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,12 +1,15 @@
from random import randint
from time import time from time import time
from core.course import CoursePlay
from flask import Blueprint, request
from core.constant import Constant
from core.course import CoursePlay
from core.error import InputError from core.error import InputError
from core.rank import RankList from core.rank import RankList
from core.score import UserPlay from core.score import UserPlay
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return
@@ -24,24 +27,38 @@ def score_token():
@arc_try @arc_try
def score_token_world(user_id): def score_token_world(user_id):
stamina_multiply = int( d = request.args.get
request.args['stamina_multiply']) if 'stamina_multiply' in request.args else 1
fragment_multiply = int( stamina_multiply = int(d('stamina_multiply', 1))
request.args['fragment_multiply']) if 'fragment_multiply' in request.args else 100 fragment_multiply = int(d('fragment_multiply', 100))
prog_boost_multiply = int( prog_boost_multiply = int(d('prog_boost_multiply', 0))
request.args['prog_boost_multiply']) if 'prog_boost_multiply' in request.args else 0 beyond_boost_gauge_use = int(d('beyond_boost_gauge_use', 0))
skill_cytusii_flag = None
skill_id = d('skill_id')
if (skill_id == 'skill_ilith_ivy' or skill_id == 'skill_hikari_vanessa') and d('is_skill_sealed') == 'false':
# 处理 ivy 技能或者 vanessa 技能
# TODO: 需要重构整个 user_play世界模式 / 课题模式,所以现在临时 work 一下
skill_cytusii_flag = ''.join([str(randint(0, 2)) for _ in range(5)])
with Connect() as c: with Connect() as c:
x = UserPlay(c, UserOnline(c, user_id)) x = UserPlay(c, UserOnline(c, user_id))
x.song.set_chart(request.args['song_id'], int( x.song.set_chart(request.args['song_id'], int(
request.args['difficulty'])) request.args['difficulty']))
x.set_play_state_for_world(stamina_multiply, x.set_play_state_for_world(
fragment_multiply, prog_boost_multiply) stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag)
return success_return({
r = {
"stamina": x.user.stamina.stamina, "stamina": x.user.stamina.stamina,
"max_stamina_ts": x.user.stamina.max_stamina_ts, "max_stamina_ts": x.user.stamina.max_stamina_ts,
"token": x.song_token "token": x.song_token,
} }
) if skill_cytusii_flag and skill_id:
r['play_parameters'] = {
skill_id: list(
map(lambda x: Constant.WORLD_VALUE_NAME_ENUM[int(x)], skill_cytusii_flag))
}
return success_return(r)
@bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token @bp.route('/token/course', methods=['GET']) # 课题模式成绩上传所需的token
@@ -96,6 +113,10 @@ def song_score_post(user_id):
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type']) request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
x.beyond_gauge = int(request.form['beyond_gauge']) x.beyond_gauge = int(request.form['beyond_gauge'])
x.submission_hash = request.form['submission_hash'] x.submission_hash = request.form['submission_hash']
if 'combo_interval_bonus' in request.form:
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
x.highest_health = request.form.get("highest_health", type=int)
x.lowest_health = request.form.get("lowest_health", type=int)
if not x.is_valid: if not x.is_valid:
raise InputError('Invalid score.', 107) raise InputError('Invalid score.', 107)
x.upload_score() x.upload_score()

View File

@@ -1,14 +1,14 @@
from flask import Blueprint, current_app, request
from core.character import UserCharacter from core.character import UserCharacter
from core.config_manager import Config from core.error import ArcError
from core.error import ArcError, NoAccess
from core.item import ItemCore from core.item import ItemCore
from core.save import SaveData from core.save import SaveData
from core.sql import Connect from core.sql import Connect
from core.user import User, UserLogin, UserOnline, UserRegister from core.user import User, UserLogin, UserOnline, UserRegister
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, header_check, success_return
bp = Blueprint('user', __name__, url_prefix='/user') bp = Blueprint('user', __name__, url_prefix='/user')
@@ -16,10 +16,9 @@ bp = Blueprint('user', __name__, url_prefix='/user')
@bp.route('', methods=['POST']) # 注册接口 @bp.route('', methods=['POST']) # 注册接口
@arc_try @arc_try
def register(): def register():
headers = request.headers error = header_check(request)
if Config.ALLOW_APPVERSION: # 版本检查 if error is not None:
if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: raise error
raise NoAccess('Invalid app version.', 1203)
with Connect() as c: with Connect() as c:
new_user = UserRegister(c) new_user = UserRegister(c)
@@ -37,6 +36,7 @@ def register():
user = UserLogin(c) user = UserLogin(c)
user.login(new_user.name, new_user.password, user.login(new_user.name, new_user.password,
device_id, request.remote_addr) device_id, request.remote_addr)
current_app.logger.info(f'New user `{user.user_id}` registered')
return success_return({'user_id': user.user_id, 'access_token': user.token}) return success_return({'user_id': user.user_id, 'access_token': user.token})
@@ -155,7 +155,7 @@ def sys_set(user_id, set_arg):
user.change_favorite_character(int(value)) user.change_favorite_character(int(value))
else: else:
value = 'true' == value value = 'true' == value
if 'is_hide_rating' == set_arg or 'max_stamina_notification_enabled' == set_arg: if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'):
user.update_user_one_column(set_arg, value) user.update_user_one_column(set_arg, value)
return success_return(user.to_dict()) return success_return(user.to_dict())
@@ -165,3 +165,9 @@ def sys_set(user_id, set_arg):
@arc_try @arc_try
def user_delete(user_id): def user_delete(user_id):
raise ArcError('Cannot delete the account.', 151, status=404) raise ArcError('Cannot delete the account.', 151, status=404)
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
@arc_try
def email_resend_verify():
raise ArcError('Email verification unavailable.', 151, status=404)

View File

@@ -1,7 +1,8 @@
from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from core.world import UserMap, get_world_all from core.world import UserMap, get_world_all
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -422,7 +422,7 @@ def all_character():
def change_character(): def change_character():
# 修改角色数据 # 修改角色数据
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror'] 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap']
return render_template('web/changechar.html', skill_ids=skill_ids) return render_template('web/changechar.html', skill_ids=skill_ids)

View File

@@ -25,7 +25,7 @@ def random_str(randomlength=10):
def update_user_char(c): def update_user_char(c):
# 用character数据更新user_char # 用character数据更新user_char_full
c.execute('''select character_id, max_level, is_uncapped from character''') c.execute('''select character_id, max_level, is_uncapped from character''')
x = c.fetchall() x = c.fetchall()
c.execute('''select user_id from user''') c.execute('''select user_id from user''')
@@ -36,7 +36,7 @@ def update_user_char(c):
c.execute('''delete from user_char_full where user_id=:a and character_id=:b''', { c.execute('''delete from user_char_full where user_id=:a and character_id=:b''', {
'a': j[0], 'b': i[0]}) 'a': j[0], 'b': i[0]})
exp = 25000 if i[1] == 30 else 10000 exp = 25000 if i[1] == 30 else 10000
c.execute('''insert into user_char_full values(?,?,?,?,?,?)''', c.execute('''insert into user_char_full values(?,?,?,?,?,?,?)''',
(j[0], i[0], i[1], exp, i[2], 0)) (j[0], i[0], i[1], exp, i[2], 0))

121
tools/clean_logdb.py Normal file
View File

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