18 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
39 changed files with 1325 additions and 251 deletions

105
README.md
View File

@@ -17,42 +17,53 @@ This procedure is mainly used for study and research, and shall not be used for
## 特性 Features
有以下 We have
:x: : 不支持 Not supported
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
:construction: : 建设中 In construction
- 登录、注册 Login and registration
- 多设备登录 Multi device login
- 多设备自动封号 Auto-ban of multiple devices
- :warning: 多设备登录 Multi device login
- 登录频次限制 Login rate limit
- :x: 销号 Destroy account
- 成绩上传 Score upload
- PTT
- 世界排名 Global rank
- 排名 Rank
- 成绩校验 Score check
- 成绩排名 Score rank
- 潜力值机制 Potential
- Best 30
- :warning: Recent Top 10
- :warning: 世界排名 Global rank
- 段位系统 Course system
- Link Play
- :warning: Link Play
- 好友系统 Friends
- :x: 好友位提升 Max friend number increase
- 云端存档 Cloud save
- 爬梯 Climbing steps
- 自定义世界模式 Customizable World Mode
- 自定义歌曲下载 Customizable songs download
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
- 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
- 世界模式 World mode
- 体力系统 Stamina system
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
- :warning: 角色技能 Character skills
- 歌曲下载 Songs downloading
- :x: 加密下载 Encrypted downloading
- 下载校验 Download check
- 下载频次限制 Download rate limit
- 购买系统 Purchase system
- 单曲和曲包 Single & Pack
- :x: 捆绑包 Bundle
- 折扣 Discount
- 五周年兑换券 5-th anniversary ticket
- :x: Extend 包自动降价 Extend pack automatic price reduction
- 奖励系统 Present system
- 兑换码系统 Redeem code system
- 角色系统 Character system
- 全剧情解锁 Unlock all the stories
- 后台查询 Background search
- 后台自定义信息 Customize some things in the background
- 成绩校验 Score check
- 下载校验 Download check
- 数据记录 Data recording
- 用户成绩 Users' scores
- 用户每日潜力值 Users' daily potential
- :wastebasket: 简单的网页管理后台 Simple web admin backend
- :construction: API
- 服务器日志 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
只是很有趣,用处探索中。
@@ -69,22 +80,46 @@ It is just so interesting. What it can do is under exploration.
只保留最新版本 Only keep the latest version.
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
>
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
> Tips: When updating, please keep the original database in case of data loss.
>
> 其它小改动请参考各个 commit 信息
> Please refer to the commit messages for other minor changes.
### Version 2.11.1
### Version 2.11.3
- 适用于Arcaea 4.4.0版本 For Arcaea 4.4.0
- 新搭档 **密特拉·泰尔塞拉**、**不来方斗亚** 已解锁 Unlock the character **Mithra Tercera** and **Toa Kozukata**.
- **密特拉·泰尔塞拉** 的技能提供支持 Add support for the skill of **Mithra Tercera**.
- 新增修改搭档的API接口 Add some API endpoints about characters.
> v2.11.2.1 ~ v2.11.2.7 for Arcaea 4.5.0 ~ 5.2.0
- 适用于 Arcaea 5.2.0 版本
For Arcaea 5.2.0
- 新搭档 **Ilith & Ivy**、**Hikari & Vanessa**、**摩耶**、**露恩** 已解锁(注意“ 洞烛(至高:第八探索者)”因客户端因素不可选用)
Unlock the character **Ilith & Ivy**, **Hikari & Vanessa**, **Maya**, and **Luin**. (Note that "Insight(Ascendant - 8th Seeker)" is not available due to the client.)
- 为以上角色的技能提供服务端支持
Provide server-side support for the skills of the above characters.
- 设置中新增可选选项 `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
- Windows/Linux/Mac OS/Android...
- Windows / Linux / Mac OS / Android...
- 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)
<!--
@@ -105,7 +140,7 @@ It is just so interesting. What it can do is under exploration.
## 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

View File

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

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

@@ -1,6 +1,6 @@
from base64 import b64decode
from flask import Blueprint, request
from flask import Blueprint, current_app, request
from core.api_user import APIUser
from core.error import PostError
@@ -32,6 +32,7 @@ def token_post(data):
with Connect() as c:
user = APIUser(c)
user.login(name, password, request.remote_addr)
current_app.logger.info(f'API user `{user.user_id}` log in')
return success_return({'token': user.api_token, 'user_id': user.user_id})

View File

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

View File

@@ -127,7 +127,8 @@ class APIUser(UserOnline):
if ip is not None:
self.ip = ip
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''', {
'a': self.name})
@@ -136,9 +137,9 @@ class APIUser(UserOnline):
raise NoData(
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
if x[1] == '':
raise UserBan(f'The user `{self.name}` is banned.')
raise UserBan(f'The user `{x[0]}` is banned.')
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)
self.user_id = x[0]

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

@@ -217,6 +217,8 @@ class UserCharacter(Character):
self.character_id = character_id
self.user = user
self.skill_flag: bool = None
@property
def skill_id_displayed(self) -> str:
'''对外显示的技能id'''
@@ -226,6 +228,12 @@ class UserCharacter(Character):
return self.skill.skill_id
return None
@property
def skill_state(self) -> str:
if self.skill_id_displayed == 'skill_maya':
return 'add_random' if self.skill_flag else 'remove_random'
return None
def select_character_uncap_condition(self, user=None):
# parameter: user - User类或子类的实例
# 获取此角色的觉醒信息
@@ -255,20 +263,22 @@ class UserCharacter(Character):
if y is None:
raise NoData('The character of the user does not exist.')
self.name = y[7]
self.char_type = y[22]
self.name = y[8]
self.char_type = y[23]
self.is_uncapped = y[4] == 1
self.is_uncapped_override = y[5] == 1
self.level.level = y[2]
self.level.exp = y[3]
self.level.max_level = y[8]
self.frag.set_parameter(y[9], y[12], y[15])
self.prog.set_parameter(y[10], y[13], y[16])
self.overdrive.set_parameter(y[11], y[14], y[17])
self.skill.skill_id = y[18]
self.skill.skill_id_uncap = y[21]
self.skill.skill_unlock_level = y[19]
self.skill.skill_requires_uncap = y[20] == 1
self.level.max_level = y[9]
self.frag.set_parameter(y[10], y[13], y[16])
self.prog.set_parameter(y[11], y[14], y[17])
self.overdrive.set_parameter(y[12], y[15], y[18])
self.skill.skill_id = y[19]
self.skill.skill_id_uncap = y[22]
self.skill.skill_unlock_level = y[20]
self.skill.skill_requires_uncap = y[21] == 1
self.skill_flag = y[6] == 1
if self.character_id in (21, 46):
self.voice = [0, 1, 2, 3, 100, 1000, 1001]
@@ -302,6 +312,11 @@ class UserCharacter(Character):
r['fatalis_is_limited'] = False # emmmmmmm
if self.character_id in [1, 6, 7, 17, 18, 24, 32, 35, 52]:
r['base_character_id'] = 1
x = self.skill_state
if x:
r['skill_state'] = x
return r
def change_uncap_override(self, user=None):
@@ -349,7 +364,7 @@ class UserCharacter(Character):
self.c.execute(
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
y = self.c.fetchone()
if not y or i.amount > y[0]:
if i.amount > 0 and (not y or i.amount > y[0]):
raise ItemNotEnough('The cores are not enough.')
for i in self.uncap_cores:
@@ -409,6 +424,12 @@ class UserCharacter(Character):
core.user_claim_item(self.user)
self.upgrade(self.user, - core.amount * Constant.CORE_EXP)
def change_skill_state(self) -> None:
'''翻转技能状态,目前为 skill_miya 专用'''
self.skill_flag = not self.skill_flag
self.c.execute(f'''update {self.database_table_name} set skill_flag = ? where user_id = ? and character_id = ?''', (
1 if self.skill_flag else 0, self.user.user_id, self.character_id))
class UserCharacterList:
'''

View File

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

View File

@@ -1,6 +1,7 @@
from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.1'
ARCAEA_SERVER_VERSION = 'v2.11.2.7'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
class Constant:
@@ -18,6 +19,11 @@ 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,
17: 6100, 18: 7200, 19: 8500, 20: 10000, 21: 11500, 22: 13000, 23: 14500, 24: 16000, 25: 17500, 26: 19000, 27: 20500, 28: 22000, 29: 23500, 30: 25000}
WORLD_VALUE_NAME_ENUM = ['frag', 'prog', 'over']
FREE_PACK_NAME = 'base'
SINGLE_PACK_NAME = 'single'
ETO_UNCAP_BONUS_PROGRESS = 7
LUNA_UNCAP_BONUS_PROGRESS = 7
AYU_UNCAP_BONUS_PROGRESS = 5
@@ -50,6 +56,8 @@ class Constant:
LINKPLAY_TCP_PORT = Config.LINKPLAY_TCP_PORT
LINKPLAY_UDP_PORT = Config.LINKPLAY_UDP_PORT
LINKPLAY_AUTHENTICATION = Config.LINKPLAY_AUTHENTICATION
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
# Well, I can't say a word when I see this.
FINALE_SWITCH = [
@@ -101,4 +109,6 @@ class Constant:
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

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

View File

@@ -7,11 +7,12 @@ from time import time
from traceback import format_exc
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.download import DownloadList
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.util import try_rename
@@ -208,6 +209,29 @@ class FileChecker:
self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False
else:
# 检查更新
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
try:
x = c.execute(
'''select value from cache where key="version"''').fetchone()
except:
x = None
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
self.logger.warning(
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
try:
self.logger.info(
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
self.update_log_database()
self.logger.info(
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库
try:
@@ -275,6 +299,12 @@ class FileChecker:
DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path)
@staticmethod
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
'''直接更新日志数据库'''
if os.path.isfile(old_path):
LogDatabaseMigrator(old_path).update_database()
def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)

View File

@@ -195,7 +195,7 @@ class ItemCharacter(UserItem):
'''select exists(select * from user_char where user_id=? and character_id=?)''', (user.user_id, self.item_id))
if self.c.fetchone() == (0,):
self.c.execute(
'''insert into user_char values(?,?,1,0,0,0)''', (user.user_id, self.item_id))
'''insert into user_char values(?,?,1,0,0,0,0)''', (user.user_id, self.item_id))
class Memory(UserItem):

View File

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

View File

@@ -96,13 +96,16 @@ class SaveData:
i['c'] = True
i['r'] = True
for i in self.unlocklist_data:
if i['unlock_key'][-3:] == '101':
x = i['unlock_key']
if x[-3:] == '101':
i['complete'] = 100
elif i['unlock_key'][:16] == 'aegleseeker|2|3|':
elif x[:16] == 'aegleseeker|2|3|':
i['complete'] = 10
elif i['unlock_key'] == 'saikyostronger|2|3|einherjar|2':
elif x == 'saikyostronger|2|3|einherjar|2':
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
else:
i['complete'] = 1

View File

@@ -2,13 +2,15 @@ from base64 import b64encode
from os import urandom
from time import time
from .bgtask import BGTask, logdb_execute
from .config_manager import Config
from .constant import Constant
from .course import CoursePlay
from .error import NoData, StaminaNotEnough
from .item import ItemCore
from .song import Chart
from .sql import Connect, Query, Sql
from .util import md5
from .util import get_today_timestamp, md5
from .world import WorldPlay
@@ -221,6 +223,9 @@ class UserPlay(UserScore):
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:
# 不能super
@@ -298,15 +303,17 @@ class UserPlay(UserScore):
self.fragment_multiply = int(x[9])
self.prog_boost_multiply = int(x[10])
self.beyond_boost_gauge_usage = int(x[11])
self.skill_cytusii_flag = x[12]
self.is_world_mode = True
self.course_play_state = -1
def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: int = 100, prog_boost_multiply: int = 0, beyond_boost_gauge_usage: int = 0) -> 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.stamina_multiply = int(stamina_multiply)
self.fragment_multiply = int(fragment_multiply)
self.prog_boost_multiply = int(prog_boost_multiply)
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage)
self.skill_cytusii_flag = skill_cytusii_flag
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})
@@ -318,8 +325,8 @@ class UserPlay(UserScore):
self.beyond_boost_gauge_usage = 0
self.clear_play_state()
self.c.execute('''insert into songplay_token values(:t,:a,:b,:c,'',-1,0,0,:d,:e,:f,:g)''', {
'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.stamina_multiply, 'e': self.fragment_multiply, 'f': self.prog_boost_multiply, 'g': self.beyond_boost_gauge_usage, 't': self.song_token})
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, 'g': self.beyond_boost_gauge_usage, 'h': self.skill_cytusii_flag, 't': self.song_token})
self.user.select_user_about_current_map()
self.user.current_map.select_map_info()
@@ -351,8 +358,8 @@ class UserPlay(UserScore):
self.course_play.score = 0
self.course_play.clear_type = 3 # 设置为PM即最大值
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.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.user.select_user_about_stamina()
if use_course_skip_purchase:
x = ItemCore(self.c)
@@ -427,9 +434,20 @@ class UserPlay(UserScore):
def record_score(self) -> None:
'''向log数据库记录分数请注意列名不同'''
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2:
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))
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def record_rating_ptt(self, user_rating_ptt: float) -> None:
'''向log数据库记录用户ptt变化'''
today_timestamp = get_today_timestamp()
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
self.user.user_id, today_timestamp)).fetchone()
old_ptt = 0 if old_ptt is None else old_ptt[0]
if old_ptt != user_rating_ptt:
c2.execute('''insert or replace into user_rating values(?,?,?)''',
(self.user.user_id, today_timestamp, user_rating_ptt))
def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
@@ -474,7 +492,9 @@ class UserPlay(UserScore):
self.update_recent30()
# 总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''', {
'a': self.user.rating_ptt, 'b': self.user.user_id})

View File

@@ -1,8 +1,10 @@
import os
import sqlite3
import traceback
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
@@ -385,7 +387,7 @@ class DatabaseMigrator:
c.execute('''delete from user_char_full''')
for i in x:
exp = 25000 if i[1] == 30 else 10000
c.executemany('''insert into user_char_full values(?,?,?,?,?,?)''', [
c.executemany('''insert into user_char_full values(?,?,?,?,?,?,0)''', [
(j[0], i[0], i[1], exp, i[2], 0) for j in y])
def update_database(self) -> None:
@@ -404,6 +406,31 @@ class DatabaseMigrator:
self.update_user_char_full(c2) # 更新user_char_full
class LogDatabaseMigrator:
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
self.c1_path = c1_path
# self.c2_path = c2_path
self.init_folder_path = Config.DATABASE_INIT_PATH
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_update(self) -> None:
'''直接更新数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
def update_database(self) -> None:
with Connect(self.c1_path) as c:
self.c = c
self.table_update()
class MemoryDatabase:
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)

View File

@@ -127,9 +127,9 @@ class UserRegister(User):
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.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.c.execute(
'''select character_id, max_level, is_uncapped from character''')
@@ -137,7 +137,7 @@ class UserRegister(User):
if x:
for i in x:
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))
def register(self):
@@ -237,7 +237,8 @@ class UserLogin(User):
self.set_ip(ip)
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''', {
'name': self.name})
@@ -251,7 +252,7 @@ class UserLogin(User):
# 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > self.now:
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
'remaining_ts': ban_timestamp-self.now})
if x[1] == '':
@@ -260,7 +261,7 @@ class UserLogin(User):
f'The account `{self.user_id}` has been banned.', 106)
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(
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
@@ -763,7 +764,7 @@ class UserChanger(UserInfo, UserRegister):
if columns is not None:
d = {}
for column in columns:
if column == 'password':
if column == 'password' and self.password != '':
d[column] = self.hash_pwd
else:
d[column] = self.__dict__[column]

View File

@@ -1,5 +1,28 @@
import hashlib
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:
@@ -37,3 +60,8 @@ def try_rename(path: str, new_path: str) -> str:
os.rename(path, final_path)
return final_path
def get_today_timestamp():
'''相对于本机本地时间的今天0点的时间戳'''
return int(mktime(date.today().timetuple()))

View File

@@ -500,10 +500,13 @@ class WorldPlay:
r['char_stats']['prog_tempest'] = self.prog_tempest
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
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:
r["user_map"]["steps"] = [
x.to_dict() for x in arcmap.steps_for_climbing]
@@ -653,22 +656,17 @@ class WorldPlay:
self.user.current_map.update()
def before_calculate(self) -> None:
if self.user_play.beyond_gauge == 0:
if self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
self._special_tempest()
elif self.character_used.skill_id_displayed == 'ilith_awakened_skill':
self._ilith_awakened_skill()
elif self.character_used.skill_id_displayed == 'skill_mithra':
self._skill_mithra()
else:
if self.character_used.skill_id_displayed == 'skill_vita':
self._skill_vita()
if self.character_used.skill_id_displayed == 'skill_mika':
self._skill_mika()
factory_dict = {'skill_vita': self._skill_vita, 'skill_mika': self._skill_mika, 'skill_ilith_ivy': self._skill_ilith_ivy,
'ilith_awakened_skill': self._ilith_awakened_skill, 'skill_hikari_vanessa': self._skill_hikari_vanessa}
if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
self._special_tempest()
if self.character_used.skill_id_displayed in factory_dict:
factory_dict[self.character_used.skill_id_displayed]()
def after_climb(self) -> None:
factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap,
'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane}
'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:
factory_dict[self.character_used.skill_id_displayed]()
@@ -709,7 +707,7 @@ class WorldPlay:
if fragment_flag:
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)
@@ -718,7 +716,7 @@ class WorldPlay:
x: 'Step' = self.user.current_map.steps_for_climbing[0]
if x.restrict_id and x.restrict_type:
self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS
self.step_value += self.character_bonus_progress * self.step_times
self.step_value += self.character_bonus_progress
self.user.current_map.reclimb(self.step_value)
@@ -728,9 +726,9 @@ class WorldPlay:
self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random(
) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS
self.step_value += self.character_bonus_progress * self.step_times
self.step_value += self.character_bonus_progress
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.user.current_map.reclimb(self.step_value)
@@ -749,7 +747,7 @@ class WorldPlay:
'''
x: 'Step' = self.user.current_map.steps_for_climbing[0]
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
self.character_bonus_progress = -1 * self.step_value / 2 / self.step_times
self.character_bonus_progress = -self.step_value / 2
self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value)
@@ -772,7 +770,41 @@ class WorldPlay:
def _skill_mithra(self) -> None:
'''
mithra 技能,每 150 combo 增加世界模式进度+1,直接理解成 prog 值增加
mithra 技能,每 150 combo 增加世界模式进度+1
'''
if self.user_play.combo_interval_bonus:
self.prog_skill_increase = 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:
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata']
'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',
'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']
'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', '', '', '', '', '', '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,
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, 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,
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]
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,
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]
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,
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]
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,
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]
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,
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]
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,
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]
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,
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]
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, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53]
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,
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]
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,
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]
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 = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
@@ -59,14 +59,16 @@ class InitData:
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', 'amount': 30}],
66: [{'core_id': 'core_chunithm', 'amount': 15}]
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',
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral']
'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",
"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']
"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",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]

View File

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

View File

@@ -682,5 +682,113 @@
],
"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

@@ -1378,5 +1378,185 @@
],
"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

@@ -69,6 +69,7 @@ level int,
exp real,
is_uncapped int,
is_uncapped_override int,
skill_flag int,
primary key(user_id, character_id)
);
create table if not exists user_char_full(user_id int,
@@ -77,6 +78,7 @@ level int,
exp real,
is_uncapped int,
is_uncapped_override int,
skill_flag int,
primary key(user_id, character_id)
);
create table if not exists character(character_id int primary key,
@@ -184,7 +186,8 @@ course_clear_type int,
stamina_multiply int,
fragment_multiply int,
prog_boost_multiply int,
beyond_boost_gauge_usage int
beyond_boost_gauge_usage int,
skill_cytusii_flag text
);
create table if not exists item(item_id text,
type text,

View File

@@ -1,12 +1,27 @@
{
"songs": [
{
"id": "dement",
"difficulties": [
"songs": [
{
"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,19 +4,22 @@ class Config:
'''
'''
服务器地址、端口号、校验码
Server address, port and verification code
服务器地址、端口号、校验码、传输加密密钥
Server address, port, verification code, and encryption key
'''
HOST = '0.0.0.0'
UDP_PORT = 10900
TCP_PORT = 10901
AUTHENTICATION = 'my_link_play_server'
TCP_SECRET_KEY = '1145141919810'
'''
--------------------------------------------------
'''
DEBUG = False
TCP_MAX_LENGTH = 0x0FFFFFFF
TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000

View File

@@ -1,7 +1,8 @@
import binascii
# import binascii
import logging
import socketserver
import threading
from json import dumps, loads
from .aes import decrypt, encrypt
from .config import Config
@@ -16,6 +17,7 @@ logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message
class UDP_handler(socketserver.BaseRequestHandler):
def handle(self):
client_msg, server = self.request
# print(client_msg)
try:
token = client_msg[:8]
iv = client_msg[8:20]
@@ -54,23 +56,52 @@ class UDP_handler(socketserver.BaseRequestHandler):
ciphertext, self.client_address)
class TCP_handler(socketserver.StreamRequestHandler):
def handle(self):
self.data = self.rfile.readline().strip()
AUTH_LEN = len(Config.AUTHENTICATION)
TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16]
message = self.data.decode('utf-8')
if Config.DEBUG:
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
data = message.split('|')
if data[0] != Config.AUTHENTICATION:
self.wfile.write(b'No authentication')
logging.warning(f'TCP-{self.client_address[0]}-No authentication')
class TCP_handler(socketserver.StreamRequestHandler):
def handle(self):
try:
if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
self.wfile.write(b'No authentication')
logging.warning(
f'TCP-{self.client_address[0]}-No authentication')
return None
cipher_len = int.from_bytes(self.rfile.read(8), byteorder='little')
if cipher_len > Config.TCP_MAX_LENGTH:
self.wfile.write(b'Body too long')
logging.warning(f'TCP-{self.client_address[0]}-Body too long')
return None
iv = self.rfile.read(12)
tag = self.rfile.read(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
r = TCPRouter(data[1:]).handle()
if Config.DEBUG:
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
self.wfile.write(r.encode('utf-8'))
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
r = TCPRouter(data).handle()
try:
r = dumps(r)
if Config.DEBUG:
logging.info(f'TCP-To-{self.client_address[0]}-{r}')
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
r = len(ciphertext).to_bytes(8, byteorder='little') + \
iv + tag[:12] + ciphertext
except Exception as e:
logging.error(e)
return None
self.wfile.write(r)
def link_play(ip: str = Config.HOST, udp_port: int = Config.UDP_PORT, tcp_port: int = Config.TCP_PORT):

View File

@@ -43,7 +43,9 @@ def unique_random(dataset, length=8, random_func=None):
def clear_player(token):
# 清除玩家信息和token
del Store.player_dict[Store.link_play_data[token]['player_id']]
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]
@@ -51,6 +53,7 @@ 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
@@ -84,19 +87,22 @@ def memory_clean(now):
class TCPRouter:
clean_timer = 0
router = {
'0': 'debug',
'1': 'create_room',
'2': 'join_room',
'3': 'update_room',
'debug',
'create_room',
'join_room',
'update_room',
'get_rooms',
}
def __init__(self, data: list):
self.data = data # data: list[str] = [command, ...]
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):
def debug(self) -> dict:
if Config.DEBUG:
return eval(self.data[1])
return 'ok'
return {'result': eval(self.data['code'])}
return {'hello_world': 'ok'}
@staticmethod
def clean_check():
@@ -106,14 +112,21 @@ class TCPRouter:
TCPRouter.clean_timer = now
memory_clean(now)
def handle(self) -> str:
def handle(self) -> dict:
self.clean_check()
if self.data[0] not in self.router:
if self.endpoint not in self.router:
return None
r = getattr(self, self.router[self.data[0]])()
if isinstance(r, tuple):
return '|'.join(map(str, r))
return str(r)
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:
@@ -141,12 +154,12 @@ class TCPRouter:
return room
def create_room(self) -> tuple:
def create_room(self) -> dict:
# 开房
# data = ['1', name, song_unlock, ]
# song_unlock: base64 str
name = self.data[1]
song_unlock = b64decode(self.data[2])
name = self.data['name']
song_unlock = b64decode(self.data['song_unlock'])
key = urandom(16)
with Store.lock:
@@ -169,33 +182,39 @@ class TCPRouter:
}
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`')
return (0, room.room_code, room.room_id, token, b64encode(key).decode('utf-8'), player.player_id)
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) -> tuple:
def join_room(self) -> 'dict | int':
# 入房
# data = ['2', name, song_unlock, room_code]
# song_unlock: base64 str
room_code = self.data[3].upper()
room_code = self.data['room_code'].upper()
key = urandom(16)
name = self.data[1]
song_unlock = b64decode(self.data[2])
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'
return 1202
room: Room = Store.room_code_dict[room_code]
player_num = room.player_num
if player_num == 4:
# 满人
return '1201'
return 1201
if player_num == 0:
# 房间不存在
return '1202'
return 1202
if room.state != 2:
# 无法加入
return '1205'
return 1205
token = unique_random(Store.link_play_data)
@@ -216,16 +235,68 @@ class TCPRouter:
}
logging.info(f'TCP-Player `{name}` joins room `{room_code}`')
return (0, room_code, room.room_id, token, b64encode(key).decode('utf-8'), player.player_id, b64encode(room.song_unlock).decode('utf-8'))
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) -> tuple:
def update_room(self) -> dict:
# 房间信息更新
# data = ['3', token]
token = int(self.data[1])
token = int(self.data['token'])
with Store.lock:
if token not in Store.link_play_data:
return '108'
return 108
r = Store.link_play_data[token]
room = r['room']
logging.info(f'TCP-Room `{room.room_code}` info update')
return (0, room.room_code, room.room_id, b64encode(r['key']).decode('utf-8'), room.players[r['player_index']].player_id, b64encode(room.song_unlock).decode('utf-8'))
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,4 @@
import logging
from time import time
from .config import Config
@@ -44,6 +45,31 @@ class Player:
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):
self.player_name = player_name.encode('ascii')
if len(self.player_name) > 16:
@@ -75,6 +101,32 @@ class Room:
self.command_queue = []
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)
@@ -133,16 +185,23 @@ class Room:
for i in range(4):
if self.players[i].player_id == self.host_id:
for j in range(1, 4):
if self.players[(i + j) % 4].player_id != 0:
self.host_id = self.players[(i + j) % 4].player_id
player = self.players[(i + j) % 4]
if player.player_id != 0:
self.host_id = player.player_id
logging.info(
f'Player `{player.name}` becomes the host of room `{self.room_code}`')
break
break
def delete_player(self, player_index: int):
# 删除某个玩家
if self.players[player_index].player_id == self.host_id:
player = self.players[player_index]
if player.player_id == self.host_id:
self.make_round()
logging.info(
f'Player `{player.name}` leaves room `{self.room_code}`')
self.players[player_index].online = 0
self.players[player_index] = Player()
self.update_song_unlock()
@@ -203,3 +262,10 @@ class Room:
for i in max_score_i:
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
from .udp_class import Room, bi
@@ -44,6 +45,8 @@ class CommandParser:
for i in self.room.players:
if i.player_id == player_id and i.online == 1:
self.room.host_id = player_id
logging.info(
f'Player `{i.name}` becomes the host of room `{self.room.room_code}`')
self.s.random_code = self.command[16:24]
self.room.command_queue.append(self.s.command_10())
@@ -189,6 +192,8 @@ class CommandParser:
# 将换房主时间提前到此刻
self.room.make_round()
logging.info(f'Room `{self.room.room_code}` starts playing')
if self.room.state in (4, 5, 6):
timestamp = round(time.time() * 1000)
self.room.countdown -= timestamp - self.room.timestamp

