1
0
mirror of synced 2024-12-22 10:55:53 +01:00
bemaniutils/bemani/frontend/jubeat/jubeat.py
Jennifer Taylor 9feae49667 Massive overhaul to Jubeat frontend, including the following:
- Clan jubility breakdown
 - Displaying music rate and stats identically across all score pages
 - Ability to choose what jubility (legacy or new) to display and sort on all players.
 - Various tweaks and small quality of life improvements.
2022-10-08 01:32:59 +00:00

242 lines
11 KiB
Python

# vim: set fileencoding=utf-8
from typing import Any, Dict, Iterator, List, Optional, Tuple
from bemani.backend.jubeat import JubeatFactory, JubeatBase
from bemani.common import Profile, ValidatedDict, GameConstants, VersionConstants
from bemani.data import Attempt, Score, Song, UserID
from bemani.frontend.base import FrontendBase
class JubeatFrontend(FrontendBase):
game: GameConstants = GameConstants.JUBEAT
valid_charts: List[int] = [
JubeatBase.CHART_TYPE_BASIC,
JubeatBase.CHART_TYPE_ADVANCED,
JubeatBase.CHART_TYPE_EXTREME,
JubeatBase.CHART_TYPE_HARD_BASIC,
JubeatBase.CHART_TYPE_HARD_ADVANCED,
JubeatBase.CHART_TYPE_HARD_EXTREME,
]
valid_rival_types: List[str] = ['rival']
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:
yield from JubeatFactory.all_games()
def sanitized_games(self) -> Iterator[Tuple[GameConstants, int, str]]:
mapping = {
VersionConstants.JUBEAT: 1,
VersionConstants.JUBEAT_RIPPLES: 2,
VersionConstants.JUBEAT_KNIT: 3,
VersionConstants.JUBEAT_COPIOUS: 4,
VersionConstants.JUBEAT_SAUCER: 5,
VersionConstants.JUBEAT_PROP: 6,
VersionConstants.JUBEAT_QUBELL: 7,
VersionConstants.JUBEAT_CLAN: 8,
VersionConstants.JUBEAT_FESTO: 9,
}
for (game, version, name) in self.all_games():
if version in mapping:
yield (game, mapping[version], name)
def get_duplicate_id(self, musicid: int, chart: int) -> Optional[Tuple[int, int]]:
# In qubell and clan omnimix, PPAP and Bonjour the world are placed
# at this arbitrary songid since they weren't assigned one originally
# In jubeat festo, these songs were given proper songids so we need to account for this
legacy_to_modern_map = {
71000001: 70000124, # PPAP
71000002: 70000154, # Bonjour the world
50000020: 80000037, # 千本桜 was removed and then revived in clan
60000063: 70000100, # Khamen break sdvx had the first id for prop(never released officially)
}
oldid = legacy_to_modern_map.get(musicid)
oldchart = chart
if oldid is not None:
return (oldid, oldchart)
else:
return None
def get_all_items(self, versions: list) -> Dict[str, List[Dict[str, Any]]]:
result = {}
for version in versions:
emblem = self.__format_jubeat_extras(version)
result[version] = emblem['emblems']
return result
def __format_jubeat_extras(self, version: int) -> Dict[str, List[Dict[str, Any]]]:
# Gotta look up the unlock catalog
items = self.data.local.game.get_items(self.game, version)
# Format it depending on the version
if version in {
VersionConstants.JUBEAT_PROP,
VersionConstants.JUBEAT_QUBELL,
VersionConstants.JUBEAT_CLAN,
VersionConstants.JUBEAT_FESTO,
}:
return {
"emblems": [
{
"index": str(item.id),
"song": item.data.get_int("music_id"),
"layer": item.data.get_int("layer"),
"evolved": item.data.get_int("evolved"),
"rarity": item.data.get_int("rarity"),
"name": item.data.get_str("name"),
}
for item in items
if item.type == "emblem"
],
}
else:
return {"emblems": []}
def format_score(self, userid: UserID, score: Score) -> Dict[str, Any]:
formatted_score = super().format_score(userid, score)
formatted_score['combo'] = score.data.get_int('combo', -1)
formatted_score['music_rate'] = score.data.get_int('music_rate', -1)
if formatted_score['music_rate'] >= 0:
formatted_score['music_rate'] /= 10
formatted_score['medal'] = score.data.get_int('medal')
formatted_score['status'] = {
JubeatBase.PLAY_MEDAL_FAILED: "FAILED",
JubeatBase.PLAY_MEDAL_CLEARED: "CLEARED",
JubeatBase.PLAY_MEDAL_NEARLY_FULL_COMBO: "NEARLY FULL COMBO",
JubeatBase.PLAY_MEDAL_FULL_COMBO: "FULL COMBO",
JubeatBase.PLAY_MEDAL_NEARLY_EXCELLENT: "NEARLY EXCELLENT",
JubeatBase.PLAY_MEDAL_EXCELLENT: "EXCELLENT",
}.get(score.data.get_int('medal'), 'NO PLAY')
formatted_score['music_rate'] = score.data.get_int('music_rate', 0) / 10
formatted_score['clear_cnt'] = score.data.get_int('clear_count', 0)
formatted_score['stats'] = score.data.get_dict('stats')
return formatted_score
def format_attempt(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]:
formatted_attempt = super().format_attempt(userid, attempt)
formatted_attempt['combo'] = attempt.data.get_int('combo', -1)
formatted_attempt['medal'] = attempt.data.get_int('medal')
formatted_attempt['music_rate'] = attempt.data.get_int('music_rate', -1)
if formatted_attempt['music_rate'] >= 0:
formatted_attempt['music_rate'] /= 10
formatted_attempt['status'] = {
JubeatBase.PLAY_MEDAL_FAILED: "FAILED",
JubeatBase.PLAY_MEDAL_CLEARED: "CLEARED",
JubeatBase.PLAY_MEDAL_NEARLY_FULL_COMBO: "NEARLY FULL COMBO",
JubeatBase.PLAY_MEDAL_FULL_COMBO: "FULL COMBO",
JubeatBase.PLAY_MEDAL_NEARLY_EXCELLENT: "NEARLY EXCELLENT",
JubeatBase.PLAY_MEDAL_EXCELLENT: "EXCELLENT",
}.get(attempt.data.get_int('medal'), 'NO PLAY')
formatted_attempt['music_rate'] = attempt.data.get_int('music_rate', 0) / 10
formatted_attempt['stats'] = attempt.data.get_dict('stats')
return formatted_attempt
def format_emblem(self, emblem: list) -> Dict[str, Any]:
return {
'background': emblem[0],
'main': emblem[1],
'ornament': emblem[2],
'effect': emblem[3],
'speech_bubble': emblem[4],
}
def format_profile(self, profile: Profile, playstats: ValidatedDict) -> Dict[str, Any]:
formatted_profile = super().format_profile(profile, playstats)
formatted_profile['plays'] = playstats.get_int('total_plays')
formatted_profile['emblem'] = self.format_emblem(profile.get_dict('last').get_int_array('emblem', 5))
formatted_profile['jubility'] = (
profile.get_int('jubility')
if profile.version not in {VersionConstants.JUBEAT_PROP, VersionConstants.JUBEAT_QUBELL, VersionConstants.JUBEAT_FESTO}
else 0
)
formatted_profile['pick_up_jubility'] = (
profile.get_float('pick_up_jubility')
if profile.version == VersionConstants.JUBEAT_FESTO
else 0
)
formatted_profile['common_jubility'] = (
profile.get_float('common_jubility')
if profile.version == VersionConstants.JUBEAT_FESTO
else 0
)
if profile.version == VersionConstants.JUBEAT_FESTO:
# Only reason this is a dictionary of dictionaries is because ValidatedDict doesn't support a list of dictionaries.
# Probably intentionally lol. Just listify the pickup/common charts.
formatted_profile['pick_up_chart'] = list(profile.get_dict('pick_up_chart').values())
formatted_profile['common_chart'] = list(profile.get_dict('common_chart').values())
elif profile.version == VersionConstants.JUBEAT_CLAN:
# Look up achievements which is where jubility was stored. This is a bit of a hack
# due to the fact that this could be formatting remote profiles, but then they should
# have no achievements.
userid = self.data.local.user.from_refid(profile.game, profile.version, profile.refid)
if userid is not None:
achievements = self.data.local.user.get_achievements(profile.game, profile.version, userid)
else:
achievements = []
jubeat_entries: List[ValidatedDict] = []
for achievement in achievements:
if achievement.type != 'jubility':
continue
# Figure out for each song, what's the highest value jubility and
# keep that.
bestentry = ValidatedDict()
for chart in [0, 1, 2]:
entry = achievement.data.get_dict(str(chart))
if entry.get_int("value") >= bestentry.get_int("value"):
bestentry = entry.clone()
bestentry.replace_int("songid", achievement.id)
bestentry.replace_int("chart", chart)
jubeat_entries.append(bestentry)
jubeat_entries = sorted(jubeat_entries, key=lambda entry: entry.get_int("value"), reverse=True)[:30]
formatted_profile['chart'] = jubeat_entries
formatted_profile['ex_count'] = profile.get_int('ex_cnt')
formatted_profile['fc_count'] = profile.get_int('fc_cnt')
return formatted_profile
def format_song(self, song: Song) -> Dict[str, Any]:
difficulties = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
difficulties[song.chart] = song.data.get_float('difficulty', 13)
if difficulties[song.chart] == 13.0:
difficulties[song.chart] = float(song.data.get_int('difficulty', 13))
formatted_song = super().format_song(song)
formatted_song['bpm_min'] = song.data.get_int('bpm_min', 120)
formatted_song['bpm_max'] = song.data.get_int('bpm_max', 120)
formatted_song['difficulties'] = difficulties
version = song.data.get_int('version', 0)
if version == 0:
# The default here is a nasty hack for installations that existed prior to importing
# version using read.py. This ensures that not importing again won't break existing
# installations.
formatted_song['version'] = int(song.id / 10000000)
else:
formatted_song['version'] = {
VersionConstants.JUBEAT: 1,
VersionConstants.JUBEAT_RIPPLES: 2,
VersionConstants.JUBEAT_RIPPLES_APPEND: 2,
VersionConstants.JUBEAT_KNIT: 3,
VersionConstants.JUBEAT_KNIT_APPEND: 3,
VersionConstants.JUBEAT_COPIOUS: 4,
VersionConstants.JUBEAT_COPIOUS_APPEND: 4,
VersionConstants.JUBEAT_SAUCER: 5,
VersionConstants.JUBEAT_SAUCER_FULFILL: 5,
VersionConstants.JUBEAT_PROP: 6,
VersionConstants.JUBEAT_QUBELL: 7,
VersionConstants.JUBEAT_CLAN: 8,
VersionConstants.JUBEAT_FESTO: 9,
}[version]
return formatted_song
def merge_song(self, existing: Dict[str, Any], new: Song) -> Dict[str, Any]:
new_song = super().merge_song(existing, new)
if existing['difficulties'][new.chart] == 0.0:
new_song['difficulties'][new.chart] = new.data.get_float('difficulty', 13)
if new_song['difficulties'][new.chart] == 13.0:
new_song['difficulties'][new.chart] = float(new.data.get_int('difficulty', 13))
return new_song