# vim: set fileencoding=utf-8 import copy import random from typing import Any, Dict, List, Optional, Tuple from bemani.backend.base import Status from bemani.backend.jubeat.base import JubeatBase from bemani.backend.jubeat.common import ( JubeatDemodataGetHitchartHandler, JubeatDemodataGetNewsHandler, JubeatGamendRegisterHandler, JubeatGametopGetMeetingHandler, JubeatLobbyCheckHandler, JubeatLoggerReportHandler, ) from bemani.backend.jubeat.stubs import JubeatCopiousAppend from bemani.common import ValidatedDict, VersionConstants, Time from bemani.data import Data, Score, UserID from bemani.protocol import Node class JubeatSaucer( JubeatDemodataGetHitchartHandler, JubeatDemodataGetNewsHandler, JubeatGamendRegisterHandler, JubeatGametopGetMeetingHandler, JubeatLobbyCheckHandler, JubeatLoggerReportHandler, JubeatBase, ): name = 'Jubeat Saucer' version = VersionConstants.JUBEAT_SAUCER def previous_version(self) -> Optional[JubeatBase]: return JubeatCopiousAppend(self.data, self.config, self.model) @classmethod def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]: """ Insert daily FC challenges into the DB. """ events = [] if data.local.network.should_schedule(cls.game, cls.version, 'fc_challenge', 'daily'): # Generate a new list of two FC challenge songs. start_time, end_time = data.local.network.get_schedule_duration('daily') all_songs = set(song.id for song in data.local.music.get_all_songs(cls.game, cls.version)) today_song = random.sample(all_songs, 1)[0] data.local.game.put_time_sensitive_settings( cls.game, cls.version, 'fc_challenge', { 'start_time': start_time, 'end_time': end_time, 'today': today_song, }, ) events.append(( 'jubeat_fc_challenge_charts', { 'version': cls.version, 'today': today_song, }, )) # Mark that we did some actual work here. data.local.network.mark_scheduled(cls.game, cls.version, 'fc_challenge', 'daily') return events def handle_shopinfo_regist_request(self, request: Node) -> Node: # Update the name of this cab for admin purposes self.update_machine_name(request.child_value('shop/name')) shopinfo = Node.void('shopinfo') data = Node.void('data') shopinfo.add_child(data) data.add_child(Node.u32('cabid', 1)) data.add_child(Node.string('locationid', 'nowhere')) data.add_child(Node.u8('is_send', 1)) data.add_child(Node.s32_array( 'white_music_list', [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -16385, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], )) data.add_child(Node.u8('tax_phase', 1)) lab = Node.void('lab') data.add_child(lab) lab.add_child(Node.bool('is_open', False)) vocaloid_event = Node.void('vocaloid_event') data.add_child(vocaloid_event) vocaloid_event.add_child(Node.u8('state', 0)) vocaloid_event.add_child(Node.s32('music_id', 0)) matching_off = Node.void('matching_off') data.add_child(matching_off) matching_off.add_child(Node.bool('is_open', True)) return shopinfo def handle_gametop_regist_request(self, request: Node) -> Node: data = request.child('data') player = data.child('player') passnode = player.child('pass') refid = passnode.child_value('refid') name = player.child_value('name') root = self.new_profile_by_refid(refid, name) return root def handle_gametop_get_pdata_request(self, request: Node) -> Node: data = request.child('data') player = data.child('player') passnode = player.child('pass') refid = passnode.child_value('refid') root = self.get_profile_by_refid(refid) if root is None: root = Node.void('gametop') root.set_attribute('status', str(Status.NO_PROFILE)) return root def handle_gametop_get_mdata_request(self, request: Node) -> Node: data = request.child('data') player = data.child('player') extid = player.child_value('jid') root = self.get_scores_by_extid(extid) if root is None: root = Node.void('gametop') root.set_attribute('status', str(Status.NO_PROFILE)) return root def handle_gametop_get_rival_mdata_request(self, request: Node) -> Node: data = request.child('data') player = data.child('player') extid = player.child_value('rival') root = self.get_scores_by_extid(extid) if root is None: root = Node.void('gametop') root.set_attribute('status', str(Status.NO_PROFILE)) return root def format_profile(self, userid: UserID, profile: ValidatedDict) -> Node: root = Node.void('gametop') data = Node.void('data') root.add_child(data) player = Node.void('player') data.add_child(player) # Player info and statistics info = Node.void('info') player.add_child(info) info.add_child(Node.s16('jubility', profile.get_int('jubility'))) info.add_child(Node.s16('jubility_yday', profile.get_int('jubility_yday'))) info.add_child(Node.s32('tune_cnt', profile.get_int('tune_cnt'))) info.add_child(Node.s32('save_cnt', profile.get_int('save_cnt'))) info.add_child(Node.s32('saved_cnt', profile.get_int('saved_cnt'))) info.add_child(Node.s32('fc_cnt', profile.get_int('fc_cnt'))) info.add_child(Node.s32('ex_cnt', profile.get_int('ex_cnt'))) info.add_child(Node.s32('pf_cnt', profile.get_int('pf_cnt'))) info.add_child(Node.s32('clear_cnt', profile.get_int('clear_cnt'))) info.add_child(Node.s32('match_cnt', profile.get_int('match_cnt'))) info.add_child(Node.s32('beat_cnt', profile.get_int('beat_cnt'))) info.add_child(Node.s32('mynews_cnt', profile.get_int('mynews_cnt'))) if 'total_best_score' in profile: info.add_child(Node.s32('total_best_score', profile.get_int('total_best_score'))) # Looks to be set to true when there's an old profile, stops tutorial from # happening on first load. info.add_child(Node.bool('inherit', profile.get_bool('has_old_version'))) # Not saved, but loaded info.add_child(Node.s32('mtg_entry_cnt', 123)) info.add_child(Node.s32('mtg_hold_cnt', 456)) info.add_child(Node.u8('mtg_result', 10)) # Secret unlocks item = Node.void('item') player.add_child(item) item.add_child(Node.s32_array( 'secret_list', profile.get_int_array( 'secret_list', 32, [-1] * 32, ), )) item.add_child(Node.s32_array( 'title_list', profile.get_int_array( 'title_list', 96, [-1] * 96, ), )) item.add_child(Node.s16('theme_list', profile.get_int('theme_list', -1))) item.add_child(Node.s32_array('marker_list', profile.get_int_array('marker_list', 2, [-1] * 2))) item.add_child(Node.s32_array('parts_list', profile.get_int_array('parts_list', 96, [-1] * 96))) new = Node.void('new') item.add_child(new) new.add_child(Node.s32_array( 'secret_list', profile.get_int_array( 'secret_list_new', 32, [-1] * 32, ), )) new.add_child(Node.s32_array( 'title_list', profile.get_int_array( 'title_list_new', 96, [-1] * 96, ), )) new.add_child(Node.s16('theme_list', profile.get_int('theme_list_new', -1))) new.add_child(Node.s32_array('marker_list', profile.get_int_array('marker_list_new', 2, [-1] * 2))) # Last played data, for showing cursor and such lastdict = profile.get_dict('last') last = Node.void('last') player.add_child(last) last.add_child(Node.s32('music_id', lastdict.get_int('music_id'))) last.add_child(Node.s8('marker', lastdict.get_int('marker'))) last.add_child(Node.s16('title', lastdict.get_int('title'))) last.add_child(Node.s8('theme', lastdict.get_int('theme'))) last.add_child(Node.s8('sort', lastdict.get_int('sort'))) last.add_child(Node.s8('rank_sort', lastdict.get_int('rank_sort'))) last.add_child(Node.s8('combo_disp', lastdict.get_int('combo_disp'))) last.add_child(Node.s8('seq_id', lastdict.get_int('seq_id'))) last.add_child(Node.s16('parts', lastdict.get_int('parts'))) last.add_child(Node.s8('category', lastdict.get_int('category'))) last.add_child(Node.s64('play_time', lastdict.get_int('play_time'))) last.add_child(Node.string('shopname', lastdict.get_str('shopname'))) last.add_child(Node.string('areaname', lastdict.get_str('areaname'))) # Miscelaneous crap player.add_child(Node.s32('session_id', 1)) # Maybe hook this up? Unsure what it does, is it like IIDX dailies? today_music = Node.void('today_music') player.add_child(today_music) today_music.add_child(Node.s32('music_id', 0)) # No news, ever. news = Node.void('news') player.add_child(news) news.add_child(Node.s16('checked', 0)) # Add rivals to profile. rivallist = Node.void('rivallist') player.add_child(rivallist) links = self.data.local.user.get_links(self.game, self.version, userid) rivalcount = 0 for link in links: if link.type != 'rival': continue rprofile = self.get_profile(link.other_userid) if rprofile is None: continue rival = Node.void('rival') rivallist.add_child(rival) rival.add_child(Node.s32('jid', rprofile.get_int('extid'))) rival.add_child(Node.string('name', rprofile.get_str('name'))) # Lazy way of keeping track of rivals, since we can only have 4 # or the game with throw up. At least, I think Fulfill can have # 4 instead of the 3 found in newer versions, given the size of # the array that it loads the values in. However, to keep things # simple, I only supported three here. rivalcount += 1 if rivalcount >= 3: break rivallist.set_attribute('count', str(rivalcount)) # Unclear what this is. Looks related to Jubeat lab. mylist = Node.void('mylist') player.add_child(mylist) mylist.set_attribute('count', '0') # No collaboration support yet. collabo = Node.void('collabo') player.add_child(collabo) collabo.add_child(Node.bool('success', False)) collabo.add_child(Node.bool('completed', False)) # Daily FC challenge. entry = self.data.local.game.get_time_sensitive_settings(self.game, self.version, 'fc_challenge') if entry is None: entry = ValidatedDict() # Figure out if we've played these songs start_time, end_time = self.data.local.network.get_schedule_duration('daily') today_attempts = self.data.local.music.get_all_attempts(self.game, self.version, userid, entry.get_int('today', -1), timelimit=start_time) challenge = Node.void('challenge') player.add_child(challenge) today = Node.void('today') challenge.add_child(today) today.add_child(Node.s32('music_id', entry.get_int('today', -1))) today.add_child(Node.u8('state', 0x40 if len(today_attempts) > 0 else 0x0)) onlynow = Node.void('onlynow') challenge.add_child(onlynow) onlynow.add_child(Node.s32('magic_no', 0)) onlynow.add_child(Node.s16('cycle', 0)) # Bistro event bistro = Node.void('bistro') player.add_child(bistro) # Presumably these can affect the speed of the event info_1 = Node.void('info') bistro.add_child(info_1) info_1.add_child(Node.float('delicious_rate', 1.0)) info_1.add_child(Node.float('favorite_rate', 1.0)) bistro.add_child(Node.s32('carry_over', profile.get_int('bistro_carry_over'))) # Your chef dude, I guess? chefdict = profile.get_dict('chef') chef = Node.void('chef') bistro.add_child(chef) chef.add_child(Node.s32('id', chefdict.get_int('id', 1))) chef.add_child(Node.u8('ability', chefdict.get_int('ability', 2))) chef.add_child(Node.u8('remain', chefdict.get_int('remain', 30))) chef.add_child(Node.u8('rate', chefdict.get_int('rate', 1))) # Routes, similar to story mode in Pop'n I guess? routes = [ { 'id': 50000284, 'price': 20, 'satisfaction': 10, 'favorite': True, }, { 'id': 50000283, 'price': 20, 'satisfaction': 20, 'favorite': False, }, { 'id': 50000282, 'price': 30, 'satisfaction': 10, 'favorite': False, }, { 'id': 50000275, 'price': 10, 'satisfaction': 55, 'favorite': False, }, { 'id': 50000274, 'price': 40, 'satisfaction': 40, 'favorite': False, }, { 'id': 50000273, 'price': 80, 'satisfaction': 60, 'favorite': False, }, { 'id': 50000272, 'price': 70, 'satisfaction': 60, 'favorite': False, }, { 'id': 50000271, 'price': 90, 'satisfaction': 80, 'favorite': False, }, { 'id': 50000270, 'price': 90, 'satisfaction': 20, 'favorite': False, }, ] for route_no in range(len(routes)): routedata = routes[route_no] route = Node.void('route') bistro.add_child(route) route.set_attribute('no', str(route_no)) music = Node.void('music') route.add_child(music) music.add_child(Node.s32('id', routedata['id'])) music.add_child(Node.u16('price', routedata['price'])) music.add_child(Node.s32('price_s32', routedata['price'])) # Look up any updated satisfaction stored by the game routesaved = self.data.local.user.get_achievement(self.game, self.version, userid, route_no + 1, 'route') if routesaved is None: routesaved = ValidatedDict() satisfaction = routesaved.get_int('satisfaction', routedata['satisfaction']) gourmates = Node.void('gourmates') route.add_child(gourmates) gourmates.add_child(Node.s32('id', route_no + 1)) gourmates.add_child(Node.u8('favorite', 1 if routedata['favorite'] else 0)) gourmates.add_child(Node.u16('satisfaction', satisfaction)) gourmates.add_child(Node.s32('satisfaction_s32', satisfaction)) # Sane defaults for unknown nodes only_now_music = Node.void('only_now_music') player.add_child(only_now_music) only_now_music.set_attribute('count', '0') requested_music = Node.void('requested_music') player.add_child(requested_music) requested_music.set_attribute('count', '0') kac_music = Node.void('kac_music') player.add_child(kac_music) kac_music.set_attribute('count', '0') history = Node.void('history') player.add_child(history) history.set_attribute('count', '0') # Basic profile info player.add_child(Node.string('name', profile.get_str('name', 'なし'))) player.add_child(Node.s32('jid', profile.get_int('extid'))) player.add_child(Node.string('refid', profile.get_str('refid'))) # Miscelaneous history stuff data.add_child(Node.u8('termver', 16)) data.add_child(Node.u32('season_etime', 0)) data.add_child(Node.s32('bistro_last_music_id', 0)) data.add_child(Node.s32_array( 'white_music_list', [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], )) data.add_child(Node.s32_array( 'old_music_list', [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], )) data.add_child(Node.s32_array( 'open_music_list', [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], )) # Unsupported collaboration events with other games collabo_info = Node.void('collabo_info') data.add_child(collabo_info) # Unsupported marathon stuff run_run_marathon = Node.void('run_run_marathon') collabo_info.add_child(run_run_marathon) run_run_marathon.set_attribute('type', '1') run_run_marathon.add_child(Node.u8('state', 1)) run_run_marathon.add_child(Node.bool('is_report_end', True)) # Unsupported policy break stuff policy_break = Node.void('policy_break') collabo_info.add_child(policy_break) policy_break.set_attribute('type', '1') policy_break.add_child(Node.u8('state', 1)) policy_break.add_child(Node.bool('is_report_end', False)) # Unsupported vocaloid stuff vocaloid_event = Node.void('vocaloid_event') collabo_info.add_child(vocaloid_event) vocaloid_event.set_attribute('type', '1') vocaloid_event.add_child(Node.u8('state', 0)) vocaloid_event.add_child(Node.s32('music_id', 0)) # No obnoxious 30 second wait to play. matching_off = Node.void('matching_off') data.add_child(matching_off) matching_off.add_child(Node.bool('is_open', True)) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict: newprofile = copy.deepcopy(oldprofile) data = request.child('data') # Grab player information player = data.child('player') # Grab last information. Lots of this will be filled in while grabbing scores last = newprofile.get_dict('last') last.replace_int('play_time', player.child_value('time_gameend')) last.replace_str('shopname', player.child_value('shopname')) last.replace_str('areaname', player.child_value('areaname')) # Grab player info for echoing back info = player.child('info') if info is not None: newprofile.replace_int('jubility', info.child_value('jubility')) newprofile.replace_int('jubility_yday', info.child_value('jubility_yday')) newprofile.replace_int('tune_cnt', info.child_value('tune_cnt')) newprofile.replace_int('save_cnt', info.child_value('save_cnt')) newprofile.replace_int('saved_cnt', info.child_value('saved_cnt')) newprofile.replace_int('fc_cnt', info.child_value('fc_cnt')) newprofile.replace_int('ex_cnt', info.child_value('exc_cnt')) # Not a mistake, Jubeat is weird newprofile.replace_int('pf_cnt', info.child_value('pf_cnt')) newprofile.replace_int('clear_cnt', info.child_value('clear_cnt')) newprofile.replace_int('match_cnt', info.child_value('match_cnt')) newprofile.replace_int('beat_cnt', info.child_value('beat_cnt')) newprofile.replace_int('total_best_score', info.child_value('total_best_score')) newprofile.replace_int('mynews_cnt', info.child_value('mynews_cnt')) # Grab unlock progress item = player.child('item') if item is not None: newprofile.replace_int_array('secret_list', 32, item.child_value('secret_list')) newprofile.replace_int_array('title_list', 96, item.child_value('title_list')) newprofile.replace_int('theme_list', item.child_value('theme_list')) newprofile.replace_int_array('marker_list', 2, item.child_value('marker_list')) newprofile.replace_int_array('parts_list', 96, item.child_value('parts_list')) newprofile.replace_int_array('secret_list_new', 32, item.child_value('secret_new')) newprofile.replace_int_array('title_list_new', 96, item.child_value('title_new')) newprofile.replace_int('theme_list_new', item.child_value('theme_new')) newprofile.replace_int_array('marker_list_new', 2, item.child_value('marker_new')) # Grab bistro progress bistro = player.child('bistro') if bistro is not None: newprofile.replace_int('bistro_carry_over', bistro.child_value('carry_over')) chefdata = newprofile.get_dict('chef') chef = bistro.child('chef') if chef is not None: chefdata.replace_int('id', chef.child_value('id')) chefdata.replace_int('ability', chef.child_value('ability')) chefdata.replace_int('remain', chef.child_value('remain')) chefdata.replace_int('rate', chef.child_value('rate')) newprofile.replace_dict('chef', chefdata) for route in bistro.children: if route.name != 'route': continue gourmates = route.child('gourmates') routeid = gourmates.child_value('id') satisfaction = gourmates.child_value('satisfaction_s32') self.data.local.user.put_achievement( self.game, self.version, userid, routeid, 'route', { 'satisfaction': satisfaction, }, ) # Get timestamps for played songs timestamps: Dict[int, int] = {} history = player.child('history') if history is not None: for tune in history.children: if tune.name != 'tune': continue entry = int(tune.attribute('log_id')) ts = int(tune.child_value('timestamp') / 1000) timestamps[entry] = ts # Grab scores and save those result = data.child('result') if result is not None: for tune in result.children: if tune.name != 'tune': continue result = tune.child('player') last.replace_int('marker', tune.child_value('marker')) last.replace_int('title', tune.child_value('title')) last.replace_int('parts', tune.child_value('parts')) last.replace_int('theme', tune.child_value('theme')) last.replace_int('sort', tune.child_value('sort')) last.replace_int('category', tune.child_value('category')) last.replace_int('rank_sort', tune.child_value('rank_sort')) last.replace_int('combo_disp', tune.child_value('combo_disp')) songid = tune.child_value('music') entry = int(tune.attribute('id')) timestamp = timestamps.get(entry, Time.now()) chart = int(result.child('score').attribute('seq')) points = result.child_value('score') flags = int(result.child('score').attribute('clear')) combo = int(result.child('score').attribute('combo')) ghost = result.child_value('mbar') # Miscelaneous last data for echoing to profile get last.replace_int('music_id', songid) last.replace_int('seq_id', chart) mapping = { self.GAME_FLAG_BIT_CLEARED: self.PLAY_MEDAL_CLEARED, self.GAME_FLAG_BIT_FULL_COMBO: self.PLAY_MEDAL_FULL_COMBO, self.GAME_FLAG_BIT_EXCELLENT: self.PLAY_MEDAL_EXCELLENT, self.GAME_FLAG_BIT_NEARLY_FULL_COMBO: self.PLAY_MEDAL_NEARLY_FULL_COMBO, self.GAME_FLAG_BIT_NEARLY_EXCELLENT: self.PLAY_MEDAL_NEARLY_EXCELLENT, } # Figure out the highest medal based on bits passed in medal = self.PLAY_MEDAL_FAILED for bit in mapping: if flags & bit > 0: medal = max(medal, mapping[bit]) self.update_score(userid, timestamp, songid, chart, points, medal, combo, ghost) # Save back last information gleaned from results newprofile.replace_dict('last', last) # Keep track of play statistics self.update_play_statistics(userid) return newprofile def format_scores(self, userid: UserID, profile: ValidatedDict, scores: List[Score]) -> Node: scores = self.data.remote.music.get_scores(self.game, self.version, userid) root = Node.void('gametop') datanode = Node.void('data') root.add_child(datanode) player = Node.void('player') datanode.add_child(player) player.add_child(Node.s32('jid', profile.get_int('extid'))) playdata = Node.void('playdata') player.add_child(playdata) playdata.set_attribute('count', str(len(scores))) music = ValidatedDict() for score in scores: data = music.get_dict(str(score.id)) play_cnt = data.get_int_array('play_cnt', 3) clear_cnt = data.get_int_array('clear_cnt', 3) clear_flags = data.get_int_array('clear_flags', 3) fc_cnt = data.get_int_array('fc_cnt', 3) ex_cnt = data.get_int_array('ex_cnt', 3) points = data.get_int_array('points', 3) # Replace data for this chart type play_cnt[score.chart] = score.plays clear_cnt[score.chart] = score.data.get_int('clear_count') fc_cnt[score.chart] = score.data.get_int('full_combo_count') ex_cnt[score.chart] = score.data.get_int('excellent_count') points[score.chart] = score.points # Format the clear flags clear_flags[score.chart] = self.GAME_FLAG_BIT_PLAYED if score.data.get_int('clear_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_CLEARED if score.data.get_int('full_combo_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_FULL_COMBO if score.data.get_int('excellent_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_EXCELLENT # Save chart data back data.replace_int_array('play_cnt', 3, play_cnt) data.replace_int_array('clear_cnt', 3, clear_cnt) data.replace_int_array('clear_flags', 3, clear_flags) data.replace_int_array('fc_cnt', 3, fc_cnt) data.replace_int_array('ex_cnt', 3, ex_cnt) data.replace_int_array('points', 3, points) # Update the ghost (untyped) ghost = data.get('ghost', [None, None, None]) ghost[score.chart] = score.data.get('ghost') data['ghost'] = ghost # Save it back music.replace_dict(str(score.id), data) for scoreid in music: scoredata = music[scoreid] musicdata = Node.void('musicdata') playdata.add_child(musicdata) musicdata.set_attribute('music_id', scoreid) musicdata.add_child(Node.s32_array('play_cnt', scoredata.get_int_array('play_cnt', 3))) musicdata.add_child(Node.s32_array('clear_cnt', scoredata.get_int_array('clear_cnt', 3))) musicdata.add_child(Node.s32_array('fc_cnt', scoredata.get_int_array('fc_cnt', 3))) musicdata.add_child(Node.s32_array('ex_cnt', scoredata.get_int_array('ex_cnt', 3))) musicdata.add_child(Node.s32_array('score', scoredata.get_int_array('points', 3))) musicdata.add_child(Node.s8_array('clear', scoredata.get_int_array('clear_flags', 3))) ghosts = scoredata.get('ghost', [None, None, None]) for i in range(len(ghosts)): ghost = ghosts[i] if ghost is None: continue bar = Node.u8_array('bar', ghost) musicdata.add_child(bar) bar.set_attribute('seq', str(i)) return root