12 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
31 changed files with 875 additions and 207 deletions

View File

@@ -86,14 +86,32 @@ It is just so interesting. What it can do is under exploration.
> 其它小改动请参考各个 commit 信息 > 其它小改动请参考各个 commit 信息
> Please refer to the commit messages for other minor changes. > Please refer to the commit messages for other minor changes.
### Version 2.11.2 ### Version 2.11.3
- 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6 > v2.11.2.1 ~ v2.11.2.7 for Arcaea 4.5.0 ~ 5.2.0
- 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**.
- 新增用户潜力值每日记录功能 Add support for recording users' potential each day. - 适用于 Arcaea 5.2.0 版本
- 修复搭档 **光 & 对立Reunion** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100) For Arcaea 5.2.0
- 添加 `finale/finale_end` 接口尝试修复最终挑战无法解锁结局的问题 Add the `finale/finale_end` endpoint to try to fix the problem that the endings cannot be unlocked correctly in the finale challenge. (#110) - 新搭档 **Ilith & Ivy**、**Hikari & Vanessa**、**摩耶**、**露恩** 已解锁(注意“ 洞烛(至高:第八探索者)”因客户端因素不可选用)
- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records. 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 ## 运行环境与依赖 Running environment and requirements

View File

@@ -1,15 +1,10 @@
from flask import Blueprint from flask import Blueprint
from . import (users, songs, token, system, items, 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 = 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(system.bp) bp.register_blueprint(i.bp)
bp.register_blueprint(items.bp)
bp.register_blueprint(purchases.bp)
bp.register_blueprint(presents.bp)
bp.register_blueprint(redeems.bp)
bp.register_blueprint(characters.bp)

View File

@@ -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

@@ -93,6 +93,10 @@ 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:
if data['password'] == '':
u.password = ''
r['password'] = ''
else:
u.set_password(data['password']) u.set_password(data['password'])
r['password'] = u.hash_pwd r['password'] = u.hash_pwd
if 'email' in data: if 'email' in data:

View File

@@ -217,6 +217,8 @@ 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'''
@@ -226,6 +228,12 @@ class UserCharacter(Character):
return self.skill.skill_id return self.skill.skill_id
return None 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): def select_character_uncap_condition(self, user=None):
# parameter: user - User类或子类的实例 # parameter: user - User类或子类的实例
# 获取此角色的觉醒信息 # 获取此角色的觉醒信息
@@ -255,20 +263,22 @@ class UserCharacter(Character):
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
self.skill_flag = y[6] == 1
if self.character_id in (21, 46): if self.character_id in (21, 46):
self.voice = [0, 1, 2, 3, 100, 1000, 1001] self.voice = [0, 1, 2, 3, 100, 1000, 1001]
@@ -302,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):
@@ -409,6 +424,12 @@ 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:
''' '''

View File

@@ -12,7 +12,7 @@ class Config:
SONG_FILE_HASH_PRE_CALCULATE = True SONG_FILE_HASH_PRE_CALCULATE = True
GAME_API_PREFIX = '/pollen/22' 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,6 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.2' ARCAEA_SERVER_VERSION = 'v2.11.2.7'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1' ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
@@ -19,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, 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
@@ -51,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 = [

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,28 +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(
@@ -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 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: 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

@@ -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)) '''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):

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')
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: except socket.timeout as e:
raise Timeout( raise Timeout(
'Timeout when waiting for data from link play server.', status=400) from e '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:
code = int(self.data_recv[0])
raise ArcError(f'Link Play error code: {code}', code, status=400) raise ArcError(f'Link Play error code: {code}', code, 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

@@ -96,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

View File

@@ -223,6 +223,9 @@ class UserPlay(UserScore):
self.course_play: 'CoursePlay' = None self.course_play: 'CoursePlay' = None
self.combo_interval_bonus: int = None # 不能给 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 # 不能super
@@ -300,15 +303,17 @@ class UserPlay(UserScore):
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.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, 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.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)
self.beyond_boost_gauge_usage = int(beyond_boost_gauge_usage) 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: 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''', { 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})
@@ -320,8 +325,8 @@ class UserPlay(UserScore):
self.beyond_boost_gauge_usage = 0 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,:g)''', { 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, '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()
@@ -353,7 +358,7 @@ class UserPlay(UserScore):
self.course_play.score = 0 self.course_play.score = 0
self.course_play.clear_type = 3 # 设置为PM即最大值 self.course_play.clear_type = 3 # 设置为PM即最大值
self.c.execute('''insert into songplay_token values(?,?,?,?,?,?,?,?,1,100,0,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:

View File

@@ -387,7 +387,7 @@ 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])
def update_database(self) -> None: def update_database(self) -> None:

View File

@@ -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):

View File

@@ -1,9 +1,30 @@
import hashlib import hashlib
import os import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from datetime import date from datetime import date
from time import mktime 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:
# md5加密算法 # md5加密算法
code = code.encode() code = code.encode()

View File

@@ -500,10 +500,13 @@ class WorldPlay:
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]
@@ -653,22 +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}
if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed:
self._special_tempest() self._special_tempest()
elif self.character_used.skill_id_displayed == 'ilith_awakened_skill':
self._ilith_awakened_skill() if self.character_used.skill_id_displayed in factory_dict:
elif self.character_used.skill_id_displayed == 'skill_mithra': factory_dict[self.character_used.skill_id_displayed]()
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()
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]()
@@ -709,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)
@@ -718,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)
@@ -728,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)
@@ -749,7 +747,7 @@ 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 = -1 * 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)
@@ -772,7 +770,41 @@ class WorldPlay:
def _skill_mithra(self) -> None: def _skill_mithra(self) -> None:
''' '''
mithra 技能,每 150 combo 增加世界模式进度+1,直接理解成 prog 值增加 mithra 技能,每 150 combo 增加世界模式进度+1
''' '''
if self.user_play.combo_interval_bonus: 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: 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)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)'] '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', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight'] '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', 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, 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, 45, 57, 56, 47, 33, 26, 29, 66, 40] 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, 45, 70, 37.5, 29, 44, 26, 26, 35, 40] 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, 45, 57, 31, 29, 65, 26, 29, 42.5, 40] 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, 75, 80, 90, 80, 50, 51, 54, 100, 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, 75, 100, 60, 50, 68, 51, 50, 53, 85] 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, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 60] 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, 75, 80, 90, 80, 50, 51, 64, 100, 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, 110, 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, 75, 100, 60, 50, 68, 51, 60, 53, 85] 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, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 60] 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, 2, 0, 0, 2, 0, 2, 2, 1] 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 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}],
@@ -60,14 +60,15 @@ class InitData:
19: [{'core_id': 'core_colorful', 'amount': 30}], 19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', '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}] 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_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", world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris'] "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

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

@@ -1468,5 +1468,95 @@
], ],
"orig_price": 100, "orig_price": 100,
"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, 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,
@@ -77,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,
@@ -184,7 +186,8 @@ 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 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

@@ -2,9 +2,24 @@
"songs": [ "songs": [
{ {
"id": "dement", "id": "dement",
"set": "base",
"purchase": "",
"difficulties": [ "difficulties": [
{ {
"ratingClass": 3 "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' 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 DEBUG = False
TCP_MAX_LENGTH = 0x0FFFFFFF
TIME_LIMIT = 3600000 TIME_LIMIT = 3600000
COMMAND_INTERVAL = 1000000 COMMAND_INTERVAL = 1000000

View File

@@ -1,7 +1,8 @@
import binascii # import binascii
import logging import logging
import socketserver import socketserver
import threading import threading
from json import dumps, loads
from .aes import decrypt, encrypt from .aes import decrypt, encrypt
from .config import Config from .config import Config
@@ -16,6 +17,7 @@ logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message
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]
@@ -54,23 +56,52 @@ class UDP_handler(socketserver.BaseRequestHandler):
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')
if Config.DEBUG: class TCP_handler(socketserver.StreamRequestHandler):
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
data = message.split('|') def handle(self):
if data[0] != Config.AUTHENTICATION: try:
if self.rfile.read(AUTH_LEN).decode('utf-8') != Config.AUTHENTICATION:
self.wfile.write(b'No authentication') self.wfile.write(b'No authentication')
logging.warning(f'TCP-{self.client_address[0]}-No authentication') logging.warning(
f'TCP-{self.client_address[0]}-No authentication')
return None return None
r = TCPRouter(data[1:]).handle() 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
if Config.DEBUG:
logging.info(f'TCP-From-{self.client_address[0]}-{message}')
r = TCPRouter(data).handle()
try:
r = dumps(r)
if Config.DEBUG: if Config.DEBUG:
logging.info(f'TCP-To-{self.client_address[0]}-{r}') logging.info(f'TCP-To-{self.client_address[0]}-{r}')
self.wfile.write(r.encode('utf-8')) 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): 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): def clear_player(token):
# 清除玩家信息和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] del Store.link_play_data[token]
@@ -51,6 +53,7 @@ def clear_room(room):
# 清除房间信息 # 清除房间信息
room_id = room.room_id room_id = room.room_id
room_code = room.room_code room_code = room.room_code
logging.info(f'Clean room `{room_code}`')
del Store.room_id_dict[room_id] del Store.room_id_dict[room_id]
del Store.room_code_dict[room_code] del Store.room_code_dict[room_code]
del room del room
@@ -84,19 +87,22 @@ def memory_clean(now):
class TCPRouter: class TCPRouter:
clean_timer = 0 clean_timer = 0
router = { router = {
'0': 'debug', 'debug',
'1': 'create_room', 'create_room',
'2': 'join_room', 'join_room',
'3': 'update_room', 'update_room',
'get_rooms',
} }
def __init__(self, data: list): def __init__(self, raw_data: 'dict | list'):
self.data = data # data: list[str] = [command, ...] 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: if Config.DEBUG:
return eval(self.data[1]) return {'result': eval(self.data['code'])}
return 'ok' return {'hello_world': 'ok'}
@staticmethod @staticmethod
def clean_check(): def clean_check():
@@ -106,14 +112,21 @@ class TCPRouter:
TCPRouter.clean_timer = now TCPRouter.clean_timer = now
memory_clean(now) memory_clean(now)
def handle(self) -> str: def handle(self) -> dict:
self.clean_check() self.clean_check()
if self.data[0] not in self.router: if self.endpoint not in self.router:
return None return None
r = getattr(self, self.router[self.data[0]])() try:
if isinstance(r, tuple): r = getattr(self, self.endpoint)()
return '|'.join(map(str, r)) except Exception as e:
return str(r) logging.error(e)
return 999
if isinstance(r, int):
return {'code': r}
return {
'code': 0,
'data': r
}
@staticmethod @staticmethod
def generate_player(name: str) -> Player: def generate_player(name: str) -> Player:
@@ -141,12 +154,12 @@ class TCPRouter:
return room return room
def create_room(self) -> tuple: def create_room(self) -> dict:
# 开房 # 开房
# data = ['1', name, song_unlock, ] # data = ['1', name, song_unlock, ]
# song_unlock: base64 str # song_unlock: base64 str
name = self.data[1] name = self.data['name']
song_unlock = b64decode(self.data[2]) song_unlock = b64decode(self.data['song_unlock'])
key = urandom(16) key = urandom(16)
with Store.lock: with Store.lock:
@@ -169,33 +182,39 @@ class TCPRouter:
} }
logging.info(f'TCP-Create room `{room.room_code}` by player `{name}`') 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] # data = ['2', name, song_unlock, room_code]
# song_unlock: base64 str # song_unlock: base64 str
room_code = self.data[3].upper() room_code = self.data['room_code'].upper()
key = urandom(16) key = urandom(16)
name = self.data[1] name = self.data['name']
song_unlock = b64decode(self.data[2]) song_unlock = b64decode(self.data['song_unlock'])
with Store.lock: with Store.lock:
if room_code not in Store.room_code_dict: if room_code not in Store.room_code_dict:
# 房间号错误 / 房间不存在 # 房间号错误 / 房间不存在
return '1202' return 1202
room: Room = Store.room_code_dict[room_code] room: Room = Store.room_code_dict[room_code]
player_num = room.player_num player_num = room.player_num
if player_num == 4: if player_num == 4:
# 满人 # 满人
return '1201' return 1201
if player_num == 0: if player_num == 0:
# 房间不存在 # 房间不存在
return '1202' return 1202
if room.state != 2: if room.state != 2:
# 无法加入 # 无法加入
return '1205' return 1205
token = unique_random(Store.link_play_data) token = unique_random(Store.link_play_data)
@@ -216,16 +235,68 @@ class TCPRouter:
} }
logging.info(f'TCP-Player `{name}` joins room `{room_code}`') 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] # data = ['3', token]
token = int(self.data[1]) token = int(self.data['token'])
with Store.lock: with Store.lock:
if token not in Store.link_play_data: if token not in Store.link_play_data:
return '108' return 108
r = Store.link_play_data[token] r = Store.link_play_data[token]
room = r['room'] room = r['room']
logging.info(f'TCP-Room `{room.room_code}` info update') 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 time import time
from .config import Config from .config import Config
@@ -44,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:
@@ -75,6 +101,32 @@ class Room:
self.command_queue = [] 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 @property
def command_queue_length(self) -> int: def command_queue_length(self) -> int:
return len(self.command_queue) return len(self.command_queue)
@@ -133,16 +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):
# 删除某个玩家 # 删除某个玩家
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() 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()
@@ -203,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
@@ -44,6 +45,8 @@ 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}`')
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
self.room.command_queue.append(self.s.command_10()) self.room.command_queue.append(self.s.command_10())
@@ -189,6 +192,8 @@ class CommandParser:
# 将换房主时间提前到此刻 # 将换房主时间提前到此刻
self.room.make_round() self.room.make_round()
logging.info(f'Room `{self.room.room_code}` starts playing')
if self.room.state in (4, 5, 6): if self.room.state in (4, 5, 6):
timestamp = round(time.time() * 1000) timestamp = round(time.time() * 1000)
self.room.countdown -= timestamp - self.room.timestamp self.room.countdown -= timestamp - self.room.timestamp

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

@@ -30,8 +30,9 @@ 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 = DownloadList(c_m, UserOnline(c, user_id))
x.song_ids = request.args.getlist('sid') x.song_ids = request.args.getlist('sid')
x.url_flag = json.loads(request.args.get('url', 'true')) x.url_flag = json.loads(request.args.get('url', 'true'))
if x.url_flag and x.is_limited: if x.url_flag and x.is_limited:

View File

@@ -1,7 +1,9 @@
from random import randint
from time import time from time import time
from flask import Blueprint, request from flask import Blueprint, request
from core.constant import Constant
from core.course import CoursePlay 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
@@ -25,22 +27,38 @@ def score_token():
@arc_try @arc_try
def score_token_world(user_id): def score_token_world(user_id):
stamina_multiply = int(request.args.get('stamina_multiply', 1)) d = request.args.get
fragment_multiply = int(request.args.get('fragment_multiply', 100))
prog_boost_multiply = int(request.args.get('prog_boost_multiply', 0)) stamina_multiply = int(d('stamina_multiply', 1))
beyond_boost_gauge_use = int(request.args.get('beyond_boost_gauge_use', 0)) 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: 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( x.set_play_state_for_world(
stamina_multiply, fragment_multiply, prog_boost_multiply, beyond_boost_gauge_use) 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
@@ -97,6 +115,8 @@ def song_score_post(user_id):
x.submission_hash = request.form['submission_hash'] x.submission_hash = request.form['submission_hash']
if 'combo_interval_bonus' in request.form: if 'combo_interval_bonus' in request.form:
x.combo_interval_bonus = int(request.form['combo_interval_bonus']) 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

@@ -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', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight'] '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))