diff --git a/README.md b/README.md index c0362c5..22185ea 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ This procedure is mainly used for study and research, and shall not be used for ## 特性 Features -:x: : 不支持 Not supported -:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official -:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored +: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 @@ -86,14 +86,32 @@ It is just so interesting. What it can do is under exploration. > 其它小改动请参考各个 commit 信息 > Please refer to the commit messages for other minor changes. -### Version 2.11.2 - -- 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6 -- 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**. -- 新增用户潜力值每日记录功能 Add support for recording users' potential each day. -- 修复搭档 **光 & 对立(Reunion)** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100) -- 添加 `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) -- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records. +### Version 2.11.3 + +> 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 diff --git a/latest version/api/__init__.py b/latest version/api/__init__.py index 7be2d23..f458152 100644 --- a/latest version/api/__init__.py +++ b/latest version/api/__init__.py @@ -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) diff --git a/latest version/api/multiplay.py b/latest version/api/multiplay.py new file mode 100644 index 0000000..d1182ed --- /dev/null +++ b/latest version/api/multiplay.py @@ -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) diff --git a/latest version/api/users.py b/latest version/api/users.py index 7dce8f5..c304290 100644 --- a/latest version/api/users.py +++ b/latest version/api/users.py @@ -93,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 diff --git a/latest version/core/character.py b/latest version/core/character.py index f15d554..72f3e22 100644 --- a/latest version/core/character.py +++ b/latest version/core/character.py @@ -217,6 +217,8 @@ def __init__(self, c, character_id=None, user=None) -> None: 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 @@ def skill_id_displayed(self) -> str: 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 @@ def select_character_info(self, user=None): 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 @@ def to_dict(self) -> dict: 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): @@ -409,6 +424,12 @@ def upgrade_by_core(self, user=None, core=None): 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: ''' diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 4588b41..1e1dbb4 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -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 diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 13e1513..7726cc2 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.2' +ARCAEA_SERVER_VERSION = 'v2.11.2.7' 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, 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 @@ -51,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 = [ diff --git a/latest version/core/download.py b/latest version/core/download.py index 2718fc9..cc358d6 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -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 @@ def is_available_file(song_id: str, file_name: str) -> bool: # 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 @@ def parse_one(self, song: dict) -> dict: 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 @@ def clear_all_cache() -> None: 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 @@ def add_one_song(self, song_id: str) -> None: 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 @@ def add_one_song(self, song_id: str) -> None: else: re['additional_files'].append( {"checksum": x.hash, 'file_name': i}) + # 有参数 requirement 作用未知 else: if 'chart' not in re: re['chart'] = {} @@ -277,9 +337,19 @@ def add_songs(self, song_ids: list = None) -> None: 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) diff --git a/latest version/core/item.py b/latest version/core/item.py index e6d2c60..2b83413 100644 --- a/latest version/core/item.py +++ b/latest version/core/item.py @@ -195,7 +195,7 @@ def user_claim_item(self, user): '''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): diff --git a/latest version/core/linkplay.py b/latest version/core/linkplay.py index a0a549f..29bccf2 100644 --- a/latest version/core/linkplay.py +++ b/latest version/core/linkplay.py @@ -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 @@ def to_dict(self) -> dict: 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 @@ def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None: self.room = room self.user.select_user_one_column('name') - self.data_swap( - (2, self.user.name, b64encode(self.user.song_unlock).decode('utf-8'), room.room_code)) - self.room.room_code = self.data_recv[1] - self.room.room_id = int(self.data_recv[2]) - self.room.song_unlock = b64decode(self.data_recv[6]) - self.user.token = int(self.data_recv[3]) - self.user.key = b64decode(self.data_recv[4]) - self.user.player_id = int(self.data_recv[5]) + 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'] diff --git a/latest version/core/save.py b/latest version/core/save.py index 16cd7d4..640ff82 100644 --- a/latest version/core/save.py +++ b/latest version/core/save.py @@ -96,13 +96,16 @@ def select_all(self, user) -> None: 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 diff --git a/latest version/core/score.py b/latest version/core/score.py index f3d9155..5174b62 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -223,6 +223,9 @@ def __init__(self, c=None, user=None) -> None: self.course_play: 'CoursePlay' = None self.combo_interval_bonus: int = None # 不能给 None 以外的默认值 + self.skill_cytusii_flag: str = None + self.highest_health: int = None + self.lowest_health: int = None def to_dict(self) -> dict: # 不能super @@ -300,15 +303,17 @@ def get_play_state(self) -> None: 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}) @@ -320,8 +325,8 @@ def set_play_state_for_world(self, stamina_multiply: int = 1, fragment_multiply: 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() @@ -353,8 +358,8 @@ def set_play_state_for_course(self, use_course_skip_purchase: bool, course_id: s 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) diff --git a/latest version/core/sql.py b/latest version/core/sql.py index f39ed4f..a19088b 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -387,7 +387,7 @@ def update_user_char_full(c) -> None: 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: diff --git a/latest version/core/user.py b/latest version/core/user.py index 15f17b9..dc135e3 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -127,9 +127,9 @@ def _build_user_id(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.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 @@ def _insert_user_char(self): 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): diff --git a/latest version/core/util.py b/latest version/core/util.py index 0236906..24c0021 100644 --- a/latest version/core/util.py +++ b/latest version/core/util.py @@ -1,9 +1,30 @@ 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: # md5加密算法 code = code.encode() diff --git a/latest version/core/world.py b/latest version/core/world.py index 6f19eac..4110116 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -500,10 +500,13 @@ def to_dict(self) -> dict: 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 @@ def update(self) -> None: 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 @@ def _eto_uncap(self) -> None: 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 @@ def _luna_uncap(self) -> None: 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 @@ def _ayu_uncap(self) -> None: 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 @@ def _skill_amane(self) -> None: ''' 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 @@ def _skill_mika(self) -> None: 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() diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index fcb2d87..d2300ac 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -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', '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', - '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', - '', '', '', '', '', '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, 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, 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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 = { 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}], 10: [{'core_id': 'core_umbral', 'amount': 30}], 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', - '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', '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", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index 25513f3..c5bf078 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -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 } ] \ No newline at end of file diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 62e8ed1..0f9a18b 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1468,5 +1468,95 @@ ], "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 } ] \ No newline at end of file diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 79cb7de..5e8d8f2 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -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, diff --git a/latest version/database/songs/songlist b/latest version/database/songs/songlist index 91d9c8b..c9d770c 100644 --- a/latest version/database/songs/songlist +++ b/latest version/database/songs/songlist @@ -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 + } + ] } - ] - } - ] + ] } \ No newline at end of file diff --git a/latest version/linkplay_server/config.py b/latest version/linkplay_server/config.py index 856a5e3..de43994 100644 --- a/latest version/linkplay_server/config.py +++ b/latest version/linkplay_server/config.py @@ -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 diff --git a/latest version/linkplay_server/main.py b/latest version/linkplay_server/main.py index defbaf2..1963f05 100644 --- a/latest version/linkplay_server/main.py +++ b/latest version/linkplay_server/main.py @@ -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 @@ 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 @@ def handle(self): ciphertext, self.client_address) +AUTH_LEN = len(Config.AUTHENTICATION) +TCP_AES_KEY = Config.TCP_SECRET_KEY.encode('utf-8').ljust(16, b'\x00')[:16] + + class TCP_handler(socketserver.StreamRequestHandler): + def handle(self): - self.data = self.rfile.readline().strip() + 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 - 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') + 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): diff --git a/latest version/linkplay_server/store.py b/latest version/linkplay_server/store.py index 0edd26d..fdb7b71 100644 --- a/latest version/linkplay_server/store.py +++ b/latest version/linkplay_server/store.py @@ -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 @@ def clean_check(): 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 @@ def generate_room() -> Room: 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 @@ def create_room(self) -> tuple: } 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) - - def join_room(self) -> tuple: + return { + 'room_code': room.room_code, + 'room_id': room.room_id, + 'token': token, + 'key': b64encode(key).decode('utf-8'), + 'player_id': player.player_id + } + + def join_room(self) -> 'dict | int': # 入房 # data = ['2', name, song_unlock, room_code] # song_unlock: base64 str - room_code = self.data[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 @@ def join_room(self) -> tuple: } 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')) - - def update_room(self) -> tuple: + return { + 'room_code': room_code, + 'room_id': room.room_id, + 'token': token, + 'key': b64encode(key).decode('utf-8'), + 'player_id': player.player_id, + 'song_unlock': b64encode(room.song_unlock).decode('utf-8') + } + + def update_room(self) -> dict: # 房间信息更新 # data = ['3', token] - token = int(self.data[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 + } diff --git a/latest version/linkplay_server/udp_class.py b/latest version/linkplay_server/udp_class.py index af27092..617906b 100644 --- a/latest version/linkplay_server/udp_class.py +++ b/latest version/linkplay_server/udp_class.py @@ -1,3 +1,4 @@ +import logging from time import time from .config import Config @@ -44,6 +45,31 @@ def __init__(self) -> None: 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 @@ def __init__(self) -> None: 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 @@ def make_round(self): for i in range(4): if self.players[i].player_id == self.host_id: for j in range(1, 4): - if self.players[(i + j) % 4].player_id != 0: - self.host_id = self.players[(i + j) % 4].player_id + 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 @@ def make_finish(self): 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}') diff --git a/latest version/linkplay_server/udp_parser.py b/latest version/linkplay_server/udp_parser.py index 90e0061..18f9b21 100644 --- a/latest version/linkplay_server/udp_parser.py +++ b/latest version/linkplay_server/udp_parser.py @@ -1,3 +1,4 @@ +import logging import time from .udp_class import Room, bi @@ -44,6 +45,8 @@ def command_01(self): 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 @@ def command_09(self): # 将换房主时间提前到此刻 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 diff --git a/latest version/main.py b/latest version/main.py index 19437b9..bea401a 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -26,6 +26,7 @@ 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.index.bp) app.register_blueprint(api.bp) app.register_blueprint(server.bp) +# app.register_blueprint(webapi.bp) @app.route('/') diff --git a/latest version/server/others.py b/latest version/server/others.py index 8b4cd14..9063770 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -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) - - x.add_songs() - return success_return(x.urls) + 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) @bp.route('/finale/progress', methods=['GET']) diff --git a/latest version/server/score.py b/latest version/server/score.py index 5c6bf5b..5acf494 100644 --- a/latest version/server/score.py +++ b/latest version/server/score.py @@ -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() diff --git a/latest version/web/index.py b/latest version/web/index.py index 8797033..a1ba21c 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -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', '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) diff --git a/latest version/web/system.py b/latest version/web/system.py index 8d5c4a2..7fc6b3e 100644 --- a/latest version/web/system.py +++ b/latest version/web/system.py @@ -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))