mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-09-27 03:17:05 -05:00
Initial commit of BEMANI Utilities to GitHub.
This commit is contained in:
5
bemani/api/objects/__init__.py
Normal file
5
bemani/api/objects/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from bemani.api.objects.base import BaseObject
|
||||
from bemani.api.objects.catalog import CatalogObject
|
||||
from bemani.api.objects.records import RecordsObject
|
||||
from bemani.api.objects.profile import ProfileObject
|
||||
from bemani.api.objects.statistics import StatisticsObject
|
||||
23
bemani/api/objects/base.py
Normal file
23
bemani/api/objects/base.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import List, Any, Dict
|
||||
|
||||
from bemani.api.exceptions import APIException
|
||||
from bemani.data import Data
|
||||
|
||||
|
||||
class BaseObject:
|
||||
"""
|
||||
A base class which represents a fetchable API object. Every fetchable object
|
||||
will subclass from this and implement one or more version fetches. These
|
||||
are dynamically looked up by the version number provided by the client, so
|
||||
objects can control which versions they reply to by subclassing or ignoring
|
||||
various fetch versions.
|
||||
"""
|
||||
|
||||
def __init__(self, data: Data, game: str, version: int, omnimix: bool) -> None:
|
||||
self.data = data
|
||||
self.game = game
|
||||
self.version = version
|
||||
self.omnimix = omnimix
|
||||
|
||||
def fetch_v1(self, idtype: str, ids: List[str], params: Dict[str, Any]) -> Any:
|
||||
raise APIException('Object fetch not supported for this version!')
|
||||
185
bemani/api/objects/catalog.py
Normal file
185
bemani/api/objects/catalog.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from bemani.api.exceptions import APIException
|
||||
from bemani.api.objects.base import BaseObject
|
||||
from bemani.common import GameConstants, APIConstants, DBConstants, VersionConstants
|
||||
from bemani.data import Song
|
||||
|
||||
|
||||
class CatalogObject(BaseObject):
|
||||
|
||||
def __format_ddr_song(self, song: Song) -> Dict[str, Any]:
|
||||
groove = song.data.get_dict('groove')
|
||||
return {
|
||||
'editid': str(song.data.get_int('edit_id')),
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'bpm_min': song.data.get_int('bpm_min'),
|
||||
'bpm_max': song.data.get_int('bpm_max'),
|
||||
'category': str(song.data.get_int('category')),
|
||||
'groove': {
|
||||
'air': groove.get_int('air'),
|
||||
'chaos': groove.get_int('chaos'),
|
||||
'freeze': groove.get_int('freeze'),
|
||||
'stream': groove.get_int('stream'),
|
||||
'voltage': groove.get_int('voltage'),
|
||||
},
|
||||
}
|
||||
|
||||
def __format_iidx_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'bpm_min': song.data.get_int('bpm_min'),
|
||||
'bpm_max': song.data.get_int('bpm_max'),
|
||||
'notecount': song.data.get_int('notecount'),
|
||||
'category': str(int(song.id / 1000)),
|
||||
}
|
||||
|
||||
def __format_jubeat_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'bpm_min': song.data.get_int('bpm_min'),
|
||||
'bpm_max': song.data.get_int('bpm_max'),
|
||||
}
|
||||
|
||||
def __format_museca_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'bpm_min': song.data.get_int('bpm_min'),
|
||||
'bpm_max': song.data.get_int('bpm_max'),
|
||||
'limited': song.data.get_int('limited'),
|
||||
}
|
||||
|
||||
def __format_popn_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'category': song.data.get_str('category'),
|
||||
}
|
||||
|
||||
def __format_reflec_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'category': str(song.data.get_int('folder')),
|
||||
'musicid': song.data.get_str('chart_id'),
|
||||
}
|
||||
|
||||
def __format_sdvx_song(self, song: Song) -> Dict[str, Any]:
|
||||
return {
|
||||
'difficulty': song.data.get_int('difficulty'),
|
||||
'bpm_min': song.data.get_int('bpm_min'),
|
||||
'bpm_max': song.data.get_int('bpm_max'),
|
||||
'limited': song.data.get_int('limited'),
|
||||
}
|
||||
|
||||
def __format_song(self, song: Song) -> Dict[str, Any]:
|
||||
base = {
|
||||
'song': str(song.id),
|
||||
'chart': str(song.chart),
|
||||
'title': song.name or "",
|
||||
'artist': song.artist or "",
|
||||
'genre': song.genre or "",
|
||||
}
|
||||
|
||||
if self.game == GameConstants.DDR:
|
||||
base.update(self.__format_ddr_song(song))
|
||||
if self.game == GameConstants.IIDX:
|
||||
base.update(self.__format_iidx_song(song))
|
||||
if self.game == GameConstants.JUBEAT:
|
||||
base.update(self.__format_jubeat_song(song))
|
||||
if self.game == GameConstants.MUSECA:
|
||||
base.update(self.__format_museca_song(song))
|
||||
if self.game == GameConstants.POPN_MUSIC:
|
||||
base.update(self.__format_popn_song(song))
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
base.update(self.__format_reflec_song(song))
|
||||
if self.game == GameConstants.SDVX:
|
||||
base.update(self.__format_sdvx_song(song))
|
||||
|
||||
return base
|
||||
|
||||
def __format_sdvx_extras(self) -> Dict[str, List[Dict[str, Any]]]:
|
||||
# Gotta look up the unlock catalog
|
||||
items = self.data.local.game.get_items(self.game, self.version)
|
||||
|
||||
# Format it depending on the version
|
||||
if self.version == 1:
|
||||
return {
|
||||
"purchases": [
|
||||
{
|
||||
"catalogid": str(item.id),
|
||||
"song": str(item.data.get_int("musicid")),
|
||||
"chart": str(item.data.get_int("chart")),
|
||||
"price": item.data.get_int("blocks"),
|
||||
}
|
||||
for item in items
|
||||
if item.type == "song_unlock"
|
||||
],
|
||||
"appealcards": [],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"purchases": [],
|
||||
"appealcards": [
|
||||
{
|
||||
"appealid": str(item.id),
|
||||
"description": item.data.get_str("description"),
|
||||
}
|
||||
for item in items
|
||||
if item.type == "appealcard"
|
||||
],
|
||||
}
|
||||
|
||||
def __format_extras(self) -> Dict[str, List[Dict[str, Any]]]:
|
||||
if self.game == GameConstants.SDVX:
|
||||
return self.__format_sdvx_extras()
|
||||
else:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def music_version(self) -> int:
|
||||
if self.game == GameConstants.IIDX:
|
||||
if self.omnimix:
|
||||
return self.version + DBConstants.OMNIMIX_VERSION_BUMP
|
||||
else:
|
||||
return self.version
|
||||
else:
|
||||
return self.version
|
||||
|
||||
def fetch_v1(self, idtype: str, ids: List[str], params: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
# Verify IDs
|
||||
if idtype != APIConstants.ID_TYPE_SERVER:
|
||||
raise APIException(
|
||||
'Unsupported ID for lookup!',
|
||||
405,
|
||||
)
|
||||
|
||||
# Fetch the songs
|
||||
songs = self.data.local.music.get_all_songs(self.game, self.music_version)
|
||||
if self.game == GameConstants.JUBEAT and self.version == VersionConstants.JUBEAT_CLAN:
|
||||
# There's always a special case. We don't store all music IDs since those in
|
||||
# the range of 80000301-80000347 are actually the same song, but copy-pasted
|
||||
# for different prefectures and slightly different charts. So, we need to copy
|
||||
# that song data so that remote clients can resolve scores for those ID ranges.
|
||||
additions: List[Song] = []
|
||||
for song in songs:
|
||||
if song.id == 80000301:
|
||||
for idrange in range(80000302, 80000348):
|
||||
additions.append(
|
||||
Song(
|
||||
song.game,
|
||||
song.version,
|
||||
idrange,
|
||||
song.chart,
|
||||
song.name,
|
||||
song.artist,
|
||||
song.genre,
|
||||
song.data,
|
||||
)
|
||||
)
|
||||
songs.extend(additions)
|
||||
retval = {
|
||||
'songs': [self.__format_song(song) for song in songs],
|
||||
}
|
||||
|
||||
# Fetch any optional extras per-game, return
|
||||
retval.update(self.__format_extras())
|
||||
return retval
|
||||
131
bemani/api/objects/profile.py
Normal file
131
bemani/api/objects/profile.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from typing import Any, Dict, List, Set, Tuple
|
||||
|
||||
from bemani.api.exceptions import APIException
|
||||
from bemani.api.objects.base import BaseObject
|
||||
from bemani.common import ValidatedDict, GameConstants, APIConstants
|
||||
from bemani.data import UserID
|
||||
|
||||
|
||||
class ProfileObject(BaseObject):
|
||||
|
||||
def __format_ddr_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {
|
||||
'area': profile.get_int('area', -1) if exact else -1,
|
||||
}
|
||||
|
||||
def __format_iidx_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
qpro = profile.get_dict('qpro')
|
||||
|
||||
return {
|
||||
'area': profile.get_int('pid', -1),
|
||||
'qpro': {
|
||||
'head': qpro.get_int('head', -1) if exact else -1,
|
||||
'hair': qpro.get_int('hair', -1) if exact else -1,
|
||||
'face': qpro.get_int('face', -1) if exact else -1,
|
||||
'body': qpro.get_int('body', -1) if exact else -1,
|
||||
'hand': qpro.get_int('hand', -1) if exact else -1,
|
||||
}
|
||||
}
|
||||
|
||||
def __format_jubeat_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def __format_museca_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def __format_popn_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {
|
||||
'character': profile.get_int('chara', -1) if exact else -1,
|
||||
}
|
||||
|
||||
def __format_reflec_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {
|
||||
'icon': profile.get_dict('config').get_int('icon_id', -1) if exact else -1,
|
||||
}
|
||||
|
||||
def __format_sdvx_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def __format_profile(self, cardids: List[str], profile: ValidatedDict, settings: ValidatedDict, exact: bool) -> Dict[str, Any]:
|
||||
base = {
|
||||
'name': profile.get_str('name'),
|
||||
'cards': cardids,
|
||||
'registered': settings.get_int('first_play_timestamp', -1),
|
||||
'updated': settings.get_int('last_play_timestamp', -1),
|
||||
'plays': settings.get_int('total_plays', -1),
|
||||
'match': 'exact' if exact else 'partial',
|
||||
}
|
||||
|
||||
if self.game == GameConstants.DDR:
|
||||
base.update(self.__format_ddr_profile(profile, exact))
|
||||
if self.game == GameConstants.IIDX:
|
||||
base.update(self.__format_iidx_profile(profile, exact))
|
||||
if self.game == GameConstants.JUBEAT:
|
||||
base.update(self.__format_jubeat_profile(profile, exact))
|
||||
if self.game == GameConstants.MUSECA:
|
||||
base.update(self.__format_museca_profile(profile, exact))
|
||||
if self.game == GameConstants.POPN_MUSIC:
|
||||
base.update(self.__format_popn_profile(profile, exact))
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
base.update(self.__format_reflec_profile(profile, exact))
|
||||
if self.game == GameConstants.SDVX:
|
||||
base.update(self.__format_sdvx_profile(profile, exact))
|
||||
|
||||
return base
|
||||
|
||||
def fetch_v1(self, idtype: str, ids: List[str], params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
# Fetch the profiles
|
||||
profiles: List[Tuple[UserID, ValidatedDict]] = []
|
||||
if idtype == APIConstants.ID_TYPE_SERVER:
|
||||
profiles.extend(self.data.local.user.get_all_profiles(self.game, self.version))
|
||||
elif idtype == APIConstants.ID_TYPE_SONG:
|
||||
raise APIException(
|
||||
'Unsupported ID for lookup!',
|
||||
405,
|
||||
)
|
||||
elif idtype == APIConstants.ID_TYPE_INSTANCE:
|
||||
raise APIException(
|
||||
'Unsupported ID for lookup!',
|
||||
405,
|
||||
)
|
||||
elif idtype == APIConstants.ID_TYPE_CARD:
|
||||
users: Set[UserID] = set()
|
||||
for cardid in ids:
|
||||
userid = self.data.local.user.from_cardid(cardid)
|
||||
if userid is not None:
|
||||
# Don't duplicate loads for users with multiple card IDs if multiples
|
||||
# of those IDs are requested.
|
||||
if userid in users:
|
||||
continue
|
||||
users.add(userid)
|
||||
|
||||
# We can possibly find another profile for this user. This is important
|
||||
# in the case that we returned scores for a user that doesn't have a
|
||||
# profile on a particular version. We allow that on this network, so in
|
||||
# order to not break remote networks, try our best to return any profile.
|
||||
profile = self.data.local.user.get_any_profile(self.game, self.version, userid)
|
||||
if profile is not None:
|
||||
profiles.append((userid, profile))
|
||||
else:
|
||||
raise APIException('Invalid ID type!')
|
||||
|
||||
# Now, fetch the users, and filter out profiles belonging to orphaned users
|
||||
retval: List[Dict[str, Any]] = []
|
||||
id_to_cards: Dict[UserID, List[str]] = {}
|
||||
for (userid, profile) in profiles:
|
||||
if userid not in id_to_cards:
|
||||
cards = self.data.local.user.get_cards(userid)
|
||||
if len(cards) == 0:
|
||||
# Can't add this user, skip the profile
|
||||
continue
|
||||
|
||||
id_to_cards[userid] = cards
|
||||
|
||||
# Format the profile and add it
|
||||
settings = self.data.local.game.get_settings(self.game, userid)
|
||||
if settings is None:
|
||||
settings = ValidatedDict({})
|
||||
|
||||
retval.append(self.__format_profile(id_to_cards[userid], profile, settings, profile['version'] == self.version))
|
||||
|
||||
return retval
|
||||
299
bemani/api/objects/records.py
Normal file
299
bemani/api/objects/records.py
Normal file
@@ -0,0 +1,299 @@
|
||||
from typing import Any, Dict, List, Set, Tuple
|
||||
|
||||
from bemani.api.exceptions import APIException
|
||||
from bemani.api.objects.base import BaseObject
|
||||
from bemani.common import GameConstants, VersionConstants, APIConstants, DBConstants
|
||||
from bemani.data import Score, UserID
|
||||
|
||||
|
||||
class RecordsObject(BaseObject):
|
||||
|
||||
def __format_ddr_record(self, record: Score) -> Dict[str, Any]:
|
||||
halo = {
|
||||
DBConstants.DDR_HALO_NONE: 'none',
|
||||
DBConstants.DDR_HALO_GOOD_FULL_COMBO: 'gfc',
|
||||
DBConstants.DDR_HALO_GREAT_FULL_COMBO: 'fc',
|
||||
DBConstants.DDR_HALO_PERFECT_FULL_COMBO: 'pfc',
|
||||
DBConstants.DDR_HALO_MARVELOUS_FULL_COMBO: 'mfc',
|
||||
}.get(record.data.get_int('halo'), 'none')
|
||||
rank = {
|
||||
DBConstants.DDR_RANK_AAA: "AAA",
|
||||
DBConstants.DDR_RANK_AA_PLUS: "AA+",
|
||||
DBConstants.DDR_RANK_AA: "AA",
|
||||
DBConstants.DDR_RANK_AA_MINUS: "AA-",
|
||||
DBConstants.DDR_RANK_A_PLUS: "A+",
|
||||
DBConstants.DDR_RANK_A: "A",
|
||||
DBConstants.DDR_RANK_A_MINUS: "A-",
|
||||
DBConstants.DDR_RANK_B_PLUS: "B+",
|
||||
DBConstants.DDR_RANK_B: "B",
|
||||
DBConstants.DDR_RANK_B_MINUS: "B-",
|
||||
DBConstants.DDR_RANK_C_PLUS: "C+",
|
||||
DBConstants.DDR_RANK_C: "C",
|
||||
DBConstants.DDR_RANK_C_MINUS: "C-",
|
||||
DBConstants.DDR_RANK_D_PLUS: "D+",
|
||||
DBConstants.DDR_RANK_D: "D",
|
||||
DBConstants.DDR_RANK_E: "E",
|
||||
}.get(record.data.get_int('rank'), 'E')
|
||||
|
||||
if self.version == VersionConstants.DDR_ACE:
|
||||
# DDR Ace is specia
|
||||
ghost = [int(x) for x in record.data.get_str('ghost')]
|
||||
else:
|
||||
if 'trace' not in record.data:
|
||||
ghost = []
|
||||
else:
|
||||
ghost = record.data.get_int_array('trace', len(record.data['trace']))
|
||||
|
||||
return {
|
||||
'rank': rank,
|
||||
'halo': halo,
|
||||
'combo': record.data.get_int('combo'),
|
||||
'ghost': ghost,
|
||||
}
|
||||
|
||||
def __format_iidx_record(self, record: Score) -> Dict[str, Any]:
|
||||
status = {
|
||||
DBConstants.IIDX_CLEAR_STATUS_NO_PLAY: 'np',
|
||||
DBConstants.IIDX_CLEAR_STATUS_FAILED: 'failed',
|
||||
DBConstants.IIDX_CLEAR_STATUS_ASSIST_CLEAR: 'ac',
|
||||
DBConstants.IIDX_CLEAR_STATUS_EASY_CLEAR: 'ec',
|
||||
DBConstants.IIDX_CLEAR_STATUS_CLEAR: 'nc',
|
||||
DBConstants.IIDX_CLEAR_STATUS_HARD_CLEAR: 'hc',
|
||||
DBConstants.IIDX_CLEAR_STATUS_EX_HARD_CLEAR: 'exhc',
|
||||
DBConstants.IIDX_CLEAR_STATUS_FULL_COMBO: 'fc',
|
||||
}.get(record.data.get_int('clear_status'), 'np')
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'miss': record.data.get_int('miss_count', -1),
|
||||
'ghost': [b for b in record.data.get_bytes('ghost')],
|
||||
'pgreat': record.data.get_int('pgreats', -1),
|
||||
'great': record.data.get_int('greats', -1),
|
||||
}
|
||||
|
||||
def __format_jubeat_record(self, record: Score) -> Dict[str, Any]:
|
||||
status = {
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_FAILED: 'failed',
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_CLEARED: 'cleared',
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_NEARLY_FULL_COMBO: 'nfc',
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_FULL_COMBO: 'fc',
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_NEARLY_EXCELLENT: 'nec',
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_EXCELLENT: 'exc',
|
||||
}.get(record.data.get_int('medal'), 'failed')
|
||||
if 'ghost' not in record.data:
|
||||
ghost: List[int] = []
|
||||
else:
|
||||
ghost = record.data.get_int_array('ghost', len(record.data['ghost']))
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'combo': record.data.get_int('combo', -1),
|
||||
'ghost': ghost,
|
||||
}
|
||||
|
||||
def __format_museca_record(self, record: Score) -> Dict[str, Any]:
|
||||
rank = {
|
||||
DBConstants.MUSECA_GRADE_DEATH: 'death',
|
||||
DBConstants.MUSECA_GRADE_POOR: 'poor',
|
||||
DBConstants.MUSECA_GRADE_MEDIOCRE: 'mediocre',
|
||||
DBConstants.MUSECA_GRADE_GOOD: 'good',
|
||||
DBConstants.MUSECA_GRADE_GREAT: 'great',
|
||||
DBConstants.MUSECA_GRADE_EXCELLENT: 'excellent',
|
||||
DBConstants.MUSECA_GRADE_SUPERB: 'superb',
|
||||
DBConstants.MUSECA_GRADE_MASTERPIECE: 'masterpiece',
|
||||
DBConstants.MUSECA_GRADE_PERFECT: 'perfect'
|
||||
}.get(record.data.get_int('grade'), 'death')
|
||||
status = {
|
||||
DBConstants.MUSECA_CLEAR_TYPE_FAILED: 'failed',
|
||||
DBConstants.MUSECA_CLEAR_TYPE_CLEARED: 'cleared',
|
||||
DBConstants.MUSECA_CLEAR_TYPE_FULL_COMBO: 'fc',
|
||||
}.get(record.data.get_int('clear_type'), 'failed')
|
||||
|
||||
return {
|
||||
'rank': rank,
|
||||
'status': status,
|
||||
'combo': record.data.get_int('combo', -1),
|
||||
'buttonrate': record.data.get_dict('stats').get_int('btn_rate'),
|
||||
'longrate': record.data.get_dict('stats').get_int('long_rate'),
|
||||
'volrate': record.data.get_dict('stats').get_int('vol_rate'),
|
||||
}
|
||||
|
||||
def __format_popn_record(self, record: Score) -> Dict[str, Any]:
|
||||
status = {
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED: 'cf',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED: 'df',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FAILED: 'sf',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_EASY_CLEAR: 'ec',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_CLEARED: 'cc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_CLEARED: 'dc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_CLEARED: 'sc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FULL_COMBO: 'cfc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FULL_COMBO: 'dfc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FULL_COMBO: 'sfc',
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_PERFECT: 'p',
|
||||
}.get(record.data.get_int('medal'), 'cf')
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'combo': record.data.get_int('combo', -1),
|
||||
}
|
||||
|
||||
def __format_reflec_record(self, record: Score) -> Dict[str, Any]:
|
||||
status = {
|
||||
DBConstants.REFLEC_BEAT_CLEAR_TYPE_NO_PLAY: 'np',
|
||||
DBConstants.REFLEC_BEAT_CLEAR_TYPE_FAILED: 'failed',
|
||||
DBConstants.REFLEC_BEAT_CLEAR_TYPE_CLEARED: 'cleared',
|
||||
DBConstants.REFLEC_BEAT_CLEAR_TYPE_HARD_CLEARED: 'hc',
|
||||
DBConstants.REFLEC_BEAT_CLEAR_TYPE_S_HARD_CLEARED: 'shc',
|
||||
}.get(record.data.get_int('clear_type'), 'np')
|
||||
halo = {
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_NONE: 'none',
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_ALMOST_COMBO: 'ac',
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO: 'fc',
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO_ALL_JUST: 'fcaj',
|
||||
}.get(record.data.get_int('combo_type'), 'none')
|
||||
|
||||
return {
|
||||
'rate': record.data.get_int('achievement_rate'),
|
||||
'status': status,
|
||||
'halo': halo,
|
||||
'combo': record.data.get_int('combo', -1),
|
||||
'miss': record.data.get_int('miss_count', -1),
|
||||
}
|
||||
|
||||
def __format_sdvx_record(self, record: Score) -> Dict[str, Any]:
|
||||
status = {
|
||||
DBConstants.SDVX_CLEAR_TYPE_NO_PLAY: 'np',
|
||||
DBConstants.SDVX_CLEAR_TYPE_FAILED: 'failed',
|
||||
DBConstants.SDVX_CLEAR_TYPE_CLEAR: 'cleared',
|
||||
DBConstants.SDVX_CLEAR_TYPE_HARD_CLEAR: 'hc',
|
||||
DBConstants.SDVX_CLEAR_TYPE_ULTIMATE_CHAIN: 'uc',
|
||||
DBConstants.SDVX_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN: 'puc',
|
||||
}.get(record.data.get_int('clear_type'), 'np')
|
||||
rank = {
|
||||
DBConstants.SDVX_GRADE_NO_PLAY: 'E',
|
||||
DBConstants.SDVX_GRADE_D: 'D',
|
||||
DBConstants.SDVX_GRADE_C: 'C',
|
||||
DBConstants.SDVX_GRADE_B: 'B',
|
||||
DBConstants.SDVX_GRADE_A: 'A',
|
||||
DBConstants.SDVX_GRADE_A_PLUS: 'A+',
|
||||
DBConstants.SDVX_GRADE_AA: 'AA',
|
||||
DBConstants.SDVX_GRADE_AA_PLUS: 'AA+',
|
||||
DBConstants.SDVX_GRADE_AAA: 'AAA',
|
||||
DBConstants.SDVX_GRADE_AAA_PLUS: 'AAA+',
|
||||
DBConstants.SDVX_GRADE_S: 'S',
|
||||
}.get(record.data.get_int('grade'), 'E')
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'rank': rank,
|
||||
'combo': record.data.get_int('combo', -1),
|
||||
'buttonrate': record.data.get_dict('stats').get_int('btn_rate'),
|
||||
'longrate': record.data.get_dict('stats').get_int('long_rate'),
|
||||
'volrate': record.data.get_dict('stats').get_int('vol_rate'),
|
||||
}
|
||||
|
||||
def __format_record(self, cardids: List[str], record: Score) -> Dict[str, Any]:
|
||||
base = {
|
||||
'cards': cardids,
|
||||
'song': str(record.id),
|
||||
'chart': str(record.chart),
|
||||
'points': record.points,
|
||||
'timestamp': record.timestamp,
|
||||
'updated': record.update,
|
||||
}
|
||||
|
||||
if self.game == GameConstants.DDR:
|
||||
base.update(self.__format_ddr_record(record))
|
||||
if self.game == GameConstants.IIDX:
|
||||
base.update(self.__format_iidx_record(record))
|
||||
if self.game == GameConstants.JUBEAT:
|
||||
base.update(self.__format_jubeat_record(record))
|
||||
if self.game == GameConstants.MUSECA:
|
||||
base.update(self.__format_museca_record(record))
|
||||
if self.game == GameConstants.POPN_MUSIC:
|
||||
base.update(self.__format_popn_record(record))
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
base.update(self.__format_reflec_record(record))
|
||||
if self.game == GameConstants.SDVX:
|
||||
base.update(self.__format_sdvx_record(record))
|
||||
|
||||
return base
|
||||
|
||||
@property
|
||||
def music_version(self) -> int:
|
||||
if self.game == GameConstants.IIDX:
|
||||
if self.omnimix:
|
||||
return self.version + DBConstants.OMNIMIX_VERSION_BUMP
|
||||
else:
|
||||
return self.version
|
||||
else:
|
||||
return self.version
|
||||
|
||||
def fetch_v1(self, idtype: str, ids: List[str], params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
since = params.get('since')
|
||||
until = params.get('until')
|
||||
|
||||
# Fetch the scores
|
||||
records: List[Tuple[UserID, Score]] = []
|
||||
if idtype == APIConstants.ID_TYPE_SERVER:
|
||||
# Because of the way this query works, we can't apply since/until to it directly.
|
||||
# If we did, it would miss higher scores earned before since or after until, and
|
||||
# incorrectly report records.
|
||||
records.extend(self.data.local.music.get_all_records(self.game, self.music_version))
|
||||
elif idtype == APIConstants.ID_TYPE_SONG:
|
||||
if len(ids) == 1:
|
||||
songid = int(ids[0])
|
||||
chart = None
|
||||
else:
|
||||
songid = int(ids[0])
|
||||
chart = int(ids[1])
|
||||
records.extend(self.data.local.music.get_all_scores(self.game, self.music_version, songid=songid, songchart=chart, since=since, until=until))
|
||||
elif idtype == APIConstants.ID_TYPE_INSTANCE:
|
||||
songid = int(ids[0])
|
||||
chart = int(ids[1])
|
||||
cardid = ids[2]
|
||||
userid = self.data.local.user.from_cardid(cardid)
|
||||
if userid is not None:
|
||||
score = self.data.local.music.get_score(self.game, self.music_version, userid, songid, chart)
|
||||
if score is not None:
|
||||
records.append((userid, score))
|
||||
elif idtype == APIConstants.ID_TYPE_CARD:
|
||||
users: Set[UserID] = set()
|
||||
for cardid in ids:
|
||||
userid = self.data.local.user.from_cardid(cardid)
|
||||
if userid is not None:
|
||||
# Don't duplicate loads for users with multiple card IDs if multiples
|
||||
# of those IDs are requested.
|
||||
if userid in users:
|
||||
continue
|
||||
users.add(userid)
|
||||
|
||||
records.extend([(userid, score) for score in self.data.local.music.get_scores(self.game, self.music_version, userid, since=since, until=until)])
|
||||
else:
|
||||
raise APIException('Invalid ID type!')
|
||||
|
||||
# Now, fetch the users, and filter out scores belonging to orphaned users
|
||||
id_to_cards: Dict[UserID, List[str]] = {}
|
||||
retval: List[Dict[str, Any]] = []
|
||||
for (userid, record) in records:
|
||||
# Postfilter for queries that can't filter. This will save on data transferred.
|
||||
if since is not None:
|
||||
if record.update < since:
|
||||
continue
|
||||
if until is not None:
|
||||
if record.update >= until:
|
||||
continue
|
||||
|
||||
if userid not in id_to_cards:
|
||||
cards = self.data.local.user.get_cards(userid)
|
||||
if len(cards) == 0:
|
||||
# Can't add this user, skip the score
|
||||
continue
|
||||
|
||||
id_to_cards[userid] = cards
|
||||
|
||||
# Format the score and add it
|
||||
retval.append(self.__format_record(id_to_cards[userid], record))
|
||||
|
||||
return retval
|
||||
230
bemani/api/objects/statistics.py
Normal file
230
bemani/api/objects/statistics.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from typing import List, Dict, Tuple, Any
|
||||
|
||||
from bemani.api.exceptions import APIException
|
||||
from bemani.api.objects.base import BaseObject
|
||||
from bemani.common import APIConstants, DBConstants, GameConstants
|
||||
from bemani.data import Attempt, UserID
|
||||
|
||||
|
||||
class StatisticsObject(BaseObject):
|
||||
|
||||
def __format_statistics(self, stats: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
'cards': [],
|
||||
'song': str(stats['id']),
|
||||
'chart': str(stats['chart']),
|
||||
'plays': stats.get('plays', -1),
|
||||
'clears': stats.get('clears', -1),
|
||||
'combos': stats.get('combos', -1),
|
||||
}
|
||||
|
||||
def __format_user_statistics(self, cardids: List[str], stats: Dict[str, Any]) -> Dict[str, Any]:
|
||||
base = self.__format_statistics(stats)
|
||||
base['cards'] = cardids
|
||||
return base
|
||||
|
||||
@property
|
||||
def music_version(self) -> int:
|
||||
if self.game == GameConstants.IIDX:
|
||||
if self.omnimix:
|
||||
return self.version + DBConstants.OMNIMIX_VERSION_BUMP
|
||||
else:
|
||||
return self.version
|
||||
else:
|
||||
return self.version
|
||||
|
||||
def __is_play(self, attempt: Attempt) -> bool:
|
||||
if self.game in [
|
||||
GameConstants.DDR,
|
||||
GameConstants.JUBEAT,
|
||||
GameConstants.MUSECA,
|
||||
GameConstants.POPN_MUSIC,
|
||||
]:
|
||||
return True
|
||||
if self.game == GameConstants.IIDX:
|
||||
return attempt.data.get_int('clear_status') != DBConstants.IIDX_CLEAR_STATUS_NO_PLAY
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
return attempt.data.get_int('clear_type') != DBConstants.REFLEC_BEAT_CLEAR_TYPE_NO_PLAY
|
||||
if self.game == GameConstants.SDVX:
|
||||
return attempt.data.get_int('clear_type') != DBConstants.SDVX_CLEAR_TYPE_NO_PLAY
|
||||
|
||||
return False
|
||||
|
||||
def __is_clear(self, attempt: Attempt) -> bool:
|
||||
if not self.__is_play(attempt):
|
||||
return False
|
||||
|
||||
if self.game == GameConstants.DDR:
|
||||
return attempt.data.get_int('rank') != DBConstants.DDR_RANK_E
|
||||
if self.game == GameConstants.IIDX:
|
||||
return attempt.data.get_int('clear_status') != DBConstants.IIDX_CLEAR_STATUS_FAILED
|
||||
if self.game == GameConstants.JUBEAT:
|
||||
return attempt.data.get_int('medal') != DBConstants.JUBEAT_PLAY_MEDAL_FAILED
|
||||
if self.game == GameConstants.MUSECA:
|
||||
return attempt.data.get_int('clear_type') != DBConstants.MUSECA_CLEAR_TYPE_FAILED
|
||||
if self.game == GameConstants.POPN_MUSIC:
|
||||
return attempt.data.get_int('medal') not in [
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED,
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED,
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FAILED,
|
||||
]
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
return attempt.data.get_int('clear_type') != DBConstants.REFLEC_BEAT_CLEAR_TYPE_FAILED
|
||||
if self.game == GameConstants.SDVX:
|
||||
return (
|
||||
attempt.data.get_int('grade') != DBConstants.SDVX_GRADE_NO_PLAY and
|
||||
attempt.data.get_int('clear_type') not in [
|
||||
DBConstants.SDVX_CLEAR_TYPE_NO_PLAY,
|
||||
DBConstants.SDVX_CLEAR_TYPE_FAILED,
|
||||
]
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def __is_combo(self, attempt: Attempt) -> bool:
|
||||
if not self.__is_play(attempt):
|
||||
return False
|
||||
|
||||
if self.game == GameConstants.DDR:
|
||||
return attempt.data.get_int('halo') != DBConstants.DDR_HALO_NONE
|
||||
if self.game == GameConstants.IIDX:
|
||||
return attempt.data.get_int('clear_status') == DBConstants.IIDX_CLEAR_STATUS_FULL_COMBO
|
||||
if self.game == GameConstants.JUBEAT:
|
||||
return attempt.data.get_int('medal') in [
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_FULL_COMBO,
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_NEARLY_EXCELLENT,
|
||||
DBConstants.JUBEAT_PLAY_MEDAL_EXCELLENT,
|
||||
]
|
||||
if self.game == GameConstants.MUSECA:
|
||||
return attempt.data.get_int('clear_type') == DBConstants.MUSECA_CLEAR_TYPE_FULL_COMBO
|
||||
if self.game == GameConstants.POPN_MUSIC:
|
||||
return attempt.data.get_int('medal') in [
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FULL_COMBO,
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FULL_COMBO,
|
||||
DBConstants.POPN_MUSIC_PLAY_MEDAL_PERFECT,
|
||||
]
|
||||
if self.game == GameConstants.REFLEC_BEAT:
|
||||
return attempt.data.get_int('combo_type') in [
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO,
|
||||
DBConstants.REFLEC_BEAT_COMBO_TYPE_FULL_COMBO_ALL_JUST,
|
||||
]
|
||||
if self.game == GameConstants.SDVX:
|
||||
return attempt.data.get_int('clear_type') in [
|
||||
DBConstants.SDVX_CLEAR_TYPE_ULTIMATE_CHAIN,
|
||||
DBConstants.SDVX_CLEAR_TYPE_PERFECT_ULTIMATE_CHAIN,
|
||||
]
|
||||
|
||||
return False
|
||||
|
||||
def __aggregate_global(self, attempts: List[Attempt]) -> List[Dict[str, Any]]:
|
||||
stats: Dict[int, Dict[int, Dict[str, int]]] = {}
|
||||
|
||||
for attempt in attempts:
|
||||
if attempt.id not in stats:
|
||||
stats[attempt.id] = {}
|
||||
if attempt.chart not in stats[attempt.id]:
|
||||
stats[attempt.id][attempt.chart] = {
|
||||
'plays': 0,
|
||||
'clears': 0,
|
||||
'combos': 0,
|
||||
}
|
||||
|
||||
if self.__is_play(attempt):
|
||||
stats[attempt.id][attempt.chart]['plays'] += 1
|
||||
if self.__is_clear(attempt):
|
||||
stats[attempt.id][attempt.chart]['clears'] += 1
|
||||
if self.__is_combo(attempt):
|
||||
stats[attempt.id][attempt.chart]['combos'] += 1
|
||||
|
||||
retval = []
|
||||
for songid in stats:
|
||||
for songchart in stats[songid]:
|
||||
stat = stats[songid][songchart]
|
||||
stat['id'] = songid
|
||||
stat['chart'] = songchart
|
||||
retval.append(self.__format_statistics(stat))
|
||||
|
||||
return retval
|
||||
|
||||
def __aggregate_local(self, cards: Dict[int, List[str]], attempts: List[Tuple[UserID, Attempt]]) -> List[Dict[str, Any]]:
|
||||
stats: Dict[UserID, Dict[int, Dict[int, Dict[str, int]]]] = {}
|
||||
|
||||
for (userid, attempt) in attempts:
|
||||
if userid not in stats:
|
||||
stats[userid] = {}
|
||||
if attempt.id not in stats[userid]:
|
||||
stats[userid][attempt.id] = {}
|
||||
if attempt.chart not in stats[userid][attempt.id]:
|
||||
stats[userid][attempt.id][attempt.chart] = {
|
||||
'plays': 0,
|
||||
'clears': 0,
|
||||
'combos': 0,
|
||||
}
|
||||
|
||||
if self.__is_play(attempt):
|
||||
stats[userid][attempt.id][attempt.chart]['plays'] += 1
|
||||
if self.__is_clear(attempt):
|
||||
stats[userid][attempt.id][attempt.chart]['clears'] += 1
|
||||
if self.__is_combo(attempt):
|
||||
stats[userid][attempt.id][attempt.chart]['combos'] += 1
|
||||
|
||||
retval = []
|
||||
for userid in stats:
|
||||
for songid in stats[userid]:
|
||||
for songchart in stats[userid][songid]:
|
||||
stat = stats[userid][songid][songchart]
|
||||
stat['id'] = songid
|
||||
stat['chart'] = songchart
|
||||
retval.append(self.__format_user_statistics(cards[userid], stat))
|
||||
|
||||
return retval
|
||||
|
||||
def fetch_v1(self, idtype: str, ids: List[str], params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
retval: List[Dict[str, Any]] = []
|
||||
|
||||
# Fetch the attempts
|
||||
if idtype == APIConstants.ID_TYPE_SERVER:
|
||||
retval = self.__aggregate_global(
|
||||
[attempt[1] for attempt in self.data.local.music.get_all_attempts(self.game, self.music_version)]
|
||||
)
|
||||
elif idtype == APIConstants.ID_TYPE_SONG:
|
||||
if len(ids) == 1:
|
||||
songid = int(ids[0])
|
||||
chart = None
|
||||
else:
|
||||
songid = int(ids[0])
|
||||
chart = int(ids[1])
|
||||
retval = self.__aggregate_global(
|
||||
[attempt[1] for attempt in self.data.local.music.get_all_attempts(self.game, self.music_version, songid=songid, songchart=chart)]
|
||||
)
|
||||
elif idtype == APIConstants.ID_TYPE_INSTANCE:
|
||||
songid = int(ids[0])
|
||||
chart = int(ids[1])
|
||||
cardid = ids[2]
|
||||
userid = self.data.local.user.from_cardid(cardid)
|
||||
if userid is not None:
|
||||
retval = self.__aggregate_local(
|
||||
{userid: self.data.local.user.get_cards(userid)},
|
||||
self.data.local.music.get_all_attempts(self.game, self.music_version, songid=songid, songchart=chart, userid=userid)
|
||||
)
|
||||
elif idtype == APIConstants.ID_TYPE_CARD:
|
||||
id_to_cards: Dict[int, List[str]] = {}
|
||||
attempts: List[Tuple[UserID, Attempt]] = []
|
||||
for cardid in ids:
|
||||
userid = self.data.local.user.from_cardid(cardid)
|
||||
if userid is not None:
|
||||
# Don't duplicate loads for users with multiple card IDs if multiples
|
||||
# of those IDs are requested.
|
||||
if userid in id_to_cards:
|
||||
continue
|
||||
|
||||
id_to_cards[userid] = self.data.local.user.get_cards(userid)
|
||||
attempts.extend(
|
||||
self.data.local.music.get_all_attempts(self.game, self.music_version, userid=userid)
|
||||
)
|
||||
retval = self.__aggregate_local(id_to_cards, attempts)
|
||||
else:
|
||||
raise APIException('Invalid ID type!')
|
||||
|
||||
return retval
|
||||
Reference in New Issue
Block a user