View File

@@ -26,6 +26,7 @@ import api
import server
import web.index
import web.login
# import webapi
from core.constant import Constant
from core.download import UserDownload
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(api.bp)
app.register_blueprint(server.bp)
# app.register_blueprint(webapi.bp)
@app.route('/')

View File

@@ -32,7 +32,7 @@ def login():
user = UserLogin(c)
user.login(name, password, device_id, request.remote_addr)
current_app.logger.info(f'User `{user.user_id}` log in')
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})

View File

@@ -30,15 +30,16 @@ def game_info():
@auth_required(request)
@arc_try
def download_song(user_id):
with Connect(in_memory=True) as c:
x = DownloadList(c, UserOnline(None, user_id))
x.song_ids = request.args.getlist('sid')
x.url_flag = json.loads(request.args.get('url', 'true'))
if x.url_flag and x.is_limited:
raise RateLimit('You have reached the download limit.', 903)
with Connect(in_memory=True) as c_m:
with Connect() as c:
x = DownloadList(c_m, UserOnline(c, user_id))
x.song_ids = request.args.getlist('sid')
x.url_flag = json.loads(request.args.get('url', 'true'))
if x.url_flag and x.is_limited:
raise RateLimit('You have reached the download limit.', 903)
x.add_songs()
return success_return(x.urls)
x.add_songs()
return success_return(x.urls)
@bp.route('/finale/progress', methods=['GET'])
@@ -54,6 +55,11 @@ def finale_start():
return success_return({})
@bp.route('/finale/finale_end', methods=['POST'])
def finale_end():
return success_return({})
map_dict = {'/user/me': user_me,
'/purchase/bundle/pack': bundle_pack,
'/serve/download/me/song': download_song,

View File

@@ -1,7 +1,9 @@
from random import randint
from time import time
from flask import Blueprint, request
from core.constant import Constant
from core.course import CoursePlay
from core.error import InputError
from core.rank import RankList
@@ -25,22 +27,38 @@ def score_token():
@arc_try
def score_token_world(user_id):
stamina_multiply = int(request.args.get('stamina_multiply', 1))
fragment_multiply = int(request.args.get('fragment_multiply', 100))
prog_boost_multiply = int(request.args.get('prog_boost_multiply', 0))
beyond_boost_gauge_use = int(request.args.get('beyond_boost_gauge_use', 0))
d = request.args.get
stamina_multiply = int(d('stamina_multiply', 1))
fragment_multiply = int(d('fragment_multiply', 100))
prog_boost_multiply = int(d('prog_boost_multiply', 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:
x = UserPlay(c, UserOnline(c, user_id))
x.song.set_chart(request.args['song_id'], int(
request.args['difficulty']))
x.set_play_state_for_world(
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use)
return success_return({
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use, skill_cytusii_flag)
r = {
"stamina": x.user.stamina.stamina,
"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
@@ -97,6 +115,8 @@ def song_score_post(user_id):
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:
raise InputError('Invalid score.', 107)
x.upload_score()

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, request
from flask import Blueprint, current_app, request
from core.character import UserCharacter
from core.error import ArcError
@@ -36,6 +36,7 @@ def register():
user = UserLogin(c)
user.login(new_user.name, new_user.password,
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})

View File

@@ -422,7 +422,7 @@ def all_character():
def change_character():
# 修改角色数据
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', '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']
'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)

View File

@@ -25,7 +25,7 @@ def random_str(randomlength=10):
def update_user_char(c):
# 用character数据更新user_char
# 用character数据更新user_char_full
c.execute('''select character_id, max_level, is_uncapped from character''')
x = c.fetchall()
c.execute('''select user_id from user''')
@@ -36,7 +36,7 @@ def update_user_char(c):
c.execute('''delete from user_char_full where user_id=:a and character_id=:b''', {
'a': j[0], 'b': i[0]})
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))

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.')