mirror of
https://github.com/Lost-MSth/Arcaea-server.git
synced 2026-02-13 11:47:32 +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:
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from time import time
|
||||
from random import randint
|
||||
|
||||
from .config import Config
|
||||
|
||||
@@ -12,26 +13,73 @@ def bi(value):
|
||||
return int.from_bytes(value, byteorder='little')
|
||||
|
||||
|
||||
class Player:
|
||||
class Score:
|
||||
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_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00'
|
||||
self.token = 0
|
||||
|
||||
self.character_id = 0xff
|
||||
self.last_character_id = 0xff
|
||||
self.is_uncapped = 0
|
||||
|
||||
self.difficulty = 0xff
|
||||
self.last_difficulty = 0xff
|
||||
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.score = Score()
|
||||
self.last_score = Score()
|
||||
|
||||
self.finish_flag = 0
|
||||
|
||||
self.player_state = 1
|
||||
@@ -45,6 +93,16 @@ class Player:
|
||||
|
||||
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
|
||||
def name(self) -> str:
|
||||
return self.player_name.decode('ascii').rstrip('\x00')
|
||||
@@ -56,15 +114,23 @@ class Player:
|
||||
'is_online': self.online == 1,
|
||||
'character_id': self.character_id,
|
||||
'is_uncapped': self.is_uncapped == 1,
|
||||
'rating_ptt': self.rating_ptt,
|
||||
'is_hide_rating': self.is_hide_rating == 1,
|
||||
'last_song': {
|
||||
'difficulty': self.last_difficulty,
|
||||
'score': self.last_score,
|
||||
'cleartype': self.last_cleartype,
|
||||
'difficulty': self.last_score.difficulty,
|
||||
'score': self.last_score.score,
|
||||
'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': {
|
||||
'difficulty': self.difficulty,
|
||||
'score': self.score,
|
||||
'cleartype': self.cleartype,
|
||||
'difficulty': self.score.difficulty,
|
||||
'score': self.score.score,
|
||||
'cleartype': self.score.cleartype,
|
||||
},
|
||||
'player_state': self.player_state,
|
||||
'last_timestamp': self.last_timestamp,
|
||||
@@ -77,30 +143,92 @@ class Player:
|
||||
else:
|
||||
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:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.room_id = 0
|
||||
self.room_code = 'AAAA00'
|
||||
self.share_token = 'abcde12345' # 5.10 新增
|
||||
|
||||
self.countdown = 0xffffffff
|
||||
self.timestamp = 0
|
||||
self.state = 0
|
||||
self.song_idx = 0xffff
|
||||
self.last_song_idx = 0xffff
|
||||
self._state = 0
|
||||
self.song_idx = 0xffff # 疑似 idx * 5
|
||||
self.last_song_idx = 0xffff # 疑似 idx * 5
|
||||
|
||||
self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH
|
||||
|
||||
self.host_id = 0
|
||||
self.players = [Player(), Player(), Player(), Player()]
|
||||
self.players = [Player(0), Player(1), Player(2), Player(3)]
|
||||
|
||||
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.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:
|
||||
p = [i.to_dict() for i in self.players if i.player_id != 0]
|
||||
for i in p:
|
||||
@@ -108,21 +236,47 @@ class Room:
|
||||
return {
|
||||
'room_id': self.room_id,
|
||||
'room_code': self.room_code,
|
||||
'share_token': self.share_token,
|
||||
'state': self.state,
|
||||
'song_idx': self.song_idx,
|
||||
'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff,
|
||||
'host_id': self.host_id,
|
||||
'players': p,
|
||||
'round_switch': self.round_switch == 1,
|
||||
'round_mode': self.round_mode,
|
||||
'last_timestamp': self.timestamp,
|
||||
'is_enterable': self.is_enterable,
|
||||
'is_matchable': self.is_matchable,
|
||||
'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
|
||||
def is_enterable(self) -> bool:
|
||||
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
|
||||
def is_playing(self) -> bool:
|
||||
return self.state in (4, 5, 6, 7)
|
||||
@@ -133,7 +287,9 @@ class Room:
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
def check_player_online(self, now: int = None):
|
||||
@@ -156,29 +312,18 @@ class Room:
|
||||
|
||||
def get_players_info(self):
|
||||
# 获取所有玩家信息
|
||||
re = b''
|
||||
re = bytearray()
|
||||
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) + \
|
||||
b(i.timer, 4) + b(i.cleartype) + b(i.player_state) + \
|
||||
b(i.download_percent) + b(i.online) + b'\x00' + i.player_name
|
||||
return re
|
||||
re.extend(i.info)
|
||||
re.append(0)
|
||||
re.extend(i.player_name)
|
||||
return bytes(re)
|
||||
|
||||
def get_player_last_score(self):
|
||||
# 获取上次曲目玩家分数,返回bytes
|
||||
if self.last_song_idx == 0xffff:
|
||||
return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' * 4
|
||||
re = b''
|
||||
|
||||
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
|
||||
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
|
||||
return b''.join(i.last_score_info for i in self.players)
|
||||
|
||||
def make_round(self):
|
||||
# 轮换房主
|
||||
@@ -203,9 +348,19 @@ class Room:
|
||||
f'Player `{player.name}` leaves room `{self.room_code}`')
|
||||
|
||||
self.players[player_index].online = 0
|
||||
self.players[player_index] = Player()
|
||||
self.players[player_index] = Player(player_index)
|
||||
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):
|
||||
# 更新房间可用歌曲
|
||||
r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH)
|
||||
@@ -245,27 +400,110 @@ class Room:
|
||||
max_score_i = []
|
||||
for i in range(4):
|
||||
player = self.players[i]
|
||||
if player.player_id != 0:
|
||||
player.finish_flag = 0
|
||||
player.last_timer = player.timer
|
||||
player.last_score = player.score
|
||||
player.last_cleartype = player.cleartype
|
||||
player.last_character_id = player.character_id
|
||||
player.last_difficulty = player.difficulty
|
||||
player.best_player_flag = 0
|
||||
if player.player_id == 0:
|
||||
continue
|
||||
player.finish_flag = 0
|
||||
player.last_score.copy(player.score)
|
||||
player.last_score.best_player_flag = 0
|
||||
|
||||
if player.last_score > max_score:
|
||||
max_score = player.last_score
|
||||
max_score_i = [i]
|
||||
elif player.last_score == max_score:
|
||||
max_score_i.append(i)
|
||||
if player.last_score.score > max_score:
|
||||
max_score = player.last_score.score
|
||||
max_score_i = [i]
|
||||
elif player.last_score.score == max_score:
|
||||
max_score_i.append(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(
|
||||
f'Room `{self.room_code}` finishes song `{self.song_idx}`')
|
||||
for i in self.players:
|
||||
if i.player_id != 0:
|
||||
logging.info(
|
||||
f'- Player `{i.name}` - Score: {i.last_score} Cleartype: {i.last_cleartype} Difficulty: {i.last_difficulty}')
|
||||
logging.info(f'- Player `{i.name}` - {i.last_score}')
|
||||
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user