1
0
mirror of synced 2024-12-14 23:32:53 +01:00
bemaniutils/bemani/backend/reflec/volzzabase.py

506 lines
21 KiB
Python
Raw Normal View History

from typing import Dict, List, Tuple
from typing_extensions import Final
from bemani.backend.reflec.base import ReflecBeatBase
from bemani.common import ID, Time, Profile
from bemani.data import Attempt, UserID
from bemani.protocol import Node
class ReflecBeatVolzzaBase(ReflecBeatBase):
# Clear types according to the game
GAME_CLEAR_TYPE_NO_PLAY: Final[int] = 0
GAME_CLEAR_TYPE_EARLY_FAILED: Final[int] = 1
GAME_CLEAR_TYPE_FAILED: Final[int] = 2
GAME_CLEAR_TYPE_CLEARED: Final[int] = 9
GAME_CLEAR_TYPE_HARD_CLEARED: Final[int] = 10
GAME_CLEAR_TYPE_S_HARD_CLEARED: Final[int] = 11
# Combo types according to the game (actually a bitmask, where bit 0 is
# full combo status, and bit 2 is just reflec status). But we don't support
# saving just reflec without full combo, so we downgrade it.
GAME_COMBO_TYPE_NONE: Final[int] = 0
GAME_COMBO_TYPE_ALL_JUST: Final[int] = 2
GAME_COMBO_TYPE_FULL_COMBO: Final[int] = 1
GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST: Final[int] = 3
def _db_to_game_clear_type(self, db_status: int) -> int:
return {
self.CLEAR_TYPE_NO_PLAY: self.GAME_CLEAR_TYPE_NO_PLAY,
self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_FAILED,
self.CLEAR_TYPE_CLEARED: self.GAME_CLEAR_TYPE_CLEARED,
self.CLEAR_TYPE_HARD_CLEARED: self.GAME_CLEAR_TYPE_HARD_CLEARED,
self.CLEAR_TYPE_S_HARD_CLEARED: self.GAME_CLEAR_TYPE_S_HARD_CLEARED,
}[db_status]
def _game_to_db_clear_type(self, status: int) -> int:
return {
self.GAME_CLEAR_TYPE_NO_PLAY: self.CLEAR_TYPE_NO_PLAY,
self.GAME_CLEAR_TYPE_EARLY_FAILED: self.CLEAR_TYPE_FAILED,
self.GAME_CLEAR_TYPE_FAILED: self.CLEAR_TYPE_FAILED,
self.GAME_CLEAR_TYPE_CLEARED: self.CLEAR_TYPE_CLEARED,
self.GAME_CLEAR_TYPE_HARD_CLEARED: self.CLEAR_TYPE_HARD_CLEARED,
self.GAME_CLEAR_TYPE_S_HARD_CLEARED: self.CLEAR_TYPE_S_HARD_CLEARED,
}[status]
def _db_to_game_combo_type(self, db_combo: int) -> int:
return {
self.COMBO_TYPE_NONE: self.GAME_COMBO_TYPE_NONE,
self.COMBO_TYPE_ALMOST_COMBO: self.GAME_COMBO_TYPE_NONE,
self.COMBO_TYPE_FULL_COMBO: self.GAME_COMBO_TYPE_FULL_COMBO,
self.COMBO_TYPE_FULL_COMBO_ALL_JUST: self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST,
}[db_combo]
def _game_to_db_combo_type(self, game_combo: int, miss_count: int) -> int:
if game_combo in [
self.GAME_COMBO_TYPE_NONE,
self.GAME_COMBO_TYPE_ALL_JUST,
]:
if miss_count >= 0 and miss_count <= 2:
return self.COMBO_TYPE_ALMOST_COMBO
else:
return self.COMBO_TYPE_NONE
if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO:
return self.COMBO_TYPE_FULL_COMBO
if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST:
return self.COMBO_TYPE_FULL_COMBO_ALL_JUST
raise Exception(f'Invalid game_combo value {game_combo}')
def _add_event_info(self, root: Node) -> None:
# Overridden in subclasses
pass
def _add_shop_score(self, root: Node) -> None:
shop_score = Node.void('shop_score')
root.add_child(shop_score)
today = Node.void('today')
shop_score.add_child(today)
yesterday = Node.void('yesterday')
shop_score.add_child(yesterday)
all_profiles = self.data.local.user.get_all_profiles(self.game, self.version)
all_attempts = self.data.local.music.get_all_attempts(self.game, self.version, timelimit=(Time.beginning_of_today() - Time.SECONDS_IN_DAY))
machine = self.data.local.machine.get_machine(self.config.machine.pcbid)
if machine.arcade is not None:
lids = [
machine.id for machine in self.data.local.machine.get_all_machines(machine.arcade)
]
else:
lids = [machine.id]
relevant_profiles = [
profile for profile in all_profiles
if profile[1].get_int('lid', -1) in lids
]
for (rootnode, timeoffset) in [
(today, 0),
(yesterday, Time.SECONDS_IN_DAY),
]:
# Grab all attempts made in the relevant day
relevant_attempts = [
attempt for attempt in all_attempts
if (
attempt[1].timestamp >= (Time.beginning_of_today() - timeoffset) and
attempt[1].timestamp <= (Time.end_of_today() - timeoffset)
)
]
# Calculate scores based on attempt
scores_by_user: Dict[UserID, Dict[int, Dict[int, Attempt]]] = {}
for (userid, attempt) in relevant_attempts:
if userid not in scores_by_user:
scores_by_user[userid] = {}
if attempt.id not in scores_by_user[userid]:
scores_by_user[userid][attempt.id] = {}
if attempt.chart not in scores_by_user[userid][attempt.id]:
# No high score for this yet, just use this attempt
scores_by_user[userid][attempt.id][attempt.chart] = attempt
else:
# If this attempt is better than the stored one, replace it
if scores_by_user[userid][attempt.id][attempt.chart].points < attempt.points:
scores_by_user[userid][attempt.id][attempt.chart] = attempt
# Calculate points earned by user in the day
points_by_user: Dict[UserID, int] = {}
for userid in scores_by_user:
points_by_user[userid] = 0
for mid in scores_by_user[userid]:
for chart in scores_by_user[userid][mid]:
points_by_user[userid] = points_by_user[userid] + scores_by_user[userid][mid][chart].points
# Output that day's earned points
for (userid, profile) in relevant_profiles:
data = Node.void('data')
rootnode.add_child(data)
data.add_child(Node.s16('day_id', int((Time.now() - timeoffset) / Time.SECONDS_IN_DAY)))
data.add_child(Node.s32('user_id', profile.extid))
data.add_child(Node.s16('icon_id', profile.get_dict('config').get_int('icon_id')))
data.add_child(Node.s16('point', min(points_by_user.get(userid, 0), 32767)))
data.add_child(Node.s32('update_time', Time.now()))
data.add_child(Node.string('name', profile.get_str('name')))
rootnode.add_child(Node.s32('time', Time.beginning_of_today() - timeoffset))
def handle_info_rb5_info_read_request(self, request: Node) -> Node:
root = Node.void('info')
self._add_event_info(root)
return root
def handle_info_rb5_info_read_hit_chart_request(self, request: Node) -> Node:
version = request.child_value('ver')
root = Node.void('info')
root.add_child(Node.s32('ver', version))
ranking = Node.void('ranking')
root.add_child(ranking)
def add_hitchart(name: str, start: int, end: int, hitchart: List[Tuple[int, int]]) -> None:
base = Node.void(name)
ranking.add_child(base)
base.add_child(Node.s32('bt', start))
base.add_child(Node.s32('et', end))
new = Node.void('new')
base.add_child(new)
for (mid, plays) in hitchart:
d = Node.void('d')
new.add_child(d)
d.add_child(Node.s16('mid', mid))
d.add_child(Node.s32('cnt', plays))
# Weekly hit chart
add_hitchart(
'weekly',
Time.now() - Time.SECONDS_IN_WEEK,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 7),
)
# Monthly hit chart
add_hitchart(
'monthly',
Time.now() - Time.SECONDS_IN_DAY * 30,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 30),
)
# All time hit chart
add_hitchart(
'total',
Time.now() - Time.SECONDS_IN_DAY * 365,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 365),
)
return root
def handle_info_rb5_info_read_shop_ranking_request(self, request: Node) -> Node:
start_music_id = request.child_value('min')
end_music_id = request.child_value('max')
root = Node.void('info')
shop_score = Node.void('shop_score')
root.add_child(shop_score)
shop_score.add_child(Node.s32('time', Time.now()))
profiles: Dict[UserID, Profile] = {}
for songid in range(start_music_id, end_music_id + 1):
allscores = self.data.local.music.get_all_scores(
self.game,
self.version,
songid=songid,
)
for ng in [
self.CHART_TYPE_BASIC,
self.CHART_TYPE_MEDIUM,
self.CHART_TYPE_HARD,
self.CHART_TYPE_SPECIAL,
]:
scores = sorted(
[score for score in allscores if score[1].chart == ng],
key=lambda score: score[1].points,
reverse=True,
)
for i in range(len(scores)):
userid, score = scores[i]
if userid not in profiles:
profiles[userid] = self.get_any_profile(userid)
profile = profiles[userid]
data = Node.void('data')
shop_score.add_child(data)
data.add_child(Node.s32('rank', i + 1))
data.add_child(Node.s16('music_id', songid))
data.add_child(Node.s8('note_grade', score.chart))
data.add_child(Node.s8('clear_type', self._db_to_game_clear_type(score.data.get_int('clear_type'))))
data.add_child(Node.s32('user_id', profile.extid))
data.add_child(Node.s16('icon_id', profile.get_dict('config').get_int('icon_id')))
data.add_child(Node.s32('score', score.points))
data.add_child(Node.s32('time', score.timestamp))
data.add_child(Node.string('name', profile.get_str('name')))
return root
def handle_lobby_rb5_lobby_entry_request(self, request: Node) -> Node:
root = Node.void('lobby')
root.add_child(Node.s32('interval', 120))
root.add_child(Node.s32('interval_p', 120))
# Create a lobby entry for this user
extid = request.child_value('e/uid')
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is not None:
profile = self.get_profile(userid)
info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid)
if profile is None or info is None:
return root
self.data.local.lobby.put_lobby(
self.game,
self.version,
userid,
{
'mid': request.child_value('e/mid'),
'ng': request.child_value('e/ng'),
'mopt': request.child_value('e/mopt'),
'lid': request.child_value('e/lid'),
'sn': request.child_value('e/sn'),
'pref': request.child_value('e/pref'),
'stg': request.child_value('e/stg'),
'pside': request.child_value('e/pside'),
'eatime': request.child_value('e/eatime'),
'ga': request.child_value('e/ga'),
'gp': request.child_value('e/gp'),
'la': request.child_value('e/la'),
'ver': request.child_value('e/ver'),
}
)
lobby = self.data.local.lobby.get_lobby(
self.game,
self.version,
userid,
)
root.add_child(Node.s32('eid', lobby.get_int('id')))
e = Node.void('e')
root.add_child(e)
e.add_child(Node.s32('eid', lobby.get_int('id')))
e.add_child(Node.u16('mid', lobby.get_int('mid')))
e.add_child(Node.u8('ng', lobby.get_int('ng')))
e.add_child(Node.s32('uid', profile.extid))
e.add_child(Node.s32('uattr', profile.get_int('uattr')))
e.add_child(Node.string('pn', profile.get_str('name')))
e.add_child(Node.s32('plyid', info.get_int('id')))
e.add_child(Node.s16('mg', profile.get_int('mg')))
e.add_child(Node.s32('mopt', lobby.get_int('mopt')))
e.add_child(Node.string('lid', lobby.get_str('lid')))
e.add_child(Node.string('sn', lobby.get_str('sn')))
e.add_child(Node.u8('pref', lobby.get_int('pref')))
e.add_child(Node.s8('stg', lobby.get_int('stg')))
e.add_child(Node.s8('pside', lobby.get_int('pside')))
e.add_child(Node.s16('eatime', lobby.get_int('eatime')))
e.add_child(Node.u8_array('ga', lobby.get_int_array('ga', 4)))
e.add_child(Node.u16('gp', lobby.get_int('gp')))
e.add_child(Node.u8_array('la', lobby.get_int_array('la', 4)))
e.add_child(Node.u8('ver', lobby.get_int('ver')))
return root
def handle_lobby_rb5_lobby_read_request(self, request: Node) -> Node:
root = Node.void('lobby')
root.add_child(Node.s32('interval', 120))
root.add_child(Node.s32('interval_p', 120))
# Look up all lobbies matching the criteria specified
ver = request.child_value('var')
mg = request.child_value('m_grade') # noqa: F841
extid = request.child_value('uid')
limit = request.child_value('max')
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is not None:
lobbies = self.data.local.lobby.get_all_lobbies(self.game, self.version)
for (user, lobby) in lobbies:
if limit <= 0:
break
if user == userid:
# If we have our own lobby, don't return it
continue
if ver != lobby.get_int('ver'):
# Don't return lobby data for different versions
continue
profile = self.get_profile(user)
info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid)
if profile is None or info is None:
# No profile info, don't return this lobby
return root
e = Node.void('e')
root.add_child(e)
e.add_child(Node.s32('eid', lobby.get_int('id')))
e.add_child(Node.u16('mid', lobby.get_int('mid')))
e.add_child(Node.u8('ng', lobby.get_int('ng')))
e.add_child(Node.s32('uid', profile.extid))
e.add_child(Node.s32('uattr', profile.get_int('uattr')))
e.add_child(Node.string('pn', profile.get_str('name')))
e.add_child(Node.s32('plyid', info.get_int('id')))
e.add_child(Node.s16('mg', profile.get_int('mg')))
e.add_child(Node.s32('mopt', lobby.get_int('mopt')))
e.add_child(Node.string('lid', lobby.get_str('lid')))
e.add_child(Node.string('sn', lobby.get_str('sn')))
e.add_child(Node.u8('pref', lobby.get_int('pref')))
e.add_child(Node.s8('stg', lobby.get_int('stg')))
e.add_child(Node.s8('pside', lobby.get_int('pside')))
e.add_child(Node.s16('eatime', lobby.get_int('eatime')))
e.add_child(Node.u8_array('ga', lobby.get_int_array('ga', 4)))
e.add_child(Node.u16('gp', lobby.get_int('gp')))
e.add_child(Node.u8_array('la', lobby.get_int_array('la', 4)))
e.add_child(Node.u8('ver', lobby.get_int('ver')))
limit = limit - 1
return root
def handle_lobby_rb5_lobby_delete_entry_request(self, request: Node) -> Node:
eid = request.child_value('eid')
self.data.local.lobby.destroy_lobby(eid)
return Node.void('lobby')
def handle_pcb_rb5_pcb_boot_request(self, request: Node) -> Node:
shop_id = ID.parse_machine_id(request.child_value('lid'))
machine = self.get_machine_by_id(shop_id)
if machine is not None:
machine_name = machine.name
close = machine.data.get_bool('close')
hour = machine.data.get_int('hour')
minute = machine.data.get_int('minute')
else:
machine_name = ''
close = False
hour = 0
minute = 0
root = Node.void('pcb')
sinfo = Node.void('sinfo')
root.add_child(sinfo)
sinfo.add_child(Node.string('nm', machine_name))
sinfo.add_child(Node.bool('cl_enbl', close))
sinfo.add_child(Node.u8('cl_h', hour))
sinfo.add_child(Node.u8('cl_m', minute))
sinfo.add_child(Node.bool('shop_flag', True))
return root
def handle_pcb_rb5_pcb_error_request(self, request: Node) -> Node:
return Node.void('pcb')
def handle_pcb_rb5_pcb_update_request(self, request: Node) -> Node:
return Node.void('pcb')
def handle_shop_rb5_shop_write_setting_request(self, request: Node) -> Node:
return Node.void('shop')
def handle_shop_rb5_shop_write_info_request(self, request: Node) -> Node:
self.update_machine_name(request.child_value('sinfo/nm'))
self.update_machine_data({
'close': request.child_value('sinfo/cl_enbl'),
'hour': request.child_value('sinfo/cl_h'),
'minute': request.child_value('sinfo/cl_m'),
'pref': request.child_value('sinfo/prf'),
})
return Node.void('shop')
def handle_player_rb5_player_start_request(self, request: Node) -> Node:
root = Node.void('player')
# Create a new play session based on info from the request
refid = request.child_value('rid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
self.data.local.lobby.put_play_session_info(
self.game,
self.version,
userid,
{
'ga': request.child_value('ga'),
'gp': request.child_value('gp'),
'la': request.child_value('la'),
'pnid': request.child_value('pnid'),
},
)
info = self.data.local.lobby.get_play_session_info(
self.game,
self.version,
userid,
)
if info is not None:
play_id = info.get_int('id')
else:
play_id = 0
else:
play_id = 0
# Session stuff, and resend global defaults
root.add_child(Node.s32('plyid', play_id))
root.add_child(Node.u64('start_time', Time.now() * 1000))
self._add_event_info(root)
return root
def handle_player_rb5_player_end_request(self, request: Node) -> Node:
# Destroy play session based on info from the request
refid = request.child_value('rid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
# Kill any lingering lobbies by this user
lobby = self.data.local.lobby.get_lobby(
self.game,
self.version,
userid,
)
if lobby is not None:
self.data.local.lobby.destroy_lobby(lobby.get_int('id'))
self.data.local.lobby.destroy_play_session_info(self.game, self.version, userid)
return Node.void('player')
def handle_player_rb5_player_delete_request(self, request: Node) -> Node:
return Node.void('player')
def handle_player_rb5_player_succeed_request(self, request: Node) -> Node:
refid = request.child_value('rid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
previous_version = self.previous_version()
profile = previous_version.get_profile(userid)
else:
profile = None
root = Node.void('player')
if profile is None:
# Return empty succeed to say this is new
root.add_child(Node.string('name', ''))
root.add_child(Node.s32('grd', -1))
root.add_child(Node.s32('ap', -1))
root.add_child(Node.s32('uattr', 0))
else:
# Return previous profile formatted to say this is data succession
root.add_child(Node.string('name', profile.get_str('name')))
root.add_child(Node.s32('grd', profile.get_int('mg'))) # This is a guess
root.add_child(Node.s32('ap', profile.get_int('ap')))
root.add_child(Node.s32('uattr', profile.get_int('uattr')))
return root
def handle_player_rb5_player_read_request(self, request: Node) -> Node:
refid = request.child_value('rid')
profile = self.get_profile_by_refid(refid)
if profile:
return profile
return Node.void('player')