1
0
mirror of synced 2024-12-11 05:55:58 +01:00
bemaniutils/bemani/backend/reflec/base.py

275 lines
11 KiB
Python

# vim: set fileencoding=utf-8
from typing import Dict, List, Optional
from typing_extensions import Final
from bemani.backend.base import Base
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
from bemani.common import Profile, ValidatedDict, GameConstants, DBConstants, Time
from bemani.data import Machine, ScoreSaveException, UserID
from bemani.protocol import Node
class ReflecBeatBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
"""
Base game class for all Reflec Beat version that we support.
"""
game: GameConstants = GameConstants.REFLEC_BEAT
# Chart types, as stored in the DB
CHART_TYPE_BASIC: Final[int] = 0
CHART_TYPE_MEDIUM: Final[int] = 1
CHART_TYPE_HARD: Final[int] = 2
CHART_TYPE_SPECIAL: Final[int] = 3
# Clear types, as saved/loaded from the DB
CLEAR_TYPE_NO_PLAY: Final[int] = DBConstants.REFLEC_BEAT_CLEAR_TYPE_NO_PLAY
CLEAR_TYPE_FAILED: Final[int] = DBConstants.REFLEC_BEAT_CLEAR_TYPE_FAILED
CLEAR_TYPE_CLEARED: Final[int] = DBConstants.REFLEC_BEAT_CLEAR_TYPE_CLEARED
CLEAR_TYPE_HARD_CLEARED: Final[int] = DBConstants.REFLEC_BEAT_CLEAR_TYPE_HARD_CLEARED
CLEAR_TYPE_S_HARD_CLEARED: Final[int] = DBConstants.REFLEC_BEAT_CLEAR_TYPE_S_HARD_CLEARED
# Combo types, as saved/loaded from the DB
COMBO_TYPE_NONE: Final[int] = DBConstants.REFLEC_BEAT_COMBO_TYPE_NONE
COMBO_TYPE_ALMOST_COMBO: Final[int] = DBConstants.REFLEC_BEAT_COMBO_TYPE_ALMOST_COMBO
COMBO_TYPE_FULL_COMBO: Final[int] = DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO
COMBO_TYPE_FULL_COMBO_ALL_JUST: Final[int] = DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO_ALL_JUST
# Return the local2 and lobby2 service so that matching will work on newer
# Reflec Beat games.
extra_services: List[str] = [
'local2',
'lobby2',
]
def previous_version(self) -> Optional['ReflecBeatBase']:
"""
Returns the previous version of the game, based on this game. Should
be overridden.
"""
return None
def format_profile(self, userid: UserID, profile: Profile) -> Node:
"""
Base handler for a profile. Given a userid and a profile dictionary,
return a Node representing a profile. Should be overridden.
"""
return Node.void('pc')
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
"""
Base handler for profile parsing. Given a request and an old profile,
return a new profile that's been updated with the contents of the request.
Should be overridden.
"""
return oldprofile
def get_profile_by_refid(self, refid: Optional[str]) -> Optional[Node]:
"""
Given a RefID, return a formatted profile node. Basically every game
needs a profile lookup, even if it handles where that happens in
a different request. This is provided for code deduplication.
"""
if refid is None:
return None
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
# User doesn't exist but should at this point
return None
# Trying to import from current version
profile = self.get_profile(userid)
if profile is None:
return None
return self.format_profile(userid, profile)
def put_profile_by_refid(self, refid: Optional[str], request: Node) -> Optional[Profile]:
"""
Given a RefID and a request node, unformat the profile and save it.
"""
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
return None
oldprofile = self.get_profile(userid)
if oldprofile is None:
# Create one so we can get refid/extid
oldprofile = Profile(self.game, self.version, refid, 0)
self.put_profile(userid, oldprofile)
newprofile = self.unformat_profile(userid, request, oldprofile)
if newprofile is not None:
self.put_profile(userid, newprofile)
return newprofile
else:
return oldprofile
def get_machine_by_id(self, shop_id: int) -> Optional[Machine]:
pcbid = self.data.local.machine.from_machine_id(shop_id)
if pcbid is not None:
return self.data.local.machine.get_machine(pcbid)
else:
return None
def update_score(
self,
userid: UserID,
songid: int,
chart: int,
points: int,
achievement_rate: int,
clear_type: int,
combo_type: int,
miss_count: int,
combo: Optional[int]=None,
stats: Optional[Dict[str, int]]=None,
param: Optional[int]=None,
kflag: Optional[int]=None,
) -> None:
"""
Given various pieces of a score, update the user's high score and score
history in a controlled manner, so all games in Reflec series can expect
the same attributes in a score. Note that the clear_types passed here are
expected to be converted from game identifier to our internal identifier,
so that any game in the series may convert them back.
"""
# Range check clear type
if clear_type not in [
self.CLEAR_TYPE_NO_PLAY,
self.CLEAR_TYPE_FAILED,
self.CLEAR_TYPE_CLEARED,
self.CLEAR_TYPE_HARD_CLEARED,
self.CLEAR_TYPE_S_HARD_CLEARED,
]:
raise Exception(f"Invalid clear_type value {clear_type}")
# Range check combo type
if combo_type not in [
self.COMBO_TYPE_NONE,
self.COMBO_TYPE_ALMOST_COMBO,
self.COMBO_TYPE_FULL_COMBO,
self.COMBO_TYPE_FULL_COMBO_ALL_JUST,
]:
raise Exception(f"Invalid combo_type value {combo_type}")
oldscore = self.data.local.music.get_score(
self.game,
self.version,
userid,
songid,
chart,
)
# Score history is verbatum, instead of highest score
now = Time.now()
history = ValidatedDict({})
oldpoints = points
if oldscore is None:
# If it is a new score, create a new dictionary to add to
scoredata = ValidatedDict({})
highscore = True
else:
# Set the score to any new record achieved
highscore = points >= oldscore.points
points = max(points, oldscore.points)
scoredata = oldscore.data
# Update the last played time
scoredata.replace_int('last_played_time', now)
# Replace clear type with highest value and timestamps
if clear_type >= scoredata.get_int('clear_type'):
scoredata.replace_int('clear_type', max(scoredata.get_int('clear_type'), clear_type))
scoredata.replace_int('best_clear_type_time', now)
history.replace_int('clear_type', clear_type)
# Replace combo type with highest value and timestamps
if combo_type >= scoredata.get_int('combo_type'):
scoredata.replace_int('combo_type', max(scoredata.get_int('combo_type'), combo_type))
scoredata.replace_int('best_clear_type_time', now)
history.replace_int('combo_type', combo_type)
# Update the combo for this song
if combo is not None:
scoredata.replace_int('combo', max(scoredata.get_int('combo'), combo))
history.replace_int('combo', combo)
# Update the param for this song
if param is not None:
scoredata.replace_int('param', max(scoredata.get_int('param'), param))
history.replace_int('param', param)
# Update the kflag for this song
if kflag is not None:
scoredata.replace_int('kflag', max(scoredata.get_int('kflag'), kflag))
history.replace_int('kflag', kflag)
# Update win/lost/draw stats for this song
if stats is not None:
scoredata.replace_dict('stats', stats)
history.replace_dict('stats', stats)
# Update the achievement rate with timestamps
if achievement_rate >= scoredata.get_int('achievement_rate'):
scoredata.replace_int('achievement_rate', max(scoredata.get_int('achievement_rate'), achievement_rate))
scoredata.replace_int('best_achievement_rate_time', now)
history.replace_int('achievement_rate', achievement_rate)
# Update the miss count with timestamps, either if it was lowered, or if the old value was blank.
# If the new value is -1 (we didn't get a miss count this time), never update the old value.
if miss_count >= 0:
if miss_count <= scoredata.get_int('miss_count', 999999) or scoredata.get_int('miss_count') == -1:
scoredata.replace_int('miss_count', min(scoredata.get_int('miss_count', 999999), miss_count))
scoredata.replace_int('best_miss_count_time', now)
history.replace_int('miss_count', miss_count)
# Look up where this score was earned
lid = self.get_machine_id()
# Reflec Beat happens to send all songs that were played by a player
# at the end of the round. It sends timestamps for the songs, but as of
# Colette they were identical for each song in the round. So, if a user
# plays the same song/chart# more than once in a round, we will end up
# failing to store the attempt since we don't allow two of the same
# attempt at the same time for the same user and song/chart. So, bump
# the timestamp by one second and retry well past the maximum number of
# songs.
for bump in range(10):
timestamp = now + bump
# Write the new score back
self.data.local.music.put_score(
self.game,
self.version,
userid,
songid,
chart,
lid,
points,
scoredata,
highscore,
timestamp=timestamp,
)
try:
# Save the history of this score too
self.data.local.music.put_attempt(
self.game,
self.version,
userid,
songid,
chart,
lid,
oldpoints,
history,
highscore,
timestamp=timestamp,
)
except ScoreSaveException:
# Try again one second in the future
continue
# We saved successfully
break