mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2025-12-14 08:06:23 +08:00
[Enhance] Link Play 2.0 e.t.c.
- For Arcaea 5.10.1(c) - Add support for Link Play 2.0. - New partners "Luna & Ilot" and "Eto & Hoppe" - Add support for the skill of "Eto & Hoppe". - Add support for refreshing ratings of Recent 30 via API and webpage. Note: This is a bug testing version.
This commit is contained in:
@@ -12,7 +12,7 @@ class Config:
|
|||||||
|
|
||||||
SONG_FILE_HASH_PRE_CALCULATE = True
|
SONG_FILE_HASH_PRE_CALCULATE = True
|
||||||
|
|
||||||
GAME_API_PREFIX = '/geriraraiu/31' # str | list[str]
|
GAME_API_PREFIX = '/pastatabetai/32' # str | list[str]
|
||||||
OLD_GAME_API_PREFIX = [] # str | list[str]
|
OLD_GAME_API_PREFIX = [] # str | list[str]
|
||||||
|
|
||||||
ALLOW_APPVERSION = [] # list[str]
|
ALLOW_APPVERSION = [] # list[str]
|
||||||
@@ -102,6 +102,10 @@ class Config:
|
|||||||
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
API_LOGIN_RATE_LIMIT = '10/5 minutes'
|
||||||
|
|
||||||
|
|
||||||
|
NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from .config_manager import Config
|
from .config_manager import Config
|
||||||
|
|
||||||
ARCAEA_SERVER_VERSION = 'v2.11.3.16'
|
ARCAEA_SERVER_VERSION = 'v2.11.3.17'
|
||||||
ARCAEA_DATABASE_VERSION = 'v2.11.3.16'
|
ARCAEA_DATABASE_VERSION = 'v2.11.3.17'
|
||||||
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
|
||||||
|
|
||||||
|
|
||||||
@@ -66,6 +66,13 @@ class Constant:
|
|||||||
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
|
LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY
|
||||||
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4 # Units: seconds
|
||||||
|
LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000]
|
||||||
|
LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1]
|
||||||
|
LINKPLAY_MATCH_TIMEOUT = 15 # Units: seconds
|
||||||
|
LINKPLAY_MATCH_MEMORY_CLEAN_INTERVAL = 60 # Units: seconds
|
||||||
|
|
||||||
# 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 = [
|
||||||
(0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B),
|
(0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import socket
|
import socket
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from json import dumps, loads
|
from json import dumps, loads
|
||||||
|
from threading import RLock
|
||||||
|
from time import time
|
||||||
|
|
||||||
from core.error import ArcError, Timeout
|
from core.error import ArcError, Timeout
|
||||||
|
|
||||||
@@ -36,6 +38,10 @@ class Player(UserInfo):
|
|||||||
self.__song_unlock: bytes = None
|
self.__song_unlock: bytes = None
|
||||||
self.client_song_map: dict = None
|
self.client_song_map: dict = None
|
||||||
|
|
||||||
|
self.last_match_timestamp: int = 0
|
||||||
|
self.match_times: int = None # 已匹配次数,减 1 后乘 5 就大致是匹配时间
|
||||||
|
self.match_room: Room = None # 匹配到的房间,这个仅用来在两个人同时匹配时使用,一人建房,通知另一个人加入
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'userId': self.user_id,
|
'userId': self.user_id,
|
||||||
@@ -55,6 +61,16 @@ class Player(UserInfo):
|
|||||||
self.client_song_map = client_song_map
|
self.client_song_map = client_song_map
|
||||||
self.__song_unlock = get_song_unlock(self.client_song_map)
|
self.__song_unlock = get_song_unlock(self.client_song_map)
|
||||||
|
|
||||||
|
def calc_available_chart_num(self, song_unlock: bytes) -> int:
|
||||||
|
'''计算交叠后可用谱面数量'''
|
||||||
|
new_unlock = [i & j for i, j in zip(self.song_unlock, song_unlock)]
|
||||||
|
s = 0
|
||||||
|
for i in range(len(new_unlock)):
|
||||||
|
for j in range(8):
|
||||||
|
if new_unlock[i] & (1 << j):
|
||||||
|
s += 1
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@@ -63,11 +79,14 @@ class Room:
|
|||||||
|
|
||||||
self.song_unlock: bytes = None
|
self.song_unlock: bytes = None
|
||||||
|
|
||||||
|
self.share_token: str = 'abcde12345'
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'roomId': str(self.room_id),
|
'roomId': str(self.room_id),
|
||||||
'roomCode': self.room_code,
|
'roomCode': self.room_code,
|
||||||
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode()
|
'orderedAllowedSongs': (b64encode(self.song_unlock)).decode(),
|
||||||
|
'shareToken': self.share_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -97,7 +116,7 @@ class RemoteMultiPlayer:
|
|||||||
raise ArcError(
|
raise ArcError(
|
||||||
'Too long body from link play server', status=400)
|
'Too long body from link play server', status=400)
|
||||||
iv = sock.recv(12)
|
iv = sock.recv(12)
|
||||||
tag = sock.recv(12)
|
tag = sock.recv(16)
|
||||||
ciphertext = sock.recv(cipher_len)
|
ciphertext = sock.recv(cipher_len)
|
||||||
received = aes_gcm_128_decrypt(
|
received = aes_gcm_128_decrypt(
|
||||||
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
|
RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||||
@@ -112,7 +131,7 @@ class RemoteMultiPlayer:
|
|||||||
iv, ciphertext, tag = aes_gcm_128_encrypt(
|
iv, ciphertext, tag = aes_gcm_128_encrypt(
|
||||||
self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
|
self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'')
|
||||||
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
|
send_data = Constant.LINKPLAY_AUTHENTICATION.encode(
|
||||||
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag[:12] + ciphertext
|
'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag + ciphertext
|
||||||
recv_data = self.tcp(send_data)
|
recv_data = self.tcp(send_data)
|
||||||
self.data_recv = loads(recv_data)
|
self.data_recv = loads(recv_data)
|
||||||
|
|
||||||
@@ -126,12 +145,15 @@ class RemoteMultiPlayer:
|
|||||||
'''创建房间'''
|
'''创建房间'''
|
||||||
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_about_link_play()
|
||||||
self.data_swap({
|
self.data_swap({
|
||||||
'endpoint': 'create_room',
|
'endpoint': 'create_room',
|
||||||
'data': {
|
'data': {
|
||||||
'name': self.user.name,
|
'name': self.user.name,
|
||||||
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8')
|
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||||
|
'rating_ptt': self.user.rating_ptt,
|
||||||
|
'is_hide_rating': self.user.is_hide_rating,
|
||||||
|
'match_times': self.user.match_times
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -151,13 +173,16 @@ class RemoteMultiPlayer:
|
|||||||
if room is not None:
|
if room is not None:
|
||||||
self.room = room
|
self.room = room
|
||||||
|
|
||||||
self.user.select_user_one_column('name')
|
self.user.select_user_about_link_play()
|
||||||
self.data_swap({
|
self.data_swap({
|
||||||
'endpoint': 'join_room',
|
'endpoint': 'join_room',
|
||||||
'data': {
|
'data': {
|
||||||
'name': self.user.name,
|
'name': self.user.name,
|
||||||
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'),
|
||||||
'room_code': self.room.room_code
|
'room_code': self.room.room_code,
|
||||||
|
'rating_ptt': self.user.rating_ptt,
|
||||||
|
'is_hide_rating': self.user.is_hide_rating,
|
||||||
|
'match_times': self.user.match_times
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
x = self.data_recv['data']
|
x = self.data_recv['data']
|
||||||
@@ -172,10 +197,14 @@ class RemoteMultiPlayer:
|
|||||||
'''更新房间'''
|
'''更新房间'''
|
||||||
if user is not None:
|
if user is not None:
|
||||||
self.user = user
|
self.user = user
|
||||||
|
|
||||||
|
self.user.select_user_about_link_play()
|
||||||
self.data_swap({
|
self.data_swap({
|
||||||
'endpoint': 'update_room',
|
'endpoint': 'update_room',
|
||||||
'data': {
|
'data': {
|
||||||
'token': self.user.token
|
'token': self.user.token,
|
||||||
|
'rating_ptt': self.user.rating_ptt,
|
||||||
|
'is_hide_rating': self.user.is_hide_rating
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,3 +227,128 @@ class RemoteMultiPlayer:
|
|||||||
})
|
})
|
||||||
|
|
||||||
return self.data_recv['data']
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
def select_room(self, room_code: str = None, share_token: str = None) -> dict:
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'select_room',
|
||||||
|
'data': {
|
||||||
|
'room_code': room_code,
|
||||||
|
'share_token': share_token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
def get_match_rooms(self) -> dict:
|
||||||
|
'''获取一定数量的公共房间列表'''
|
||||||
|
self.data_swap({
|
||||||
|
'endpoint': 'get_match_rooms',
|
||||||
|
'data': {
|
||||||
|
'limit': 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.data_recv['data']
|
||||||
|
|
||||||
|
|
||||||
|
class MatchStore:
|
||||||
|
|
||||||
|
last_get_rooms_timestamp = 0
|
||||||
|
room_cache: 'list[Room]' = []
|
||||||
|
|
||||||
|
player_queue: 'dict[int, Player]' = {}
|
||||||
|
|
||||||
|
lock = RLock()
|
||||||
|
|
||||||
|
last_memory_clean_timestamp = 0
|
||||||
|
|
||||||
|
def __init__(self, c=None) -> None:
|
||||||
|
self.c = c
|
||||||
|
self.remote = RemoteMultiPlayer()
|
||||||
|
|
||||||
|
def refresh_rooms(self):
|
||||||
|
now = time()
|
||||||
|
if now - self.last_get_rooms_timestamp < Constant.LINKPLAY_MATCH_GET_ROOMS_INTERVAL:
|
||||||
|
return
|
||||||
|
MatchStore.room_cache = self.remote.get_match_rooms()['rooms']
|
||||||
|
MatchStore.last_get_rooms_timestamp = now
|
||||||
|
|
||||||
|
def init_player(self, user: 'Player'):
|
||||||
|
user.match_times = 0
|
||||||
|
MatchStore.player_queue[user.user_id] = user
|
||||||
|
user.last_match_timestamp = time()
|
||||||
|
user.c = self.c
|
||||||
|
user.select_user_about_link_play()
|
||||||
|
user.c = None
|
||||||
|
|
||||||
|
def clear_player(self, user_id: int):
|
||||||
|
MatchStore.player_queue.pop(user_id, None)
|
||||||
|
|
||||||
|
def clean_room_cache(self):
|
||||||
|
MatchStore.room_cache = []
|
||||||
|
MatchStore.last_get_rooms_timestamp = 0
|
||||||
|
|
||||||
|
def memory_clean(self):
|
||||||
|
now = time()
|
||||||
|
if now - self.last_memory_clean_timestamp < Constant.LINKPLAY_MEMORY_CLEAN_INTERVAL:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
for i in MatchStore.player_queue:
|
||||||
|
if now - i.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||||
|
self.clear_player(i)
|
||||||
|
|
||||||
|
def match(self, user_id: int):
|
||||||
|
user = MatchStore.player_queue.get(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise ArcError(
|
||||||
|
f'User `{user_id}` not found in match queue.', code=999)
|
||||||
|
|
||||||
|
if user.match_room is not None:
|
||||||
|
# 二人开新房,第二人加入
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.join_room(user.match_room, user)
|
||||||
|
self.clear_player(user_id)
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
self.refresh_rooms()
|
||||||
|
|
||||||
|
rule = min(user.match_times, len(Constant.LINKPLAY_MATCH_PTT_ABS) -
|
||||||
|
1, len(Constant.LINKPLAY_MATCH_UNLOCK_MIN) - 1)
|
||||||
|
ptt_abs = Constant.LINKPLAY_MATCH_PTT_ABS[rule]
|
||||||
|
unlock_min = Constant.LINKPLAY_MATCH_UNLOCK_MIN[rule]
|
||||||
|
|
||||||
|
# 加入已有房间
|
||||||
|
for i in MatchStore.room_cache:
|
||||||
|
f = True
|
||||||
|
for j in i['players']:
|
||||||
|
if j['player_id'] != 0 and abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs:
|
||||||
|
f = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0):
|
||||||
|
room = Room()
|
||||||
|
room.room_code = i['room_code']
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.join_room(room, user)
|
||||||
|
self.clean_room_cache()
|
||||||
|
self.clear_player(user_id)
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
now = time()
|
||||||
|
|
||||||
|
# 二人开新房,第一人开房
|
||||||
|
for p in MatchStore.player_queue.values():
|
||||||
|
if p.user_id == user_id or now - p.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT:
|
||||||
|
continue
|
||||||
|
new_rule = min(rule, p.match_times)
|
||||||
|
if abs(user.rating_ptt - p.rating_ptt) < Constant.LINKPLAY_MATCH_PTT_ABS[new_rule] and user.calc_available_chart_num(p.song_unlock) >= Constant.LINKPLAY_MATCH_UNLOCK_MIN[new_rule]:
|
||||||
|
user.c = self.c
|
||||||
|
self.remote.create_room(user)
|
||||||
|
self.clear_player(user_id)
|
||||||
|
p.match_room = self.remote.room
|
||||||
|
return self.remote.to_dict()
|
||||||
|
|
||||||
|
user.match_times += 1
|
||||||
|
user.last_match_timestamp = now
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
97
latest version/core/notification.py
Normal file
97
latest version/core/notification.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from .config_manager import Config
|
||||||
|
from .user import User
|
||||||
|
from .sql import Connect
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNotification:
|
||||||
|
|
||||||
|
notification_type = None
|
||||||
|
|
||||||
|
def __init__(self, c_m=None) -> None:
|
||||||
|
self.receiver = None
|
||||||
|
self.sender = None
|
||||||
|
self.timestamp = None
|
||||||
|
self.content = None
|
||||||
|
|
||||||
|
self.c_m = c_m
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
now = round(time() * 1000)
|
||||||
|
return now - self.timestamp > Config.NOTIFICATION_EXPIRE_TIME
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def insert(self):
|
||||||
|
self.c_m.execute(
|
||||||
|
'''select max(id) from notification where user_id = ?''', (self.receiver.user_id,))
|
||||||
|
x = self.c_m.fetchone()
|
||||||
|
if x is None or x[0] is None:
|
||||||
|
x = 0
|
||||||
|
else:
|
||||||
|
x = x[0] + 1
|
||||||
|
|
||||||
|
self.c_m.execute(
|
||||||
|
'''insert into notification values (?, ?, ?, ?, ?, ?, ?)''',
|
||||||
|
(self.receiver.user_id, x, self.notification_type, self.content,
|
||||||
|
self.sender.user_id, self.sender.name, self.timestamp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoomInviteNotification(BaseNotification):
|
||||||
|
|
||||||
|
notification_type = 'room_inv'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, l: list, user: User = None) -> 'RoomInviteNotification':
|
||||||
|
x = cls()
|
||||||
|
x.sender = User()
|
||||||
|
x.sender.user_id = l[2]
|
||||||
|
x.sender.name = l[3]
|
||||||
|
x.content = l[1]
|
||||||
|
x.timestamp = l[4]
|
||||||
|
x.receiver = user
|
||||||
|
return x
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_sender(cls, sender: User, receiver: User, share_token: str, c_m) -> 'RoomInviteNotification':
|
||||||
|
x = cls()
|
||||||
|
x.c_m = c_m
|
||||||
|
x.sender = sender
|
||||||
|
x.receiver = receiver
|
||||||
|
x.content = share_token
|
||||||
|
x.timestamp = round(time() * 1000)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'sender': self.sender.name,
|
||||||
|
'type': self.notification_type,
|
||||||
|
'shareToken': self.content,
|
||||||
|
'sendTs': self.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationFactory:
|
||||||
|
def __init__(self, c_m: Connect, user=None):
|
||||||
|
self.c_m = c_m
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def get_notification(self) -> 'list[BaseNotification]':
|
||||||
|
r = []
|
||||||
|
|
||||||
|
self.c_m.execute('''select type, content, sender_user_id, sender_name, timestamp from notification where user_id = ?''',
|
||||||
|
(self.user.user_id,))
|
||||||
|
for i in self.c_m.fetchall():
|
||||||
|
x = None
|
||||||
|
if i[0] == 'room_inv':
|
||||||
|
x = RoomInviteNotification.from_list(i, self.user)
|
||||||
|
|
||||||
|
if x is not None and not x.is_expired:
|
||||||
|
r.append(x)
|
||||||
|
self.c_m.execute(
|
||||||
|
'''delete from notification where user_id = ?''', (self.user.user_id,))
|
||||||
|
return r
|
||||||
@@ -37,7 +37,7 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
# 但其实还是很慢
|
# 但其实还是很慢
|
||||||
with Connect() as c:
|
with Connect() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
|
'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart''')
|
||||||
x = c.fetchall()
|
x = c.fetchall()
|
||||||
|
|
||||||
songs = [i[0] for i in x]
|
songs = [i[0] for i in x]
|
||||||
@@ -45,7 +45,7 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs)
|
||||||
|
|
||||||
for i in x:
|
for i in x:
|
||||||
for j in range(0, 4):
|
for j in range(0, 5):
|
||||||
defnum = -10 # 没在库里的全部当做定数 -10
|
defnum = -10 # 没在库里的全部当做定数 -10
|
||||||
if i[j+1] is not None and i[j+1] > 0:
|
if i[j+1] is not None and i[j+1] > 0:
|
||||||
defnum = float(i[j+1]) / 10
|
defnum = float(i[j+1]) / 10
|
||||||
@@ -66,6 +66,38 @@ class RefreshAllScoreRating(BaseOperation):
|
|||||||
Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [
|
Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [
|
||||||
'user_id', 'song_id', 'difficulty'], where_values)
|
'user_id', 'song_id', 'difficulty'], where_values)
|
||||||
|
|
||||||
|
# 更新 recent30
|
||||||
|
song_defum: 'dict[str, list[int]]' = {}
|
||||||
|
for i in x:
|
||||||
|
song_defum[i[0]] = []
|
||||||
|
for j in range(0, 5):
|
||||||
|
defnum = -10
|
||||||
|
if i[j+1] is not None and i[j+1] > 0:
|
||||||
|
defnum = float(i[j+1]) / 10
|
||||||
|
song_defum[i[0]].append(defnum)
|
||||||
|
|
||||||
|
users = c.execute('''select user_id from user''').fetchall()
|
||||||
|
for i in users:
|
||||||
|
values = []
|
||||||
|
where_values = []
|
||||||
|
user_id = i[0]
|
||||||
|
c.execute(
|
||||||
|
'''select r_index, song_id, difficulty, score from recent30 where user_id = ?''', (user_id,))
|
||||||
|
for j in c.fetchall():
|
||||||
|
if j[1] in song_defum:
|
||||||
|
defnum = song_defum[j[1]][j[2]]
|
||||||
|
else:
|
||||||
|
defnum = -10
|
||||||
|
ptt = Score.calculate_rating(defnum, j[3])
|
||||||
|
ptt = max(ptt, 0)
|
||||||
|
|
||||||
|
values.append((ptt,))
|
||||||
|
where_values.append((user_id, j[0]))
|
||||||
|
|
||||||
|
if values:
|
||||||
|
Sql(c).update_many('recent30', ['rating'], values, [
|
||||||
|
'user_id', 'r_index'], where_values)
|
||||||
|
|
||||||
|
|
||||||
class RefreshSongFileCache(BaseOperation):
|
class RefreshSongFileCache(BaseOperation):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -510,6 +510,15 @@ class MemoryDatabase:
|
|||||||
file_path text, time int, device_id text);''')
|
file_path text, time int, device_id text);''')
|
||||||
self.c.execute(
|
self.c.execute(
|
||||||
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
'''create index if not exists download_token_1 on download_token (song_id, file_name);''')
|
||||||
|
self.c.execute('''
|
||||||
|
create table if not exists notification(
|
||||||
|
user_id int, id int,
|
||||||
|
type text, content text,
|
||||||
|
sender_user_id int, sender_name text,
|
||||||
|
timestamp int,
|
||||||
|
primary key(user_id, id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -647,6 +647,20 @@ class UserInfo(User):
|
|||||||
self.beyond_boost_gauge = x[8] if x[8] else 0
|
self.beyond_boost_gauge = x[8] if x[8] else 0
|
||||||
self.kanae_stored_prog = x[9] if x[9] else 0
|
self.kanae_stored_prog = x[9] if x[9] else 0
|
||||||
|
|
||||||
|
def select_user_about_link_play(self) -> None:
|
||||||
|
'''
|
||||||
|
查询 user 表有关 link play 的信息
|
||||||
|
'''
|
||||||
|
self.c.execute(
|
||||||
|
'''select name, rating_ptt, is_hide_rating from user where user_id=?''', (self.user_id,))
|
||||||
|
x = self.c.fetchone()
|
||||||
|
if not x:
|
||||||
|
raise NoData('No user.', 108, -3)
|
||||||
|
|
||||||
|
self.name = x[0]
|
||||||
|
self.rating_ptt = x[1]
|
||||||
|
self.is_hide_rating = x[2] == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def global_rank(self) -> int:
|
def global_rank(self) -> int:
|
||||||
'''用户世界排名,如果超过设定最大值,返回0'''
|
'''用户世界排名,如果超过设定最大值,返回0'''
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def aes_gcm_128_encrypt(key, plaintext, associated_data):
|
|||||||
iv = os.urandom(12)
|
iv = os.urandom(12)
|
||||||
encryptor = Cipher(
|
encryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, min_tag_length=12),
|
modes.GCM(iv, min_tag_length=16),
|
||||||
).encryptor()
|
).encryptor()
|
||||||
encryptor.authenticate_additional_data(associated_data)
|
encryptor.authenticate_additional_data(associated_data)
|
||||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||||
@@ -20,7 +20,7 @@ def aes_gcm_128_encrypt(key, plaintext, associated_data):
|
|||||||
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
|
def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag):
|
||||||
decryptor = Cipher(
|
decryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, tag, min_tag_length=12),
|
modes.GCM(iv, tag, min_tag_length=16),
|
||||||
).decryptor()
|
).decryptor()
|
||||||
decryptor.authenticate_additional_data(associated_data)
|
decryptor.authenticate_additional_data(associated_data)
|
||||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
|||||||
@@ -490,7 +490,8 @@ class WorldSkillMixin:
|
|||||||
'skill_amane': self._skill_amane,
|
'skill_amane': self._skill_amane,
|
||||||
'skill_maya': self._skill_maya,
|
'skill_maya': self._skill_maya,
|
||||||
'luna_uncap': self._luna_uncap,
|
'luna_uncap': self._luna_uncap,
|
||||||
'skill_kanae_uncap': self._skill_kanae_uncap
|
'skill_kanae_uncap': self._skill_kanae_uncap,
|
||||||
|
'skill_eto_hoppe': self._skill_eto_hoppe,
|
||||||
}
|
}
|
||||||
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]()
|
||||||
@@ -633,6 +634,14 @@ class WorldSkillMixin:
|
|||||||
self.kanae_stored_progress = self.progress_normalized
|
self.kanae_stored_progress = self.progress_normalized
|
||||||
self.user.current_map.reclimb(self.final_progress)
|
self.user.current_map.reclimb(self.final_progress)
|
||||||
|
|
||||||
|
def _skill_eto_hoppe(self) -> None:
|
||||||
|
'''
|
||||||
|
eto_hoppe 技能,体力大于等于 6 格时,世界进度翻倍
|
||||||
|
'''
|
||||||
|
if self.user.stamina.stamina >= 6:
|
||||||
|
self.character_bonus_progress_normalized = self.progress_normalized
|
||||||
|
self.user.current_map.reclimb(self.final_progress)
|
||||||
|
|
||||||
|
|
||||||
class BaseWorldPlay(WorldSkillMixin):
|
class BaseWorldPlay(WorldSkillMixin):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -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)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan']
|
'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', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe']
|
||||||
|
|
||||||
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', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan']
|
'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_aichan', 'skill_luna_ilot', 'skill_eto_hoppe']
|
||||||
|
|
||||||
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', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', '']
|
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '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, 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, 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, 33, 51, 27, 50, 60, 45, 50]
|
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, 45, 50, 38, 22]
|
||||||
|
|
||||||
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, 33, 58, 31, 50, 50, 45, 41]
|
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, 45, 41, 12, 31]
|
||||||
|
|
||||||
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, 33, 58, 31, 50, 34, 45, 41]
|
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, 45, 41, 12, 19]
|
||||||
|
|
||||||
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, 58, 51, 40, 50, 70, 50, 61.6]
|
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, 50, 61.6, 48, 37]
|
||||||
|
|
||||||
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, 58, 96, 47, 50, 80, 67, 41]
|
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, 67, 41, 55, 50]
|
||||||
|
|
||||||
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, 59.5, 58, 96, 47, 50, 54, 90, 41]
|
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, 90, 41, 34, 30]
|
||||||
|
|
||||||
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, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 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, 50, 61.6]
|
65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 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, 50, 61.6, 48, 37]
|
||||||
|
|
||||||
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, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 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, 67, 41]
|
80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 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, 67, 41, 55, 50]
|
||||||
|
|
||||||
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, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 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, 90, 41]
|
56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 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, 90, 41, 34, 30]
|
||||||
|
|
||||||
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, 2, 0, 0, 2, 0, 0]
|
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0, 0, 0]
|
||||||
|
|
||||||
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}],
|
||||||
@@ -71,7 +71,7 @@ class InitData:
|
|||||||
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene']
|
'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene']
|
||||||
|
|
||||||
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', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman']
|
"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', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy']
|
||||||
|
|
||||||
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
world_unlocks = ["scenery_chap1", "scenery_chap2",
|
||||||
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"]
|
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"]
|
||||||
|
|||||||
@@ -844,5 +844,23 @@
|
|||||||
],
|
],
|
||||||
"orig_price": 500,
|
"orig_price": 500,
|
||||||
"price": 500
|
"price": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rotaeno",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pack",
|
||||||
|
"id": "rotaeno",
|
||||||
|
"is_available": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"amount": 5,
|
||||||
|
"id": "core_generic",
|
||||||
|
"is_available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig_price": 500,
|
||||||
|
"price": 500
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -8,7 +8,7 @@ def encrypt(key, plaintext, associated_data):
|
|||||||
iv = urandom(12)
|
iv = urandom(12)
|
||||||
encryptor = Cipher(
|
encryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, min_tag_length=12),
|
modes.GCM(iv, min_tag_length=16),
|
||||||
).encryptor()
|
).encryptor()
|
||||||
encryptor.authenticate_additional_data(associated_data)
|
encryptor.authenticate_additional_data(associated_data)
|
||||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||||
@@ -18,7 +18,7 @@ def encrypt(key, plaintext, associated_data):
|
|||||||
def decrypt(key, associated_data, iv, ciphertext, tag):
|
def decrypt(key, associated_data, iv, ciphertext, tag):
|
||||||
decryptor = Cipher(
|
decryptor = Cipher(
|
||||||
algorithms.AES(key),
|
algorithms.AES(key),
|
||||||
modes.GCM(iv, tag, min_tag_length=12),
|
modes.GCM(iv, tag, min_tag_length=16),
|
||||||
).decryptor()
|
).decryptor()
|
||||||
decryptor.authenticate_additional_data(associated_data)
|
decryptor.authenticate_additional_data(associated_data)
|
||||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||||
|
|||||||
@@ -24,9 +24,16 @@ class Config:
|
|||||||
|
|
||||||
COMMAND_INTERVAL = 1000000
|
COMMAND_INTERVAL = 1000000
|
||||||
|
|
||||||
COUNTDOWM_TIME = 3999
|
|
||||||
|
|
||||||
PLAYER_PRE_TIMEOUT = 3000000
|
PLAYER_PRE_TIMEOUT = 3000000
|
||||||
PLAYER_TIMEOUT = 20000000
|
PLAYER_TIMEOUT = 15000000
|
||||||
|
|
||||||
LINK_PLAY_UNLOCK_LENGTH = 512
|
LINK_PLAY_UNLOCK_LENGTH = 512
|
||||||
|
|
||||||
|
COUNTDOWN_SONG_READY = 4 * 1000000
|
||||||
|
COUNTDOWN_SONG_START = 6 * 1000000
|
||||||
|
|
||||||
|
# 计时模式
|
||||||
|
COUNTDOWN_MATCHING = 15 * 1000000
|
||||||
|
COUNTDOWN_SELECT_SONG = 45 * 1000000
|
||||||
|
COUNTDOWN_SELECT_DIFFICULTY = 45 * 1000000
|
||||||
|
COUNTDOWN_RESULT = 60 * 1000000
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# import binascii
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
@@ -21,11 +21,12 @@ class UDP_handler(socketserver.BaseRequestHandler):
|
|||||||
try:
|
try:
|
||||||
token = client_msg[:8]
|
token = client_msg[:8]
|
||||||
iv = client_msg[8:20]
|
iv = client_msg[8:20]
|
||||||
tag = client_msg[20:32]
|
tag = client_msg[20:36]
|
||||||
ciphertext = client_msg[32:]
|
ciphertext = client_msg[36:]
|
||||||
if bi(token) not in Store.link_play_data:
|
|
||||||
|
user = Store.link_play_data.get(bi(token))
|
||||||
|
if user is None:
|
||||||
return None
|
return None
|
||||||
user = Store.link_play_data[bi(token)]
|
|
||||||
|
|
||||||
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
plaintext = decrypt(user['key'], b'', iv, ciphertext, tag)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -52,8 +53,7 @@ class UDP_handler(socketserver.BaseRequestHandler):
|
|||||||
# logging.info(
|
# logging.info(
|
||||||
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
# f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}')
|
||||||
|
|
||||||
server.sendto(token + iv + tag[:12] +
|
server.sendto(token + iv + tag + ciphertext, self.client_address)
|
||||||
ciphertext, self.client_address)
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_LEN = len(Config.AUTHENTICATION)
|
AUTH_LEN = len(Config.AUTHENTICATION)
|
||||||
@@ -77,7 +77,7 @@ class TCP_handler(socketserver.StreamRequestHandler):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
iv = self.rfile.read(12)
|
iv = self.rfile.read(12)
|
||||||
tag = self.rfile.read(12)
|
tag = self.rfile.read(16)
|
||||||
ciphertext = self.rfile.read(cipher_len)
|
ciphertext = self.rfile.read(cipher_len)
|
||||||
|
|
||||||
self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag)
|
self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag)
|
||||||
@@ -96,8 +96,8 @@ class TCP_handler(socketserver.StreamRequestHandler):
|
|||||||
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}')
|
||||||
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
|
iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'')
|
||||||
r = len(ciphertext).to_bytes(8, byteorder='little') + \
|
r = len(ciphertext).to_bytes(
|
||||||
iv + tag[:12] + ciphertext
|
8, byteorder='little') + iv + tag + ciphertext
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ from time import time
|
|||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .udp_class import Player, Room, bi
|
from .udp_class import Player, Room, bi
|
||||||
|
from .udp_sender import CommandSender
|
||||||
|
|
||||||
|
|
||||||
class Store:
|
class Store:
|
||||||
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
# token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id}
|
||||||
link_play_data = {}
|
link_play_data = {}
|
||||||
room_id_dict = {} # 'room_id': Room
|
room_id_dict: "dict[int, Room]" = {} # 'room_id': Room
|
||||||
room_code_dict = {} # 'room_code': Room
|
room_code_dict = {} # 'room_code': Room
|
||||||
player_dict = {} # 'player_id' : Player
|
player_dict = {} # 'player_id' : Player
|
||||||
|
|
||||||
|
share_token_dict = {} # 'share_token': Room
|
||||||
|
|
||||||
lock = RLock()
|
lock = RLock()
|
||||||
|
|
||||||
|
|
||||||
@@ -28,6 +31,14 @@ def random_room_code():
|
|||||||
return re
|
return re
|
||||||
|
|
||||||
|
|
||||||
|
def random_share_token():
|
||||||
|
CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
re = ''
|
||||||
|
for _ in range(10):
|
||||||
|
re += CHARSET[randint(0, 35)]
|
||||||
|
return re
|
||||||
|
|
||||||
|
|
||||||
def unique_random(dataset, length=8, random_func=None):
|
def unique_random(dataset, length=8, random_func=None):
|
||||||
'''无重复随机,且默认非0,没处理可能的死循环'''
|
'''无重复随机,且默认非0,没处理可能的死循环'''
|
||||||
if random_func is None:
|
if random_func is None:
|
||||||
@@ -45,18 +56,27 @@ def clear_player(token):
|
|||||||
# 清除玩家信息和token
|
# 清除玩家信息和token
|
||||||
player_id = 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}`')
|
logging.info(f'Clean player `{Store.player_dict[player_id].name}`')
|
||||||
del Store.player_dict[player_id]
|
with Store.lock:
|
||||||
del Store.link_play_data[token]
|
if player_id in Store.player_dict:
|
||||||
|
del Store.player_dict[player_id]
|
||||||
|
if token in Store.link_play_data:
|
||||||
|
del Store.link_play_data[token]
|
||||||
|
|
||||||
|
|
||||||
def clear_room(room):
|
def clear_room(room):
|
||||||
# 清除房间信息
|
# 清除房间信息
|
||||||
room_id = room.room_id
|
room_id = room.room_id
|
||||||
room_code = room.room_code
|
room_code = room.room_code
|
||||||
|
share_token = room.share_token
|
||||||
logging.info(f'Clean room `{room_code}`')
|
logging.info(f'Clean room `{room_code}`')
|
||||||
del Store.room_id_dict[room_id]
|
with Store.lock:
|
||||||
del Store.room_code_dict[room_code]
|
if room_id in Store.room_id_dict:
|
||||||
del room
|
del Store.room_id_dict[room_id]
|
||||||
|
if room_code in Store.room_code_dict:
|
||||||
|
del Store.room_code_dict[room_code]
|
||||||
|
if share_token in Store.share_token_dict:
|
||||||
|
del Store.share_token_dict[share_token]
|
||||||
|
del room
|
||||||
|
|
||||||
|
|
||||||
def memory_clean(now):
|
def memory_clean(now):
|
||||||
@@ -92,6 +112,8 @@ class TCPRouter:
|
|||||||
'join_room',
|
'join_room',
|
||||||
'update_room',
|
'update_room',
|
||||||
'get_rooms',
|
'get_rooms',
|
||||||
|
'select_room',
|
||||||
|
'get_match_rooms'
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, raw_data: 'dict | list'):
|
def __init__(self, raw_data: 'dict | list'):
|
||||||
@@ -115,7 +137,7 @@ class TCPRouter:
|
|||||||
def handle(self) -> dict:
|
def handle(self) -> dict:
|
||||||
self.clean_check()
|
self.clean_check()
|
||||||
if self.endpoint not in self.router:
|
if self.endpoint not in self.router:
|
||||||
return None
|
return {'code': 999}
|
||||||
try:
|
try:
|
||||||
r = getattr(self, self.endpoint)()
|
r = getattr(self, self.endpoint)()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -144,7 +166,7 @@ class TCPRouter:
|
|||||||
room_id = unique_random(Store.room_id_dict)
|
room_id = unique_random(Store.room_id_dict)
|
||||||
room = Room()
|
room = Room()
|
||||||
room.room_id = room_id
|
room.room_id = room_id
|
||||||
room.timestamp = round(time() * 1000)
|
room.timestamp = round(time() * 1000000)
|
||||||
Store.room_id_dict[room_id] = room
|
Store.room_id_dict[room_id] = room
|
||||||
|
|
||||||
room_code = unique_random(
|
room_code = unique_random(
|
||||||
@@ -152,6 +174,11 @@ class TCPRouter:
|
|||||||
room.room_code = room_code
|
room.room_code = room_code
|
||||||
Store.room_code_dict[room_code] = room
|
Store.room_code_dict[room_code] = room
|
||||||
|
|
||||||
|
share_token = unique_random(
|
||||||
|
Store.share_token_dict, random_func=random_share_token)
|
||||||
|
room.share_token = share_token
|
||||||
|
Store.share_token_dict[share_token] = room
|
||||||
|
|
||||||
return room
|
return room
|
||||||
|
|
||||||
def create_room(self) -> dict:
|
def create_room(self) -> dict:
|
||||||
@@ -160,6 +187,9 @@ class TCPRouter:
|
|||||||
# song_unlock: base64 str
|
# song_unlock: base64 str
|
||||||
name = self.data['name']
|
name = self.data['name']
|
||||||
song_unlock = b64decode(self.data['song_unlock'])
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
match_times = self.data.get('match_times', None)
|
||||||
|
|
||||||
key = urandom(16)
|
key = urandom(16)
|
||||||
with Store.lock:
|
with Store.lock:
|
||||||
@@ -167,6 +197,9 @@ class TCPRouter:
|
|||||||
player = self.generate_player(name)
|
player = self.generate_player(name)
|
||||||
|
|
||||||
player.song_unlock = song_unlock
|
player.song_unlock = song_unlock
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
|
player.player_index = 0
|
||||||
room.song_unlock = song_unlock
|
room.song_unlock = song_unlock
|
||||||
room.host_id = player.player_id
|
room.host_id = player.player_id
|
||||||
room.players[0] = player
|
room.players[0] = player
|
||||||
@@ -174,6 +207,12 @@ class TCPRouter:
|
|||||||
token = room.room_id
|
token = room.room_id
|
||||||
player.token = token
|
player.token = token
|
||||||
|
|
||||||
|
# 匹配模式追加
|
||||||
|
if match_times is not None:
|
||||||
|
room.is_public = 1
|
||||||
|
room.round_mode = 3
|
||||||
|
room.timed_mode = 1
|
||||||
|
|
||||||
Store.link_play_data[token] = {
|
Store.link_play_data[token] = {
|
||||||
'key': key,
|
'key': key,
|
||||||
'room': room,
|
'room': room,
|
||||||
@@ -198,6 +237,9 @@ class TCPRouter:
|
|||||||
key = urandom(16)
|
key = urandom(16)
|
||||||
name = self.data['name']
|
name = self.data['name']
|
||||||
song_unlock = b64decode(self.data['song_unlock'])
|
song_unlock = b64decode(self.data['song_unlock'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
match_times = self.data.get('match_times', None)
|
||||||
|
|
||||||
with Store.lock:
|
with Store.lock:
|
||||||
if room_code not in Store.room_code_dict:
|
if room_code not in Store.room_code_dict:
|
||||||
@@ -212,7 +254,7 @@ class TCPRouter:
|
|||||||
if player_num == 0:
|
if player_num == 0:
|
||||||
# 房间不存在
|
# 房间不存在
|
||||||
return 1202
|
return 1202
|
||||||
if room.state != 2:
|
if room.state not in (0, 1, 2) or (room.is_public and match_times is None):
|
||||||
# 无法加入
|
# 无法加入
|
||||||
return 1205
|
return 1205
|
||||||
|
|
||||||
@@ -221,16 +263,18 @@ class TCPRouter:
|
|||||||
player = self.generate_player(name)
|
player = self.generate_player(name)
|
||||||
player.token = token
|
player.token = token
|
||||||
player.song_unlock = song_unlock
|
player.song_unlock = song_unlock
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
room.update_song_unlock()
|
room.update_song_unlock()
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
if room.players[i].player_id == 0:
|
if room.players[i].player_id == 0:
|
||||||
room.players[i] = player
|
room.players[i] = player
|
||||||
player_index = i
|
player.player_index = i
|
||||||
break
|
break
|
||||||
Store.link_play_data[token] = {
|
Store.link_play_data[token] = {
|
||||||
'key': key,
|
'key': key,
|
||||||
'room': room,
|
'room': room,
|
||||||
'player_index': player_index,
|
'player_index': player.player_index,
|
||||||
'player_id': player.player_id
|
'player_id': player.player_id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,11 +292,23 @@ class TCPRouter:
|
|||||||
# 房间信息更新
|
# 房间信息更新
|
||||||
# data = ['3', token]
|
# data = ['3', token]
|
||||||
token = int(self.data['token'])
|
token = int(self.data['token'])
|
||||||
|
rating_ptt = self.data.get('rating_ptt', 0)
|
||||||
|
is_hide_rating = self.data.get('is_hide_rating', False)
|
||||||
|
|
||||||
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']
|
||||||
|
|
||||||
|
# 更新玩家信息
|
||||||
|
player_index = r['player_index']
|
||||||
|
player = room.players[player_index]
|
||||||
|
player.rating_ptt = rating_ptt
|
||||||
|
player.is_hide_rating = is_hide_rating
|
||||||
|
cs = CommandSender(room)
|
||||||
|
room.command_queue.append(cs.command_12(player_index))
|
||||||
|
|
||||||
logging.info(f'TCP-Room `{room.room_code}` info update')
|
logging.info(f'TCP-Room `{room.room_code}` info update')
|
||||||
return {
|
return {
|
||||||
'room_code': room.room_code,
|
'room_code': room.room_code,
|
||||||
@@ -300,3 +356,55 @@ class TCPRouter:
|
|||||||
'has_more': f2,
|
'has_more': f2,
|
||||||
'rooms': rooms
|
'rooms': rooms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def select_room(self) -> dict:
|
||||||
|
# 查询房间信息
|
||||||
|
|
||||||
|
room_code = self.data.get('room_code', None)
|
||||||
|
share_token = self.data.get('share_token', None)
|
||||||
|
|
||||||
|
if room_code is not None:
|
||||||
|
room = Store.room_code_dict.get(room_code, None)
|
||||||
|
elif share_token is not None:
|
||||||
|
room = Store.share_token_dict.get(share_token, None)
|
||||||
|
if room is None:
|
||||||
|
return 108
|
||||||
|
|
||||||
|
return {
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'share_token': room.share_token,
|
||||||
|
'is_enterable': room.is_enterable,
|
||||||
|
'is_matchable': room.is_matchable,
|
||||||
|
'is_playing': room.is_playing,
|
||||||
|
'is_public': room.is_public == 1,
|
||||||
|
'timed_mode': room.timed_mode == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_match_rooms(self):
|
||||||
|
n = 0
|
||||||
|
rooms = []
|
||||||
|
|
||||||
|
for room in Store.room_id_dict.values():
|
||||||
|
if not room.is_matchable:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rooms.append({
|
||||||
|
'room_id': room.room_id,
|
||||||
|
'room_code': room.room_code,
|
||||||
|
'share_token': room.share_token,
|
||||||
|
'is_matchable': room.is_matchable,
|
||||||
|
'next_state_timestamp': room.next_state_timestamp,
|
||||||
|
'song_unlock': b64encode(room.song_unlock).decode('utf-8'),
|
||||||
|
'players': [{
|
||||||
|
'player_id': i.player_id,
|
||||||
|
'name': i.name,
|
||||||
|
'rating_ptt': i.rating_ptt
|
||||||
|
} for i in room.players]
|
||||||
|
})
|
||||||
|
if n >= 100:
|
||||||
|
break
|
||||||
|
return {
|
||||||
|
'amount': n,
|
||||||
|
'rooms': rooms
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from time import time
|
from time import time
|
||||||
|
from random import randint
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -12,26 +13,73 @@ def bi(value):
|
|||||||
return int.from_bytes(value, byteorder='little')
|
return int.from_bytes(value, byteorder='little')
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Score:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
self.difficulty = 0xff
|
||||||
|
|
||||||
|
self.score = 0
|
||||||
|
self.cleartype = 0
|
||||||
|
self.timer = 0
|
||||||
|
|
||||||
|
self.best_score_flag = 0 # personal best
|
||||||
|
self.best_player_flag = 0 # high score
|
||||||
|
|
||||||
|
# 5.10 新增
|
||||||
|
self.shiny_perfect_count = 0 # 2 bytes
|
||||||
|
self.perfect_count = 0 # 2 bytes
|
||||||
|
self.near_count = 0 # 2 bytes
|
||||||
|
self.miss_count = 0 # 2 bytes
|
||||||
|
self.early_count = 0 # 2 bytes
|
||||||
|
self.late_count = 0 # 2 bytes
|
||||||
|
|
||||||
|
self.healthy = 0 # 4 bytes signed? 不确定,但似乎没影响
|
||||||
|
|
||||||
|
def copy(self, x: 'Score'):
|
||||||
|
self.difficulty = x.difficulty
|
||||||
|
self.score = x.score
|
||||||
|
self.cleartype = x.cleartype
|
||||||
|
self.timer = x.timer
|
||||||
|
self.best_score_flag = x.best_score_flag
|
||||||
|
self.best_player_flag = x.best_player_flag
|
||||||
|
self.shiny_perfect_count = x.shiny_perfect_count
|
||||||
|
self.perfect_count = x.perfect_count
|
||||||
|
self.near_count = x.near_count
|
||||||
|
self.miss_count = x.miss_count
|
||||||
|
self.early_count = x.early_count
|
||||||
|
self.late_count = x.late_count
|
||||||
|
self.healthy = x.healthy
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.difficulty = 0xff
|
||||||
|
self.score = 0
|
||||||
|
self.cleartype = 0
|
||||||
|
self.timer = 0
|
||||||
|
self.best_score_flag = 0
|
||||||
|
self.best_player_flag = 0
|
||||||
|
self.shiny_perfect_count = 0
|
||||||
|
self.perfect_count = 0
|
||||||
|
self.near_count = 0
|
||||||
|
self.miss_count = 0
|
||||||
|
self.early_count = 0
|
||||||
|
self.late_count = 0
|
||||||
|
self.healthy = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Score: {self.score}, Cleartype: {self.cleartype}, Difficulty: {self.difficulty}, Timer: {self.timer}, Best Score Flag: {self.best_score_flag}, Best Player Flag: {self.best_player_flag}, Shiny Perfect: {self.shiny_perfect_count}, Perfect: {self.perfect_count}, Near: {self.near_count}, Miss: {self.miss_count}, Early: {self.early_count}, Late: {self.late_count}, Healthy: {self.healthy}'
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, player_index: int = 0) -> None:
|
||||||
self.player_id = 0
|
self.player_id = 0
|
||||||
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
||||||
self.token = 0
|
self.token = 0
|
||||||
|
|
||||||
self.character_id = 0xff
|
self.character_id = 0xff
|
||||||
self.last_character_id = 0xff
|
|
||||||
self.is_uncapped = 0
|
self.is_uncapped = 0
|
||||||
|
|
||||||
self.difficulty = 0xff
|
self.score = Score()
|
||||||
self.last_difficulty = 0xff
|
self.last_score = Score()
|
||||||
self.score = 0
|
|
||||||
self.last_score = 0
|
|
||||||
self.timer = 0
|
|
||||||
self.last_timer = 0
|
|
||||||
self.cleartype = 0
|
|
||||||
self.last_cleartype = 0
|
|
||||||
self.best_score_flag = 0
|
|
||||||
self.best_player_flag = 0
|
|
||||||
self.finish_flag = 0
|
self.finish_flag = 0
|
||||||
|
|
||||||
self.player_state = 1
|
self.player_state = 1
|
||||||
@@ -45,6 +93,16 @@ class Player:
|
|||||||
|
|
||||||
self.start_command_num = 0
|
self.start_command_num = 0
|
||||||
|
|
||||||
|
# 5.10 新增
|
||||||
|
|
||||||
|
self.voting: int = 0x8000 # 2 bytes, song_idx, 0xffff 为不选择,0x8000 为默认值
|
||||||
|
self.player_index: int = player_index # 1 byte 不确定对不对
|
||||||
|
self.switch_2: int = 0 # 1 byte
|
||||||
|
|
||||||
|
self.rating_ptt: int = 0 # 2 bytes
|
||||||
|
self.is_hide_rating: int = 0 # 1 byte
|
||||||
|
self.switch_4: int = 0 # 1 byte 只能确定有 00 和 01
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.player_name.decode('ascii').rstrip('\x00')
|
return self.player_name.decode('ascii').rstrip('\x00')
|
||||||
@@ -56,15 +114,23 @@ class Player:
|
|||||||
'is_online': self.online == 1,
|
'is_online': self.online == 1,
|
||||||
'character_id': self.character_id,
|
'character_id': self.character_id,
|
||||||
'is_uncapped': self.is_uncapped == 1,
|
'is_uncapped': self.is_uncapped == 1,
|
||||||
|
'rating_ptt': self.rating_ptt,
|
||||||
|
'is_hide_rating': self.is_hide_rating == 1,
|
||||||
'last_song': {
|
'last_song': {
|
||||||
'difficulty': self.last_difficulty,
|
'difficulty': self.last_score.difficulty,
|
||||||
'score': self.last_score,
|
'score': self.last_score.score,
|
||||||
'cleartype': self.last_cleartype,
|
'cleartype': self.last_score.cleartype,
|
||||||
|
'shine_perfect': self.last_score.shiny_perfect_count,
|
||||||
|
'perfect': self.last_score.perfect_count,
|
||||||
|
'near': self.last_score.near_count,
|
||||||
|
'miss': self.last_score.miss_count,
|
||||||
|
'early': self.last_score.early_count,
|
||||||
|
'late': self.last_score.late_count,
|
||||||
},
|
},
|
||||||
'song': {
|
'song': {
|
||||||
'difficulty': self.difficulty,
|
'difficulty': self.score.difficulty,
|
||||||
'score': self.score,
|
'score': self.score.score,
|
||||||
'cleartype': self.cleartype,
|
'cleartype': self.score.cleartype,
|
||||||
},
|
},
|
||||||
'player_state': self.player_state,
|
'player_state': self.player_state,
|
||||||
'last_timestamp': self.last_timestamp,
|
'last_timestamp': self.last_timestamp,
|
||||||
@@ -77,30 +143,92 @@ class Player:
|
|||||||
else:
|
else:
|
||||||
self.player_name += b'\x00' * (16 - len(self.player_name))
|
self.player_name += b'\x00' * (16 - len(self.player_name))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> bytes:
|
||||||
|
re = bytearray()
|
||||||
|
re.extend(b(self.player_id, 8))
|
||||||
|
re.append(self.character_id)
|
||||||
|
re.append(self.is_uncapped)
|
||||||
|
re.append(self.score.difficulty)
|
||||||
|
re.extend(b(self.score.score, 4))
|
||||||
|
re.extend(b(self.score.timer, 4))
|
||||||
|
re.append(self.score.cleartype)
|
||||||
|
re.append(self.player_state)
|
||||||
|
re.append(self.download_percent)
|
||||||
|
re.append(self.online)
|
||||||
|
|
||||||
|
re.extend(b(self.voting, 2))
|
||||||
|
re.append(self.player_index)
|
||||||
|
re.append(self.switch_2)
|
||||||
|
re.extend(b(self.rating_ptt, 2))
|
||||||
|
re.append(self.is_hide_rating)
|
||||||
|
re.append(self.switch_4)
|
||||||
|
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_score_info(self) -> bytes:
|
||||||
|
if self.player_id == 0:
|
||||||
|
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
|
x = self.last_score
|
||||||
|
re = bytearray()
|
||||||
|
re.append(self.character_id)
|
||||||
|
re.append(x.difficulty)
|
||||||
|
re.extend(b(x.score, 4))
|
||||||
|
re.append(x.cleartype)
|
||||||
|
re.append(x.best_score_flag)
|
||||||
|
re.append(x.best_player_flag)
|
||||||
|
re.extend(b(x.shiny_perfect_count, 2))
|
||||||
|
re.extend(b(x.perfect_count, 2))
|
||||||
|
re.extend(b(x.near_count, 2))
|
||||||
|
re.extend(b(x.miss_count, 2))
|
||||||
|
re.extend(b(x.early_count, 2))
|
||||||
|
re.extend(b(x.late_count, 2))
|
||||||
|
re.extend(b(x.healthy, 4))
|
||||||
|
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
|
|
||||||
class Room:
|
class Room:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.room_id = 0
|
self.room_id = 0
|
||||||
self.room_code = 'AAAA00'
|
self.room_code = 'AAAA00'
|
||||||
|
self.share_token = 'abcde12345' # 5.10 新增
|
||||||
|
|
||||||
self.countdown = 0xffffffff
|
self.countdown = 0xffffffff
|
||||||
self.timestamp = 0
|
self.timestamp = 0
|
||||||
self.state = 0
|
self._state = 0
|
||||||
self.song_idx = 0xffff
|
self.song_idx = 0xffff # 疑似 idx * 5
|
||||||
self.last_song_idx = 0xffff
|
self.last_song_idx = 0xffff # 疑似 idx * 5
|
||||||
|
|
||||||
self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
|
self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||||
|
|
||||||
self.host_id = 0
|
self.host_id = 0
|
||||||
self.players = [Player(), Player(), Player(), Player()]
|
self.players = [Player(0), Player(1), Player(2), Player(3)]
|
||||||
|
|
||||||
self.interval = 1000
|
self.interval = 1000
|
||||||
self.times = 100
|
self.times = 100 # ???
|
||||||
|
|
||||||
self.round_switch = 0
|
self.round_mode: int = 1 # 5.10 从 bool 修改为 int 1~3
|
||||||
|
self.is_public = 0 # 5.10 新增
|
||||||
|
self.timed_mode = 0 # 5.10 新增
|
||||||
|
|
||||||
|
self.selected_voter_player_id: int = 0 # 5.10 新增
|
||||||
|
|
||||||
self.command_queue = []
|
self.command_queue = []
|
||||||
|
|
||||||
|
self.next_state_timestamp = 0 # 计时模式下一个状态时间
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> int:
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, value: int):
|
||||||
|
self._state = value
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
p = [i.to_dict() for i in self.players if i.player_id != 0]
|
p = [i.to_dict() for i in self.players if i.player_id != 0]
|
||||||
for i in p:
|
for i in p:
|
||||||
@@ -108,21 +236,47 @@ class Room:
|
|||||||
return {
|
return {
|
||||||
'room_id': self.room_id,
|
'room_id': self.room_id,
|
||||||
'room_code': self.room_code,
|
'room_code': self.room_code,
|
||||||
|
'share_token': self.share_token,
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
'song_idx': self.song_idx,
|
'song_idx': self.song_idx,
|
||||||
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
|
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
|
||||||
'host_id': self.host_id,
|
'host_id': self.host_id,
|
||||||
'players': p,
|
'players': p,
|
||||||
'round_switch': self.round_switch == 1,
|
'round_mode': self.round_mode,
|
||||||
'last_timestamp': self.timestamp,
|
'last_timestamp': self.timestamp,
|
||||||
'is_enterable': self.is_enterable,
|
'is_enterable': self.is_enterable,
|
||||||
|
'is_matchable': self.is_matchable,
|
||||||
'is_playing': self.is_playing,
|
'is_playing': self.is_playing,
|
||||||
|
'is_public': self.is_public == 1,
|
||||||
|
'timed_mode': self.timed_mode == 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def room_info(self) -> bytes:
|
||||||
|
re = bytearray()
|
||||||
|
re.extend(b(self.host_id, 8))
|
||||||
|
re.append(self.state)
|
||||||
|
re.extend(b(self.countdown, 4))
|
||||||
|
re.extend(b(self.timestamp, 8))
|
||||||
|
re.extend(b(self.song_idx, 2))
|
||||||
|
re.extend(b(self.interval, 2))
|
||||||
|
re.extend(b(self.times, 7))
|
||||||
|
re.extend(self.get_player_last_score())
|
||||||
|
re.extend(b(self.last_song_idx, 2))
|
||||||
|
re.append(self.round_mode)
|
||||||
|
re.append(self.is_public)
|
||||||
|
re.append(self.timed_mode)
|
||||||
|
re.extend(b(self.selected_voter_player_id, 8))
|
||||||
|
return bytes(re)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_enterable(self) -> bool:
|
def is_enterable(self) -> bool:
|
||||||
return 0 < self.player_num < 4 and self.state == 2
|
return 0 < self.player_num < 4 and self.state == 2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_matchable(self) -> bool:
|
||||||
|
return self.is_public and 0 < self.player_num < 4 and self.state == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_playing(self) -> bool:
|
def is_playing(self) -> bool:
|
||||||
return self.state in (4, 5, 6, 7)
|
return self.state in (4, 5, 6, 7)
|
||||||
@@ -133,7 +287,9 @@ class Room:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def player_num(self) -> int:
|
def player_num(self) -> int:
|
||||||
self.check_player_online()
|
now = round(time() * 1000000)
|
||||||
|
if now - self.timestamp >= 1000000:
|
||||||
|
self.check_player_online(now)
|
||||||
return sum(i.player_id != 0 for i in self.players)
|
return sum(i.player_id != 0 for i in self.players)
|
||||||
|
|
||||||
def check_player_online(self, now: int = None):
|
def check_player_online(self, now: int = None):
|
||||||
@@ -156,29 +312,18 @@ class Room:
|
|||||||
|
|
||||||
def get_players_info(self):
|
def get_players_info(self):
|
||||||
# 获取所有玩家信息
|
# 获取所有玩家信息
|
||||||
re = b''
|
re = bytearray()
|
||||||
for i in self.players:
|
for i in self.players:
|
||||||
re += b(i.player_id, 8) + b(i.character_id) + b(i.is_uncapped) + b(i.difficulty) + b(i.score, 4) + \
|
re.extend(i.info)
|
||||||
b(i.timer, 4) + b(i.cleartype) + b(i.player_state) + \
|
re.append(0)
|
||||||
b(i.download_percent) + b(i.online) + b'\x00' + i.player_name
|
re.extend(i.player_name)
|
||||||
return re
|
return bytes(re)
|
||||||
|
|
||||||
def get_player_last_score(self):
|
def get_player_last_score(self):
|
||||||
# 获取上次曲目玩家分数,返回bytes
|
# 获取上次曲目玩家分数,返回bytes
|
||||||
if self.last_song_idx == 0xffff:
|
if self.last_song_idx == 0xffff:
|
||||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' * 4
|
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' * 4
|
||||||
re = b''
|
return b''.join(i.last_score_info for i in self.players)
|
||||||
|
|
||||||
for i in range(4):
|
|
||||||
player = self.players[i]
|
|
||||||
|
|
||||||
if player.player_id != 0:
|
|
||||||
re += b(player.last_character_id) + b(player.last_difficulty) + b(player.last_score, 4) + b(
|
|
||||||
player.last_cleartype) + b(player.best_score_flag) + b(player.best_player_flag)
|
|
||||||
else:
|
|
||||||
re += b'\xff\xff\x00\x00\x00\x00\x00\x00\x00'
|
|
||||||
|
|
||||||
return re
|
|
||||||
|
|
||||||
def make_round(self):
|
def make_round(self):
|
||||||
# 轮换房主
|
# 轮换房主
|
||||||
@@ -203,9 +348,19 @@ class Room:
|
|||||||
f'Player `{player.name}` leaves room `{self.room_code}`')
|
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(player_index)
|
||||||
self.update_song_unlock()
|
self.update_song_unlock()
|
||||||
|
|
||||||
|
if self.state in (2, 3):
|
||||||
|
self.state = 1
|
||||||
|
self.song_idx = 0xffff
|
||||||
|
self.voting_clear()
|
||||||
|
|
||||||
|
print(self.player_num)
|
||||||
|
if self.state in (1, 2) and self.timed_mode and self.player_num <= 1:
|
||||||
|
self.next_state_timestamp = 0
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
|
||||||
def update_song_unlock(self):
|
def update_song_unlock(self):
|
||||||
# 更新房间可用歌曲
|
# 更新房间可用歌曲
|
||||||
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||||
@@ -245,27 +400,110 @@ class Room:
|
|||||||
max_score_i = []
|
max_score_i = []
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
player = self.players[i]
|
player = self.players[i]
|
||||||
if player.player_id != 0:
|
if player.player_id == 0:
|
||||||
player.finish_flag = 0
|
continue
|
||||||
player.last_timer = player.timer
|
player.finish_flag = 0
|
||||||
player.last_score = player.score
|
player.last_score.copy(player.score)
|
||||||
player.last_cleartype = player.cleartype
|
player.last_score.best_player_flag = 0
|
||||||
player.last_character_id = player.character_id
|
|
||||||
player.last_difficulty = player.difficulty
|
|
||||||
player.best_player_flag = 0
|
|
||||||
|
|
||||||
if player.last_score > max_score:
|
if player.last_score.score > max_score:
|
||||||
max_score = player.last_score
|
max_score = player.last_score.score
|
||||||
max_score_i = [i]
|
max_score_i = [i]
|
||||||
elif player.last_score == max_score:
|
elif player.last_score.score == max_score:
|
||||||
max_score_i.append(i)
|
max_score_i.append(i)
|
||||||
|
|
||||||
for i in max_score_i:
|
for i in max_score_i:
|
||||||
self.players[i].best_player_flag = 1
|
self.players[i].last_score.best_player_flag = 1
|
||||||
|
|
||||||
|
self.voting_clear()
|
||||||
|
for i in self.players:
|
||||||
|
i.score.clear()
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
|
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
|
||||||
for i in self.players:
|
for i in self.players:
|
||||||
if i.player_id != 0:
|
if i.player_id != 0:
|
||||||
logging.info(
|
logging.info(f'- Player `{i.name}` - {i.last_score}')
|
||||||
f'- Player `{i.name}` - Score: {i.last_score} Cleartype: {i.last_cleartype} Difficulty: {i.last_difficulty}')
|
|
||||||
|
@property
|
||||||
|
def is_all_player_voted(self) -> bool:
|
||||||
|
# 是否所有玩家都投票
|
||||||
|
if self.state != 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for i in self.players:
|
||||||
|
if i.player_id != 0 and i.voting == 0x8000:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def random_song(self):
|
||||||
|
random_list = []
|
||||||
|
for i in range(Config.LINK_PLAY_UNLOCK_LENGTH):
|
||||||
|
for j in range(8):
|
||||||
|
if self.song_unlock[i] & (1 << j):
|
||||||
|
random_list.append(i * 8 + j)
|
||||||
|
|
||||||
|
if not random_list:
|
||||||
|
self.song_idx = 0
|
||||||
|
else:
|
||||||
|
self.song_idx = random_list[randint(0, len(random_list) - 1)]
|
||||||
|
|
||||||
|
def make_voting(self):
|
||||||
|
# 投票
|
||||||
|
self.state = 3
|
||||||
|
self.selected_voter_player_id = 0
|
||||||
|
|
||||||
|
random_list = []
|
||||||
|
random_list_player_id = []
|
||||||
|
for i in self.players:
|
||||||
|
if i.player_id == 0 or i.voting == 0xffff or i.voting == 0x8000:
|
||||||
|
continue
|
||||||
|
random_list.append(i.voting)
|
||||||
|
random_list_player_id.append(i.player_id)
|
||||||
|
|
||||||
|
if random_list:
|
||||||
|
idx = randint(0, len(random_list) - 1)
|
||||||
|
self.song_idx = random_list[idx] * 5
|
||||||
|
self.selected_voter_player_id = random_list_player_id[idx]
|
||||||
|
else:
|
||||||
|
self.random_song()
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
f'Room `{self.room_code}` votes song `{self.song_idx}`')
|
||||||
|
|
||||||
|
def voting_clear(self):
|
||||||
|
# 清除投票
|
||||||
|
self.selected_voter_player_id = 0
|
||||||
|
for i in self.players:
|
||||||
|
i.voting = 0x8000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_next_state(self) -> bool:
|
||||||
|
if not self.timed_mode and self.state not in (4, 5, 6):
|
||||||
|
self.countdown = 0xffffffff
|
||||||
|
return False
|
||||||
|
now = round(time() * 1000000)
|
||||||
|
if self.countdown == 0xffffffff:
|
||||||
|
# 还没开始计时
|
||||||
|
if self.is_public and self.state == 1:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_MATCHING
|
||||||
|
elif self.state == 2:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_SONG
|
||||||
|
elif self.state == 3:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_DIFFICULTY
|
||||||
|
elif self.state == 4:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_READY
|
||||||
|
elif self.state == 5 or self.state == 6:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_SONG_START
|
||||||
|
elif self.state == 8:
|
||||||
|
self.next_state_timestamp = now + Config.COUNTDOWN_RESULT
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不是哥们,616 你脑子怎么长的,上个版本是毫秒时间戳,新版本变成了微秒???那你这倒计时怎么还是毫秒啊!!!
|
||||||
|
self.countdown = (self.next_state_timestamp - now) // 1000
|
||||||
|
if self.countdown <= 0:
|
||||||
|
self.countdown = 0
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
from .udp_class import Room, bi
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
from .udp_class import Room, bi
|
||||||
from .udp_sender import CommandSender
|
from .udp_sender import CommandSender
|
||||||
|
|
||||||
|
|
||||||
class CommandParser:
|
class CommandParser:
|
||||||
route = [None, 'command_01', 'command_02', 'command_03', 'command_04', 'command_05',
|
|
||||||
'command_06', 'command_07', 'command_08', 'command_09', 'command_0a', 'command_0b']
|
route = {
|
||||||
|
0x01: 'command_01',
|
||||||
|
0x02: 'command_02',
|
||||||
|
0x03: 'command_03',
|
||||||
|
0x04: 'command_04',
|
||||||
|
0x06: 'command_06',
|
||||||
|
0x07: 'command_07',
|
||||||
|
0x08: 'command_08',
|
||||||
|
0x09: 'command_09',
|
||||||
|
0x0a: 'command_0a',
|
||||||
|
0x0b: 'command_0b',
|
||||||
|
0x20: 'command_20',
|
||||||
|
0x22: 'command_22',
|
||||||
|
0x23: 'command_23',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, room: Room, player_index: int = 0) -> None:
|
def __init__(self, room: Room, player_index: int = 0) -> None:
|
||||||
self.room = room
|
self.room = room
|
||||||
@@ -31,7 +44,7 @@ class CommandParser:
|
|||||||
re.append(self.room.command_queue[i])
|
re.append(self.room.command_queue[i])
|
||||||
|
|
||||||
if self.room.players[self.player_index].extra_command_queue:
|
if self.room.players[self.player_index].extra_command_queue:
|
||||||
re += self.room.players[self.player_index].extra_command_queue
|
re += self.room.players[self.player_index].extra_command_queue[-12:]
|
||||||
self.room.players[self.player_index].extra_command_queue = []
|
self.room.players[self.player_index].extra_command_queue = []
|
||||||
|
|
||||||
if r:
|
if r:
|
||||||
@@ -52,10 +65,14 @@ class CommandParser:
|
|||||||
self.room.command_queue.append(self.s.command_10())
|
self.room.command_queue.append(self.s.command_10())
|
||||||
|
|
||||||
def command_02(self):
|
def command_02(self):
|
||||||
|
# 房主选歌
|
||||||
|
if self.room.round_mode == 3:
|
||||||
|
logging.warning('Error: round_mode == 3 in command 02')
|
||||||
|
return None
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
song_idx = bi(self.command[24:26])
|
song_idx = bi(self.command[24:26])
|
||||||
|
|
||||||
flag = 2
|
flag = 5
|
||||||
if self.room.state == 2:
|
if self.room.state == 2:
|
||||||
flag = 0
|
flag = 0
|
||||||
self.room.state = 3
|
self.room.state = 3
|
||||||
@@ -69,10 +86,17 @@ class CommandParser:
|
|||||||
# 尝试进入结算
|
# 尝试进入结算
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
player = self.room.players[self.player_index]
|
player = self.room.players[self.player_index]
|
||||||
player.score = bi(self.command[24:28])
|
player.score.score = bi(self.command[24:28])
|
||||||
player.cleartype = self.command[28]
|
player.score.cleartype = self.command[28]
|
||||||
player.difficulty = self.command[29]
|
player.score.difficulty = self.command[29]
|
||||||
player.best_score_flag = self.command[30]
|
player.score.best_score_flag = self.command[30]
|
||||||
|
player.score.shiny_perfect_count = bi(self.command[31:33])
|
||||||
|
player.score.perfect_count = bi(self.command[33:35])
|
||||||
|
player.score.near_count = bi(self.command[35:37])
|
||||||
|
player.score.miss_count = bi(self.command[37:39])
|
||||||
|
player.score.early_count = bi(self.command[39:41])
|
||||||
|
player.score.late_count = bi(self.command[41:43])
|
||||||
|
player.score.healthy = bi(self.command[43:47])
|
||||||
player.finish_flag = 1
|
player.finish_flag = 1
|
||||||
player.last_timestamp -= Config.COMMAND_INTERVAL
|
player.last_timestamp -= Config.COMMAND_INTERVAL
|
||||||
self.room.last_song_idx = self.room.song_idx
|
self.room.last_song_idx = self.room.song_idx
|
||||||
@@ -94,19 +118,16 @@ class CommandParser:
|
|||||||
flag = 1
|
flag = 1
|
||||||
self.room.delete_player(i)
|
self.room.delete_player(i)
|
||||||
self.room.command_queue.append(self.s.command_12(i))
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
self.room.update_song_unlock()
|
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
break
|
break
|
||||||
|
|
||||||
return [self.s.command_0d(flag)]
|
return [self.s.command_0d(flag)]
|
||||||
|
|
||||||
def command_05(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def command_06(self):
|
def command_06(self):
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
self.room.state = 1
|
self.room.state = 1
|
||||||
self.room.song_idx = 0xffff
|
self.room.song_idx = 0xffff
|
||||||
|
self.room.voting_clear()
|
||||||
|
|
||||||
self.room.command_queue.append(self.s.command_13())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
@@ -117,13 +138,17 @@ class CommandParser:
|
|||||||
|
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
|
# 07 可能需要一个 0d 响应,code = 0x0b
|
||||||
|
|
||||||
def command_08(self):
|
def command_08(self):
|
||||||
self.room.round_switch = bi(self.command[24:25])
|
# 可能弃用
|
||||||
self.s.random_code = self.command[16:24]
|
logging.warning('Command 08 is outdated')
|
||||||
self.room.command_queue.append(self.s.command_13())
|
pass
|
||||||
|
# self.room.round_mode = bi(self.command[24:25])
|
||||||
|
# self.s.random_code = self.command[16:24]
|
||||||
|
# self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
def command_09(self):
|
def command_09(self):
|
||||||
re = []
|
|
||||||
self.s.random_code = self.command[16:24]
|
self.s.random_code = self.command[16:24]
|
||||||
player = self.room.players[self.player_index]
|
player = self.room.players[self.player_index]
|
||||||
|
|
||||||
@@ -133,133 +158,166 @@ class CommandParser:
|
|||||||
self.room.update_song_unlock()
|
self.room.update_song_unlock()
|
||||||
player.start_command_num = self.room.command_queue_length
|
player.start_command_num = self.room.command_queue_length
|
||||||
self.room.command_queue.append(self.s.command_15())
|
self.room.command_queue.append(self.s.command_15())
|
||||||
else:
|
return None
|
||||||
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
|
||||||
re.append(self.s.command_0c())
|
|
||||||
player.last_timestamp = self.s.timestamp
|
|
||||||
|
|
||||||
# 离线判断
|
flag_0c = False
|
||||||
flag_13, player_index_list = self.room.check_player_online(
|
|
||||||
self.s.timestamp)
|
|
||||||
for i in player_index_list:
|
|
||||||
self.room.command_queue.append(self.s.command_12(i))
|
|
||||||
|
|
||||||
flag_11 = False
|
if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL:
|
||||||
flag_12 = False
|
flag_0c = True
|
||||||
|
player.last_timestamp = self.s.timestamp
|
||||||
|
|
||||||
if player.online == 0:
|
# 离线判断
|
||||||
flag_12 = True
|
flag_13, player_index_list = self.room.check_player_online(
|
||||||
player.online = 1
|
self.s.timestamp)
|
||||||
|
for i in player_index_list:
|
||||||
|
self.room.command_queue.append(self.s.command_12(i))
|
||||||
|
|
||||||
if self.room.is_ready(1, 1):
|
flag_11 = False
|
||||||
flag_13 = True
|
flag_12 = False
|
||||||
self.room.state = 2
|
|
||||||
|
|
||||||
if player.player_state != self.command[32]:
|
if player.online == 0:
|
||||||
flag_12 = True
|
flag_12 = True
|
||||||
player.player_state = self.command[32]
|
player.online = 1
|
||||||
|
|
||||||
if player.difficulty != self.command[33] and player.player_state != 5 and player.player_state != 6 and player.player_state != 7 and player.player_state != 8:
|
if self.room.state in (1, 2) and player.player_state == 8:
|
||||||
flag_12 = True
|
# 还在结算给踢了
|
||||||
player.difficulty = self.command[33]
|
# 冗余,为了保险
|
||||||
|
self.room.delete_player(self.player_index)
|
||||||
|
self.room.command_queue.append(
|
||||||
|
self.s.command_12(self.player_index))
|
||||||
|
self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
if player.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
if self.room.is_ready(1, 1) and ((self.room.player_num > 1 and not self.room.is_public) or (self.room.is_public and self.room.player_num == 4)):
|
||||||
flag_12 = True
|
flag_13 = True
|
||||||
player.cleartype = self.command[34]
|
self.room.state = 2
|
||||||
|
|
||||||
if player.download_percent != self.command[35]:
|
if self.room.state == 1 and self.room.is_public and self.room.player_num > 1 and self.room.should_next_state:
|
||||||
flag_12 = True
|
flag_0c = True
|
||||||
player.download_percent = self.command[35]
|
flag_13 = True
|
||||||
|
self.room.state = 2
|
||||||
|
|
||||||
if player.character_id != self.command[36]:
|
if self.room.state in (2, 3) and self.room.player_num < 2:
|
||||||
flag_12 = True
|
flag_13 = True
|
||||||
player.character_id = self.command[36]
|
self.room.state = 1
|
||||||
|
|
||||||
if player.is_uncapped != self.command[37]:
|
if self.room.state == 2 and self.room.should_next_state:
|
||||||
flag_12 = True
|
flag_0c = True
|
||||||
player.is_uncapped = self.command[37]
|
self.room.state = 3
|
||||||
|
flag_13 = True
|
||||||
|
if self.room.round_mode == 3:
|
||||||
|
self.room.make_voting()
|
||||||
|
else:
|
||||||
|
self.room.random_song()
|
||||||
|
|
||||||
if self.room.state == 3 and player.score != bi(self.command[24:28]):
|
if player.player_state != self.command[32]:
|
||||||
flag_12 = True
|
flag_12 = True
|
||||||
player.score = bi(self.command[24:28])
|
player.player_state = self.command[32]
|
||||||
|
|
||||||
if self.room.is_ready(3, 4):
|
if player.score.difficulty != self.command[33] and player.player_state not in (5, 6, 7, 8):
|
||||||
flag_13 = True
|
flag_12 = True
|
||||||
self.room.countdown = Config.COUNTDOWM_TIME
|
player.score.difficulty = self.command[33]
|
||||||
self.room.timestamp = round(time.time() * 1000)
|
|
||||||
self.room.state = 4
|
|
||||||
if self.room.round_switch == 1:
|
|
||||||
# 将换房主时间提前到此刻
|
|
||||||
self.room.make_round()
|
|
||||||
|
|
||||||
logging.info(f'Room `{self.room.room_code}` starts playing')
|
if player.score.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8:
|
||||||
|
flag_12 = True
|
||||||
|
player.score.cleartype = self.command[34]
|
||||||
|
|
||||||
if self.room.state in (4, 5, 6):
|
if player.download_percent != self.command[35]:
|
||||||
timestamp = round(time.time() * 1000)
|
flag_12 = True
|
||||||
self.room.countdown -= timestamp - self.room.timestamp
|
player.download_percent = self.command[35]
|
||||||
self.room.timestamp = timestamp
|
|
||||||
if self.room.state == 4 and self.room.countdown <= 0:
|
|
||||||
# 此处不清楚
|
|
||||||
self.room.state = 5
|
|
||||||
self.room.countdown = 5999
|
|
||||||
flag_11 = True
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.is_ready(5, 6):
|
if player.character_id != self.command[36]:
|
||||||
self.room.state = 6
|
flag_12 = True
|
||||||
flag_13 = True
|
player.character_id = self.command[36]
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.is_ready(5, 7):
|
if player.is_uncapped != self.command[37]:
|
||||||
self.room.state = 7
|
flag_12 = True
|
||||||
self.room.countdown = 0xffffffff
|
player.is_uncapped = self.command[37]
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if self.room.state == 5 and self.room.countdown <= 0:
|
if self.room.state == 3 and player.score.score != bi(self.command[24:28]):
|
||||||
print('我怎么知道这是啥')
|
flag_12 = True
|
||||||
|
player.score.score = bi(self.command[24:28])
|
||||||
|
|
||||||
if self.room.state == 6 and self.room.countdown <= 0:
|
if self.room.is_ready(3, 4) or (self.room.state == 3 and self.room.should_next_state):
|
||||||
# 此处不清楚
|
flag_13 = True
|
||||||
self.room.state = 7
|
flag_0c = True
|
||||||
self.room.countdown = 0xffffffff
|
self.room.state = 4
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0
|
if self.room.round_mode == 2:
|
||||||
|
# 将换房主时间提前到此刻
|
||||||
|
self.room.make_round()
|
||||||
|
logging.info(f'Room `{self.room.room_code}` starts playing')
|
||||||
|
|
||||||
if self.room.state in (7, 8):
|
if self.room.state == 4:
|
||||||
if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0:
|
if player.download_percent != 0xff:
|
||||||
player.last_timer = player.timer
|
# 有人没下载完把他踢了!
|
||||||
player.last_score = player.score
|
self.room.delete_player(self.player_index)
|
||||||
player.timer = bi(self.command[28:32])
|
|
||||||
player.score = bi(self.command[24:28])
|
|
||||||
|
|
||||||
if player.timer != 0 or self.room.state != 8:
|
|
||||||
for i in self.room.players:
|
|
||||||
i.extra_command_queue.append(
|
|
||||||
self.s.command_0e(self.player_index))
|
|
||||||
|
|
||||||
if self.room.is_ready(8, 1):
|
|
||||||
flag_13 = True
|
|
||||||
self.room.state = 1
|
|
||||||
self.room.song_idx = 0xffff
|
|
||||||
|
|
||||||
for i in self.room.players:
|
|
||||||
i.timer = 0
|
|
||||||
i.score = 0
|
|
||||||
|
|
||||||
if self.room.is_finish():
|
|
||||||
# 有人退房导致的结算
|
|
||||||
self.room.make_finish()
|
|
||||||
flag_13 = True
|
|
||||||
|
|
||||||
if flag_11:
|
|
||||||
self.room.command_queue.append(self.s.command_11())
|
|
||||||
if flag_12:
|
|
||||||
self.room.command_queue.append(
|
self.room.command_queue.append(
|
||||||
self.s.command_12(self.player_index))
|
self.s.command_12(self.player_index))
|
||||||
if flag_13:
|
self.room.command_queue.append(self.s.command_14())
|
||||||
self.room.command_queue.append(self.s.command_13())
|
|
||||||
|
|
||||||
return re
|
if self.room.should_next_state:
|
||||||
|
self.room.state = 5
|
||||||
|
flag_11 = True
|
||||||
|
flag_13 = True
|
||||||
|
|
||||||
|
if self.room.state == 5:
|
||||||
|
flag_13 = True
|
||||||
|
if self.room.is_ready(5, 6):
|
||||||
|
self.room.state = 6
|
||||||
|
if self.room.is_ready(5, 7):
|
||||||
|
self.room.state = 7
|
||||||
|
|
||||||
|
if self.room.state in (5, 6) and self.room.should_next_state:
|
||||||
|
# 此处不清楚
|
||||||
|
self.room.state = 7
|
||||||
|
flag_13 = True
|
||||||
|
|
||||||
|
if self.room.state in (7, 8):
|
||||||
|
player_now_timer = bi(self.command[28:32])
|
||||||
|
if player.score.timer < player_now_timer or player_now_timer == 0 and player.score.timer != 0:
|
||||||
|
player.last_score.timer = player.score.timer
|
||||||
|
player.last_score.score = player.score.score
|
||||||
|
player.score.timer = player_now_timer
|
||||||
|
player.score.score = bi(self.command[24:28])
|
||||||
|
|
||||||
|
if player.score.timer != 0 or self.room.state != 8:
|
||||||
|
for i in self.room.players:
|
||||||
|
i.extra_command_queue.append(
|
||||||
|
self.s.command_0e(self.player_index))
|
||||||
|
|
||||||
|
if self.room.is_ready(8, 1):
|
||||||
|
flag_13 = True
|
||||||
|
self.room.state = 1
|
||||||
|
self.room.song_idx = 0xffff
|
||||||
|
|
||||||
|
if self.room.state == 8 and self.room.should_next_state:
|
||||||
|
flag_0c = True
|
||||||
|
flag_13 = True
|
||||||
|
self.room.state = 1
|
||||||
|
self.room.song_idx = 0xffff
|
||||||
|
|
||||||
|
if self.room.state in (1, 2) and player.player_state == 8:
|
||||||
|
# 还在结算给踢了
|
||||||
|
self.room.delete_player(self.player_index)
|
||||||
|
self.room.command_queue.append(
|
||||||
|
self.s.command_12(self.player_index))
|
||||||
|
self.room.command_queue.append(self.s.command_14())
|
||||||
|
|
||||||
|
if self.room.is_finish():
|
||||||
|
# 有人退房导致的结算
|
||||||
|
self.room.make_finish()
|
||||||
|
flag_13 = True
|
||||||
|
|
||||||
|
if flag_11:
|
||||||
|
self.room.command_queue.append(self.s.command_11())
|
||||||
|
if flag_12:
|
||||||
|
self.room.command_queue.append(
|
||||||
|
self.s.command_12(self.player_index))
|
||||||
|
if flag_13:
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
|
if flag_0c:
|
||||||
|
return [self.s.command_0c()]
|
||||||
|
|
||||||
def command_0a(self):
|
def command_0a(self):
|
||||||
# 退出房间
|
# 退出房间
|
||||||
@@ -267,9 +325,6 @@ class CommandParser:
|
|||||||
|
|
||||||
self.room.command_queue.append(self.s.command_12(self.player_index))
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
|
|
||||||
if self.room.state in (2, 3):
|
|
||||||
self.room.state = 1
|
|
||||||
self.room.song_idx = 0xffff
|
|
||||||
# self.room.command_queue.append(self.s.command_11())
|
# self.room.command_queue.append(self.s.command_11())
|
||||||
self.room.command_queue.append(self.s.command_13())
|
self.room.command_queue.append(self.s.command_13())
|
||||||
self.room.command_queue.append(self.s.command_14())
|
self.room.command_queue.append(self.s.command_14())
|
||||||
@@ -281,3 +336,45 @@ class CommandParser:
|
|||||||
if self.player_index != i and self.room.players[i].online == 1:
|
if self.player_index != i and self.room.players[i].online == 1:
|
||||||
self.room.players[i].extra_command_queue.append(
|
self.room.players[i].extra_command_queue.append(
|
||||||
self.s.command_0f(self.player_index, song_idx))
|
self.s.command_0f(self.player_index, song_idx))
|
||||||
|
|
||||||
|
def command_20(self):
|
||||||
|
# 表情
|
||||||
|
sticker_id = bi(self.command[16:18])
|
||||||
|
for i in range(4):
|
||||||
|
if self.player_index != i and self.room.players[i].online == 1:
|
||||||
|
self.room.players[i].extra_command_queue.append(
|
||||||
|
self.s.command_21(self.player_index, sticker_id))
|
||||||
|
|
||||||
|
def command_22(self):
|
||||||
|
# 房间设置,懒得判断房主
|
||||||
|
self.s.random_code = self.command[16:24]
|
||||||
|
self.room.is_public = self.command[25]
|
||||||
|
if self.room.is_public == 0:
|
||||||
|
self.room.round_mode = self.command[24]
|
||||||
|
self.room.timed_mode = self.command[26]
|
||||||
|
else:
|
||||||
|
self.room.round_mode = 3
|
||||||
|
self.room.timed_mode = 1
|
||||||
|
self.room.state = 1
|
||||||
|
self.room.command_queue.append(self.s.command_11())
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
return [self.s.command_0d(1)]
|
||||||
|
|
||||||
|
def command_23(self):
|
||||||
|
# 歌曲投票
|
||||||
|
self.s.random_code = self.command[16:24]
|
||||||
|
if self.room.player_num < 2:
|
||||||
|
return [self.s.command_0d(6)]
|
||||||
|
if self.room.state != 2:
|
||||||
|
return [self.s.command_0d(5)]
|
||||||
|
player = self.room.players[self.player_index]
|
||||||
|
player.voting = bi(self.command[24:26])
|
||||||
|
logging.info(
|
||||||
|
f'Player `{player.name}` votes for song `{player.voting}`')
|
||||||
|
self.room.command_queue.append(self.s.command_12(self.player_index))
|
||||||
|
|
||||||
|
if self.room.is_all_player_voted:
|
||||||
|
self.room.make_voting()
|
||||||
|
self.room.command_queue.append(self.s.command_13())
|
||||||
|
|
||||||
|
return [self.s.command_0d(1)]
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
|
from os import urandom
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from .udp_class import Room, b
|
from .udp_class import Room, b
|
||||||
|
|
||||||
|
|
||||||
|
PADDING = [b(i) * i for i in range(16)] + [b'']
|
||||||
|
|
||||||
|
|
||||||
class CommandSender:
|
class CommandSender:
|
||||||
|
|
||||||
PROTOCOL_NAME = b'\x06\x16'
|
PROTOCOL_NAME = b'\x06\x16'
|
||||||
PROTOCOL_VERSION = b'\x09'
|
PROTOCOL_VERSION = b'\x0D'
|
||||||
|
|
||||||
def __init__(self, room: Room = None) -> None:
|
def __init__(self, room: Room = None) -> None:
|
||||||
self.room = room
|
self.room = room
|
||||||
self.timestamp = round(time() * 1000000)
|
self.timestamp = round(time() * 1000000)
|
||||||
|
self.room.timestamp = self.timestamp + 1
|
||||||
|
|
||||||
self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00'
|
self._random_code = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def random_code(self):
|
||||||
|
if self._random_code is None:
|
||||||
|
self._random_code = urandom(4) + b'\x00\x00\x00\x00'
|
||||||
|
return self._random_code
|
||||||
|
|
||||||
|
@random_code.setter
|
||||||
|
def random_code(self, value):
|
||||||
|
self._random_code = value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def command_encode(t: tuple):
|
def command_encode(t: tuple):
|
||||||
r = b''.join(t)
|
r = b''.join(t)
|
||||||
x = 16 - len(r) % 16
|
x = 16 - len(r) % 16
|
||||||
return r + b(x) * x
|
return r + PADDING[x]
|
||||||
|
|
||||||
def command_prefix(self, command: bytes):
|
def command_prefix(self, command: bytes):
|
||||||
length = self.room.command_queue_length
|
length = self.room.command_queue_length
|
||||||
if command >= b'\x10':
|
if b'\x10' <= command <= b'\x1f':
|
||||||
length += 1
|
length += 1
|
||||||
|
|
||||||
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4))
|
||||||
@@ -31,12 +46,18 @@ class CommandSender:
|
|||||||
return self.command_encode((*self.command_prefix(b'\x0c'), self.random_code, b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8)))
|
return self.command_encode((*self.command_prefix(b'\x0c'), self.random_code, b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8)))
|
||||||
|
|
||||||
def command_0d(self, code: int):
|
def command_0d(self, code: int):
|
||||||
|
# 3 你不是房主
|
||||||
|
# 5 有玩家目前无法开始
|
||||||
|
# 6 需要更多玩家以开始
|
||||||
|
# 7 有玩家无法游玩这首歌
|
||||||
|
|
||||||
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
|
return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code)))
|
||||||
|
|
||||||
def command_0e(self, player_index: int):
|
def command_0e(self, player_index: int):
|
||||||
# 分数广播
|
# 分数广播
|
||||||
|
# 我猜,616 写错了,首先 4 个 00 大概是分数使用了 8 bytes 转换,其次上一个分数根本就不需要哈哈哈哈哈哈!
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
return self.command_encode((*self.command_prefix(b'\x0e'), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b'\x01', b(player.last_score, 4), b(player.last_timer, 4), b(player.online)))
|
return self.command_encode((*self.command_prefix(b'\x0e'), player.info, b(player.last_score.score, 4), b'\x00' * 4, b(player.last_score.timer, 4), b'\x00' * 4))
|
||||||
|
|
||||||
def command_0f(self, player_index: int, song_idx: int):
|
def command_0f(self, player_index: int, song_idx: int):
|
||||||
# 歌曲推荐
|
# 歌曲推荐
|
||||||
@@ -52,13 +73,17 @@ class CommandSender:
|
|||||||
|
|
||||||
def command_12(self, player_index: int):
|
def command_12(self, player_index: int):
|
||||||
player = self.room.players[player_index]
|
player = self.room.players[player_index]
|
||||||
return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b(player.online)))
|
return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), player.info))
|
||||||
|
|
||||||
def command_13(self):
|
def command_13(self):
|
||||||
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
|
return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, self.room.room_info))
|
||||||
|
|
||||||
def command_14(self):
|
def command_14(self):
|
||||||
return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
|
return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock))
|
||||||
|
|
||||||
def command_15(self):
|
def command_15(self):
|
||||||
return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1)))
|
return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, self.room.room_info))
|
||||||
|
|
||||||
|
def command_21(self, player_index: int, sticker_id: int):
|
||||||
|
player = self.room.players[player_index]
|
||||||
|
return self.command_encode((*self.command_prefix(b'\x21'), b(player.player_id, 8), b(sticker_id, 2)))
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from flask import Blueprint, request
|
|||||||
|
|
||||||
from core.config_manager import Config
|
from core.config_manager import Config
|
||||||
from core.error import ArcError
|
from core.error import ArcError
|
||||||
from core.linkplay import Player, RemoteMultiPlayer, Room
|
from core.linkplay import MatchStore, Player, RemoteMultiPlayer, Room
|
||||||
|
from core.notification import RoomInviteNotification
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
|
|
||||||
from .auth import auth_required
|
from .auth import auth_required
|
||||||
@@ -68,3 +69,104 @@ def multiplayer_update(user_id):
|
|||||||
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
return success_return(r)
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/room/<room_code>/invite', methods=['POST']) # 邀请
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def room_invite(user_id, room_code):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
other_user_id = request.form.get('to', type=int)
|
||||||
|
|
||||||
|
x = RemoteMultiPlayer()
|
||||||
|
share_token = x.select_room(room_code=room_code)['share_token']
|
||||||
|
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
with Connect() as c:
|
||||||
|
sender = Player(c, user_id)
|
||||||
|
sender.select_user_about_link_play()
|
||||||
|
n = RoomInviteNotification.from_sender(
|
||||||
|
sender, Player(c, other_user_id), share_token, c_m)
|
||||||
|
n.insert()
|
||||||
|
|
||||||
|
return success_return({}) # 无返回
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/room/status', methods=['POST']) # 房间号码获取
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def room_status(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
share_token = request.form.get('shareToken', type=str)
|
||||||
|
|
||||||
|
x = RemoteMultiPlayer()
|
||||||
|
room_code = x.select_room(share_token=share_token)['room_code']
|
||||||
|
|
||||||
|
return success_return({
|
||||||
|
'roomId': room_code,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/join/', methods=['POST']) # 匹配
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_join(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
user = Player(None, user_id)
|
||||||
|
user.get_song_unlock(request.json['clientSongMap'])
|
||||||
|
|
||||||
|
x = MatchStore(c)
|
||||||
|
x.init_player(user)
|
||||||
|
r = x.match(user_id)
|
||||||
|
|
||||||
|
if r is None:
|
||||||
|
return success_return({
|
||||||
|
'userId': user_id,
|
||||||
|
'status': 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
r['endPoint'] = request.host.split(
|
||||||
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/status/', methods=['POST']) # 匹配状态,5s 一次
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_status(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
with Connect() as c:
|
||||||
|
|
||||||
|
r = MatchStore(c).match(user_id)
|
||||||
|
if r is None:
|
||||||
|
return success_return({
|
||||||
|
'userId': user_id,
|
||||||
|
'status': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
r['endPoint'] = request.host.split(
|
||||||
|
':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST
|
||||||
|
r['port'] = int(Config.LINKPLAY_UDP_PORT)
|
||||||
|
return success_return(r)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/me/matchmaking/leave/', methods=['POST']) # 退出匹配
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def matchmaking_leave(user_id):
|
||||||
|
if not Config.LINKPLAY_HOST:
|
||||||
|
raise ArcError('The link play server is unavailable.', 151, status=404)
|
||||||
|
|
||||||
|
MatchStore().clear_player(user_id)
|
||||||
|
|
||||||
|
return success_return({})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from core.bundle import BundleDownload
|
|||||||
from core.download import DownloadList
|
from core.download import DownloadList
|
||||||
from core.error import RateLimit
|
from core.error import RateLimit
|
||||||
from core.item import ItemCharacter
|
from core.item import ItemCharacter
|
||||||
|
from core.notification import NotificationFactory
|
||||||
from core.sql import Connect
|
from core.sql import Connect
|
||||||
from core.system import GameInfo
|
from core.system import GameInfo
|
||||||
from core.user import UserOnline
|
from core.user import UserOnline
|
||||||
@@ -28,6 +29,15 @@ def game_info():
|
|||||||
return success_return(GameInfo().to_dict())
|
return success_return(GameInfo().to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/notification/me', methods=['GET']) # 通知
|
||||||
|
@auth_required(request)
|
||||||
|
@arc_try
|
||||||
|
def notification_me(user_id):
|
||||||
|
with Connect(in_memory=True) as c_m:
|
||||||
|
x = NotificationFactory(c_m, UserOnline(c_m, user_id))
|
||||||
|
return success_return([i.to_dict() for i in x.get_notification()])
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/game/content_bundle', methods=['GET']) # 热更新
|
@bp.route('/game/content_bundle', methods=['GET']) # 热更新
|
||||||
@arc_try
|
@arc_try
|
||||||
def game_content_bundle():
|
def game_content_bundle():
|
||||||
|
|||||||
@@ -443,7 +443,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', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap']
|
'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', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan']
|
||||||
return render_template('web/changechar.html', skill_ids=skill_ids)
|
return render_template('web/changechar.html', skill_ids=skill_ids)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user