# vim: set fileencoding=utf-8 import base64 from typing import Any, Dict, List, Optional, Tuple from typing_extensions import Final from bemani.backend.ess import EventLogHandler from bemani.backend.ddr.base import DDRBase from bemani.backend.ddr.ddr2014 import DDR2014 from bemani.common import Profile, ValidatedDict, VersionConstants, CardCipher, Time, ID, intish from bemani.data import Data, Achievement, Machine, Score, UserID from bemani.protocol import Node class DDRAce( DDRBase, EventLogHandler, ): name: str = 'DanceDanceRevolution A' version: int = VersionConstants.DDR_ACE GAME_STYLE_SINGLE: Final[int] = 0 GAME_STYLE_DOUBLE: Final[int] = 1 GAME_STYLE_VERSUS: Final[int] = 2 GAME_RIVAL_TYPE_RIVAL3: Final[int] = 32 GAME_RIVAL_TYPE_RIVAL2: Final[int] = 16 GAME_RIVAL_TYPE_RIVAL1: Final[int] = 8 GAME_RIVAL_TYPE_WORLD: Final[int] = 4 GAME_RIVAL_TYPE_AREA: Final[int] = 2 GAME_RIVAL_TYPE_MACHINE: Final[int] = 1 GAME_CHART_SINGLE_BEGINNER: Final[int] = 0 GAME_CHART_SINGLE_BASIC: Final[int] = 1 GAME_CHART_SINGLE_DIFFICULT: Final[int] = 2 GAME_CHART_SINGLE_EXPERT: Final[int] = 3 GAME_CHART_SINGLE_CHALLENGE: Final[int] = 4 GAME_CHART_DOUBLE_BASIC: Final[int] = 5 GAME_CHART_DOUBLE_DIFFICULT: Final[int] = 6 GAME_CHART_DOUBLE_EXPERT: Final[int] = 7 GAME_CHART_DOUBLE_CHALLENGE: Final[int] = 8 GAME_HALO_NONE: Final[int] = 6 GAME_HALO_GOOD_COMBO: Final[int] = 7 GAME_HALO_GREAT_COMBO: Final[int] = 8 GAME_HALO_PERFECT_COMBO: Final[int] = 9 GAME_HALO_MARVELOUS_COMBO: Final[int] = 10 GAME_RANK_E: Final[int] = 15 GAME_RANK_D: Final[int] = 14 GAME_RANK_D_PLUS: Final[int] = 13 GAME_RANK_C_MINUS: Final[int] = 12 GAME_RANK_C: Final[int] = 11 GAME_RANK_C_PLUS: Final[int] = 10 GAME_RANK_B_MINUS: Final[int] = 9 GAME_RANK_B: Final[int] = 8 GAME_RANK_B_PLUS: Final[int] = 7 GAME_RANK_A_MINUS: Final[int] = 6 GAME_RANK_A: Final[int] = 5 GAME_RANK_A_PLUS: Final[int] = 4 GAME_RANK_AA_MINUS: Final[int] = 3 GAME_RANK_AA: Final[int] = 2 GAME_RANK_AA_PLUS: Final[int] = 1 GAME_RANK_AAA: Final[int] = 0 GAME_MAX_SONGS: Final[int] = 1024 GAME_COMMON_AREA_OFFSET: Final[int] = 1 GAME_COMMON_WEIGHT_DISPLAY_OFFSET: Final[int] = 3 GAME_COMMON_CHARACTER_OFFSET: Final[int] = 4 GAME_COMMON_EXTRA_CHARGE_OFFSET: Final[int] = 5 GAME_COMMON_TOTAL_PLAYS_OFFSET: Final[int] = 9 GAME_COMMON_SINGLE_PLAYS_OFFSET: Final[int] = 11 GAME_COMMON_DOUBLE_PLAYS_OFFSET: Final[int] = 12 GAME_COMMON_WEIGHT_OFFSET: Final[int] = 17 GAME_COMMON_NAME_OFFSET: Final[int] = 25 GAME_COMMON_SEQ_OFFSET: Final[int] = 26 GAME_OPTION_SPEED_OFFSET: Final[int] = 1 GAME_OPTION_BOOST_OFFSET: Final[int] = 2 GAME_OPTION_APPEARANCE_OFFSET: Final[int] = 3 GAME_OPTION_TURN_OFFSET: Final[int] = 4 GAME_OPTION_STEP_ZONE_OFFSET: Final[int] = 5 GAME_OPTION_SCROLL_OFFSET: Final[int] = 6 GAME_OPTION_ARROW_COLOR_OFFSET: Final[int] = 7 GAME_OPTION_CUT_OFFSET: Final[int] = 8 GAME_OPTION_FREEZE_OFFSET: Final[int] = 9 GAME_OPTION_JUMPS_OFFSET: Final[int] = 10 GAME_OPTION_ARROW_SKIN_OFFSET: Final[int] = 11 GAME_OPTION_FILTER_OFFSET: Final[int] = 12 GAME_OPTION_GUIDELINE_OFFSET: Final[int] = 13 GAME_OPTION_GAUGE_OFFSET: Final[int] = 14 GAME_OPTION_COMBO_POSITION_OFFSET: Final[int] = 15 GAME_OPTION_FAST_SLOW_OFFSET: Final[int] = 16 GAME_LAST_CALORIES_OFFSET: Final[int] = 10 GAME_RIVAL_SLOT_1_ACTIVE_OFFSET: Final[int] = 1 GAME_RIVAL_SLOT_2_ACTIVE_OFFSET: Final[int] = 2 GAME_RIVAL_SLOT_3_ACTIVE_OFFSET: Final[int] = 3 GAME_RIVAL_SLOT_1_DDRCODE_OFFSET: Final[int] = 9 GAME_RIVAL_SLOT_2_DDRCODE_OFFSET: Final[int] = 10 GAME_RIVAL_SLOT_3_DDRCODE_OFFSET: Final[int] = 11 def previous_version(self) -> Optional[DDRBase]: return DDR2014(self.data, self.config, self.model) @classmethod def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]: # DDR Ace has a weird bug where it sends a profile save for a blank # profile before reading it back when creating a new profile. If there # is no profile on read-back, it errors out, and it also uses the name # and area ID as the takeover/succession data if the user had previous # data on an old game. However, if for some reason the user cancels out # of the name entry, loses power or disconnects from the network at the # right time, then the profile exists in a broken state forever until they # edit it on the front-end. As a work-around to this, we remember the last # time each profile was written to, and we look up profiles that are older # than a few minutes (the maximum possible time for DDR Ace to write back # a new profile after creating a blank one) and have blank names and delete # them in order to keep the profiles on the network in sane order. This # should normally never delete any profiles. profiles = data.local.user.get_all_profiles(cls.game, cls.version) several_minutes_ago = Time.now() - (Time.SECONDS_IN_MINUTE * 5) events = [] for userid, profile in profiles: if profile.get_str('name') == "" and profile.get_int('write_time') < several_minutes_ago: data.local.user.delete_profile(cls.game, cls.version, userid) events.append(( 'ddr_profile_purge', { 'userid': userid, }, )) return events @property def supports_paseli(self) -> bool: if self.model.dest != 'J': # DDR Ace in USA mode doesn't support PASELI properly. # When in Asia mode it shows PASELI but won't let you select it. return False else: # All other modes should work with PASELI. return True def game_to_db_rank(self, game_rank: int) -> int: return { self.GAME_RANK_AAA: self.RANK_AAA, self.GAME_RANK_AA_PLUS: self.RANK_AA_PLUS, self.GAME_RANK_AA: self.RANK_AA, self.GAME_RANK_AA_MINUS: self.RANK_AA_MINUS, self.GAME_RANK_A_PLUS: self.RANK_A_PLUS, self.GAME_RANK_A: self.RANK_A, self.GAME_RANK_A_MINUS: self.RANK_A_MINUS, self.GAME_RANK_B_PLUS: self.RANK_B_PLUS, self.GAME_RANK_B: self.RANK_B, self.GAME_RANK_B_MINUS: self.RANK_B_MINUS, self.GAME_RANK_C_PLUS: self.RANK_C_PLUS, self.GAME_RANK_C: self.RANK_C, self.GAME_RANK_C_MINUS: self.RANK_C_MINUS, self.GAME_RANK_D_PLUS: self.RANK_D_PLUS, self.GAME_RANK_D: self.RANK_D, self.GAME_RANK_E: self.RANK_E, }[game_rank] def db_to_game_rank(self, db_rank: int) -> int: return { self.RANK_AAA: self.GAME_RANK_AAA, self.RANK_AA_PLUS: self.GAME_RANK_AA_PLUS, self.RANK_AA: self.GAME_RANK_AA, self.RANK_AA_MINUS: self.GAME_RANK_AA_MINUS, self.RANK_A_PLUS: self.GAME_RANK_A_PLUS, self.RANK_A: self.GAME_RANK_A, self.RANK_A_MINUS: self.GAME_RANK_A_MINUS, self.RANK_B_PLUS: self.GAME_RANK_B_PLUS, self.RANK_B: self.GAME_RANK_B, self.RANK_B_MINUS: self.GAME_RANK_B_MINUS, self.RANK_C_PLUS: self.GAME_RANK_C_PLUS, self.RANK_C: self.GAME_RANK_C, self.RANK_C_MINUS: self.GAME_RANK_C_MINUS, self.RANK_D_PLUS: self.GAME_RANK_D_PLUS, self.RANK_D: self.GAME_RANK_D, self.RANK_E: self.GAME_RANK_E, }[db_rank] def game_to_db_chart(self, game_chart: int) -> int: return { self.GAME_CHART_SINGLE_BEGINNER: self.CHART_SINGLE_BEGINNER, self.GAME_CHART_SINGLE_BASIC: self.CHART_SINGLE_BASIC, self.GAME_CHART_SINGLE_DIFFICULT: self.CHART_SINGLE_DIFFICULT, self.GAME_CHART_SINGLE_EXPERT: self.CHART_SINGLE_EXPERT, self.GAME_CHART_SINGLE_CHALLENGE: self.CHART_SINGLE_CHALLENGE, self.GAME_CHART_DOUBLE_BASIC: self.CHART_DOUBLE_BASIC, self.GAME_CHART_DOUBLE_DIFFICULT: self.CHART_DOUBLE_DIFFICULT, self.GAME_CHART_DOUBLE_EXPERT: self.CHART_DOUBLE_EXPERT, self.GAME_CHART_DOUBLE_CHALLENGE: self.CHART_DOUBLE_CHALLENGE, }[game_chart] def db_to_game_chart(self, db_chart: int) -> int: return { self.CHART_SINGLE_BEGINNER: self.GAME_CHART_SINGLE_BEGINNER, self.CHART_SINGLE_BASIC: self.GAME_CHART_SINGLE_BASIC, self.CHART_SINGLE_DIFFICULT: self.GAME_CHART_SINGLE_DIFFICULT, self.CHART_SINGLE_EXPERT: self.GAME_CHART_SINGLE_EXPERT, self.CHART_SINGLE_CHALLENGE: self.GAME_CHART_SINGLE_CHALLENGE, self.CHART_DOUBLE_BASIC: self.GAME_CHART_DOUBLE_BASIC, self.CHART_DOUBLE_DIFFICULT: self.GAME_CHART_DOUBLE_DIFFICULT, self.CHART_DOUBLE_EXPERT: self.GAME_CHART_DOUBLE_EXPERT, self.CHART_DOUBLE_CHALLENGE: self.GAME_CHART_DOUBLE_CHALLENGE, }[db_chart] def game_to_db_halo(self, game_halo: int) -> int: if game_halo == self.GAME_HALO_MARVELOUS_COMBO: return self.HALO_MARVELOUS_FULL_COMBO elif game_halo == self.GAME_HALO_PERFECT_COMBO: return self.HALO_PERFECT_FULL_COMBO elif game_halo == self.GAME_HALO_GREAT_COMBO: return self.HALO_GREAT_FULL_COMBO elif game_halo == self.GAME_HALO_GOOD_COMBO: return self.HALO_GOOD_FULL_COMBO else: return self.HALO_NONE def db_to_game_halo(self, db_halo: int) -> int: if db_halo == self.HALO_MARVELOUS_FULL_COMBO: return self.GAME_HALO_MARVELOUS_COMBO elif db_halo == self.HALO_PERFECT_FULL_COMBO: return self.GAME_HALO_PERFECT_COMBO elif db_halo == self.HALO_GREAT_FULL_COMBO: return self.GAME_HALO_GREAT_COMBO elif db_halo == self.HALO_GOOD_FULL_COMBO: return self.GAME_HALO_GOOD_COMBO else: return self.GAME_HALO_NONE def handle_tax_get_phase_request(self, request: Node) -> Node: tax = Node.void('tax') tax.add_child(Node.s32('phase', 0)) return tax def __handle_userload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: has_profile: bool = False achievements: List[Achievement] = [] scores: List[Score] = [] if userid is not None: has_profile = self.has_profile(userid) achievements = self.data.local.user.get_achievements(self.game, self.version, userid) scores = self.data.remote.music.get_scores(self.game, self.music_version, userid) # Place scores into an arrangement for easier distribution to Ace. scores_by_mcode: Dict[int, List[Optional[Score]]] = {} for score in scores: if score.id not in scores_by_mcode: scores_by_mcode[score.id] = [None] * 9 scores_by_mcode[score.id][self.db_to_game_chart(score.chart)] = score # First, set new flag response.add_child(Node.bool('is_new', not has_profile)) # Now, return the scores to Ace for mcode in scores_by_mcode: music = Node.void('music') response.add_child(music) music.add_child(Node.u32('mcode', mcode)) scores_that_matter = scores_by_mcode[mcode] while scores_that_matter[-1] is None: scores_that_matter = scores_that_matter[:-1] for score in scores_that_matter: note = Node.void('note') music.add_child(note) if score is None: note.add_child(Node.u16('count', 0)) note.add_child(Node.u8('rank', 0)) note.add_child(Node.u8('clearkind', 0)) note.add_child(Node.s32('score', 0)) note.add_child(Node.s32('ghostid', 0)) else: note.add_child(Node.u16('count', score.plays)) note.add_child(Node.u8('rank', self.db_to_game_rank(score.data.get_int('rank')))) note.add_child(Node.u8('clearkind', self.db_to_game_halo(score.data.get_int('halo')))) note.add_child(Node.s32('score', score.points)) note.add_child(Node.s32('ghostid', score.key)) # Active event settings activeevents = [ 1, 3, 5, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, ] # Event reward settings rewards = { '30': { 999: 5, } } # Now handle event progress and activation. events = {ach.id: ach.data for ach in achievements if ach.type == '9999'} progress = [ach for ach in achievements if ach.type != '9999'] # Make sure we always send a babylon's adventure save event or the game won't send progress babylon_included = False for evtprogress in progress: if evtprogress.id == 999 and evtprogress.type == '30': babylon_included = True break if not babylon_included: progress.append(Achievement( 999, '30', None, { 'completed': False, 'progress': 0, }, )) for event in activeevents: # Get completion data playerstats = events.get(event, ValidatedDict({'completed': False})) # Return the data eventdata = Node.void('eventdata') response.add_child(eventdata) eventdata.add_child(Node.u32('eventid', event)) eventdata.add_child(Node.s32('eventtype', 9999)) eventdata.add_child(Node.u32('eventno', 0)) eventdata.add_child(Node.s64('condition', 0)) eventdata.add_child(Node.u32('reward', 0)) eventdata.add_child(Node.s32('comptime', 1 if playerstats.get_bool('completed') else 0)) eventdata.add_child(Node.s64('savedata', 0)) for evtprogress in progress: # Babylon's adventure progres and anything else the game sends eventdata = Node.void('eventdata') response.add_child(eventdata) eventdata.add_child(Node.u32('eventid', evtprogress.id)) eventdata.add_child(Node.s32('eventtype', int(evtprogress.type))) eventdata.add_child(Node.u32('eventno', 0)) eventdata.add_child(Node.s64('condition', 0)) eventdata.add_child(Node.u32('reward', rewards.get(evtprogress.type, {}).get(evtprogress.id))) eventdata.add_child(Node.s32('comptime', 1 if evtprogress.data.get_bool('completed') else 0)) eventdata.add_child(Node.s64('savedata', evtprogress.data.get_int('progress'))) def __handle_usersave(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: if userid is None: # the game sends us empty user ID strings when a guest is playing. # Return early so it doesn't wait a minute and a half to show the # results screen. return if requestdata.child_value('isgameover'): style = int(requestdata.child_value('playstyle')) is_dp = style == self.GAME_STYLE_DOUBLE # We don't save anything for gameover requests, since we # already saved scores on individual ones. So, just use this # as a spot to bump play counts and such play_stats = self.get_play_statistics(userid) if is_dp: play_stats.increment_int('double_plays') else: play_stats.increment_int('single_plays') self.update_play_statistics(userid, play_stats) # Now is a good time to check if we have workout mode enabled, # and if so, store the calories earned for this set. profile = self.get_profile(userid) enabled = profile.get_bool('workout_mode') weight = profile.get_int('weight') if enabled and weight > 0: # We enabled weight display, find the calories and save them total = 0 for child in requestdata.children: if child.name != 'note': continue total = total + (child.child_value('calorie') or 0) self.data.local.user.put_time_based_achievement( self.game, self.version, userid, 0, 'workout', { 'calories': total, 'weight': weight, }, ) # Find any event updates for child in requestdata.children: if child.name != 'event': continue # Skip empty events or events we don't support eventid = child.child_value('eventid') eventtype = child.child_value('eventtype') if eventid == 0 or eventtype == 0: continue # Save data to replay to the client later completed = child.child_value('comptime') != 0 progress = child.child_value('savedata') self.data.local.user.put_achievement( self.game, self.version, userid, eventid, str(eventtype), { 'completed': completed, 'progress': progress, }, ) return # Find the highest stagenum played score = None stagenum = 0 for child in requestdata.children: if child.name != 'note': continue if child.child_value('stagenum') > stagenum: score = child stagenum = child.child_value('stagenum') if score is None: raise Exception('Couldn\'t find newest score to save!') songid = score.child_value('mcode') chart = self.game_to_db_chart(score.child_value('notetype')) rank = self.game_to_db_rank(score.child_value('rank')) halo = self.game_to_db_halo(score.child_value('clearkind')) points = score.child_value('score') combo = score.child_value('maxcombo') ghost = score.child_value('ghost') self.update_score( userid, songid, chart, points, rank, halo, combo, ghost=ghost, ) def __handle_rivalload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: data = Node.void('data') response.add_child(data) data.add_child(Node.s32('recordtype', requestdata.child_value('loadflag'))) thismachine = self.data.local.machine.get_machine(self.config.machine.pcbid) machines_by_id: Dict[int, Optional[Machine]] = {thismachine.id: thismachine} loadkind = requestdata.child_value('loadflag') profiles_by_userid: Dict[UserID, Profile] = {} def get_machine(lid: int) -> Optional[Machine]: if lid not in machines_by_id: pcbid = self.data.local.machine.from_machine_id(lid) if pcbid is None: machines_by_id[lid] = None return None machine = self.data.local.machine.get_machine(pcbid) if machine is None: machines_by_id[lid] = None return None machines_by_id[lid] = machine return machines_by_id[lid] if loadkind == self.GAME_RIVAL_TYPE_WORLD: # Just load all scores for this network scores = self.data.remote.music.get_all_records(self.game, self.music_version) elif loadkind == self.GAME_RIVAL_TYPE_AREA: if thismachine.arcade is not None: match_arcade = thismachine.arcade match_machine = None else: match_arcade = None match_machine = thismachine.id # Load up all scores by any user registered on a machine in the same arcade profiles = self.data.local.user.get_all_profiles(self.game, self.version) userids: List[UserID] = [] for userid, profiledata in profiles: profiles_by_userid[userid] = profiledata # If we have an arcade to match, see if this user's location matches the arcade. # If we don't, just match lid directly if match_arcade is not None: theirmachine = get_machine(profiledata.get_int('lid')) if theirmachine is not None and theirmachine.arcade == match_arcade: userids.append(userid) elif match_machine is not None: if profiledata.get_int('lid') == match_machine: userids.append(userid) # Load all scores for users in the area scores = self.data.local.music.get_all_records(self.game, self.music_version, userlist=userids) elif loadkind == self.GAME_RIVAL_TYPE_MACHINE: # Load up all scores and filter them by those earned at this location scores = self.data.local.music.get_all_records(self.game, self.music_version, locationlist=[thismachine.id]) elif loadkind in [ self.GAME_RIVAL_TYPE_RIVAL1, self.GAME_RIVAL_TYPE_RIVAL2, self.GAME_RIVAL_TYPE_RIVAL3, ]: # Load up this user's highscores, format the way the below code expects it extid = requestdata.child_value('ddrcode') otherid = self.data.remote.user.from_extid(self.game, self.version, extid) userscores = self.data.remote.music.get_scores(self.game, self.music_version, otherid) scores = [(otherid, score) for score in userscores] else: # Nothing here scores = [] missing_users = [userid for (userid, _) in scores if userid not in profiles_by_userid] for (userid, profile) in self.get_any_profiles(missing_users): profiles_by_userid[userid] = profile for userid, score in scores: if profiles_by_userid.get(userid) is None: raise Exception(f'Logic error, couldn\'t find any profile for {userid}') profiledata = profiles_by_userid[userid] record = Node.void('record') data.add_child(record) record.add_child(Node.u32('mcode', score.id)) record.add_child(Node.u8('notetype', self.db_to_game_chart(score.chart))) record.add_child(Node.u8('rank', self.db_to_game_rank(score.data.get_int('rank')))) record.add_child(Node.u8('clearkind', self.db_to_game_halo(score.data.get_int('halo')))) record.add_child(Node.u8('flagdata', 0)) record.add_child(Node.string('name', profiledata.get_str('name'))) record.add_child(Node.s32('area', profiledata.get_int('area', 58))) record.add_child(Node.s32('code', profiledata.extid)) record.add_child(Node.s32('score', score.points)) record.add_child(Node.s32('ghostid', score.key)) def __handle_usernew(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: if userid is None: raise Exception('Expecting valid UserID to create new profile!') machine = self.data.local.machine.get_machine(self.config.machine.pcbid) profile = Profile( self.game, self.version, "", 0, { 'lid': machine.id, }, ) self.put_profile(userid, profile) response.add_child(Node.string('seq', ID.format_extid(profile.extid))) response.add_child(Node.s32('code', profile.extid)) response.add_child(Node.string('shoparea', '')) def __handle_inheritance(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: if userid is not None: previous_version = self.previous_version() profile = previous_version.get_profile(userid) else: profile = None response.add_child(Node.s32('InheritanceStatus', 1 if profile is not None else 0)) def __handle_ghostload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None: ghostid = requestdata.child_value('ghostid') ghost = self.data.local.music.get_score_by_key(self.game, self.music_version, ghostid) if ghost is None: return userid, score = ghost profile = self.get_profile(userid) if profile is None: return if 'ghost' not in score.data: return ghostdata = Node.void('ghostdata') response.add_child(ghostdata) ghostdata.add_child(Node.s32('code', profile.extid)) ghostdata.add_child(Node.u32('mcode', score.id)) ghostdata.add_child(Node.u8('notetype', self.db_to_game_chart(score.chart))) ghostdata.add_child(Node.s32('ghostsize', len(score.data['ghost']))) ghostdata.add_child(Node.string('ghost', score.data['ghost'])) def handle_playerdata_usergamedata_advanced_request(self, request: Node) -> Optional[Node]: playerdata = Node.void('playerdata') # DDR Ace decides to be difficult and have a third level of packet switching mode = request.child_value('data/mode') refid = request.child_value('data/refid') extid = request.child_value('data/ddrcode') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: # Possibly look up by extid instead userid = self.data.remote.user.from_extid(self.game, self.version, extid) if mode == 'userload': self.__handle_userload(userid, request.child('data'), playerdata) elif mode == 'usersave': self.__handle_usersave(userid, request.child('data'), playerdata) elif mode == 'rivalload': self.__handle_rivalload(userid, request.child('data'), playerdata) elif mode == 'usernew': self.__handle_usernew(userid, request.child('data'), playerdata) elif mode == 'inheritance': self.__handle_inheritance(userid, request.child('data'), playerdata) elif mode == 'ghostload': self.__handle_ghostload(userid, request.child('data'), playerdata) else: # We don't support this return None playerdata.add_child(Node.s32('result', 0)) return playerdata def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node: playerdata = Node.void('playerdata') refid = request.child_value('data/refid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0) usergamedata = profile.get_dict('usergamedata') for record in request.child('data/record').children: if record.name != 'd': continue strdata = base64.b64decode(record.value) bindata = base64.b64decode(record.child_value('bin1')) # Grab and format the profile objects strdatalist = strdata.split(b',') profiletype = strdatalist[1].decode('utf-8') strdatalist = strdatalist[2:] # Extract relevant bits for frontend/API if profiletype == 'COMMON': profile.replace_str('name', strdatalist[self.GAME_COMMON_NAME_OFFSET].decode('ascii')) profile.replace_int('area', intish(strdatalist[self.GAME_COMMON_AREA_OFFSET].decode('ascii'), 16)) profile.replace_bool('workout_mode', int(strdatalist[self.GAME_COMMON_WEIGHT_DISPLAY_OFFSET].decode('ascii'), 16) != 0) profile.replace_int('weight', int(float(strdatalist[self.GAME_COMMON_WEIGHT_OFFSET].decode('ascii')) * 10)) profile.replace_int('character', int(strdatalist[self.GAME_COMMON_CHARACTER_OFFSET].decode('ascii'), 16)) if profiletype == 'OPTION': profile.replace_int('combo', int(strdatalist[self.GAME_OPTION_COMBO_POSITION_OFFSET].decode('ascii'), 16)) profile.replace_int('early_late', int(strdatalist[self.GAME_OPTION_FAST_SLOW_OFFSET].decode('ascii'), 16)) profile.replace_int('arrowskin', int(strdatalist[self.GAME_OPTION_ARROW_SKIN_OFFSET].decode('ascii'), 16)) profile.replace_int('guidelines', int(strdatalist[self.GAME_OPTION_GUIDELINE_OFFSET].decode('ascii'), 16)) profile.replace_int('filter', int(strdatalist[self.GAME_OPTION_FILTER_OFFSET].decode('ascii'), 16)) usergamedata[profiletype] = { 'strdata': b','.join(strdatalist), 'bindata': bindata, } profile.replace_dict('usergamedata', usergamedata) profile.replace_int('write_time', Time.now()) self.put_profile(userid, profile) playerdata.add_child(Node.s32('result', 0)) return playerdata def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node: playerdata = Node.void('playerdata') player = Node.void('player') playerdata.add_child(player) refid = request.child_value('data/refid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: profile = self.get_profile(userid) links = self.data.local.user.get_links(self.game, self.version, userid) records = 0 record = Node.void('record') player.add_child(record) def acehex(val: int) -> str: return hex(val)[2:] if profile is None: # Just return a default empty node record.add_child(Node.string('d', '')) records = 1 else: # Figure out what profiles are being requested profiletypes = request.child_value('data/recv_csv').split(',')[::2] usergamedata = profile.get_dict('usergamedata') for ptype in profiletypes: if ptype in usergamedata: records = records + 1 if ptype == "COMMON": # Return basic profile options name = profile.get_str('name') area = profile.get_int('area', self.get_machine_region()) if name == "": # This is a bogus profile created by the first login, substitute the # previous version values so that profile succession works. previous_version = self.previous_version() old_profile = previous_version.get_profile(userid) if old_profile is not None: name = old_profile.get_str('name') area = old_profile.get_int('area', self.get_machine_region()) else: area = self.get_machine_region() common = usergamedata[ptype]['strdata'].split(b',') common[self.GAME_COMMON_NAME_OFFSET] = name.encode('ascii') common[self.GAME_COMMON_AREA_OFFSET] = acehex(area).encode('ascii') common[self.GAME_COMMON_WEIGHT_DISPLAY_OFFSET] = b'1' if profile.get_bool('workout_mode') else b'0' common[self.GAME_COMMON_WEIGHT_OFFSET] = str(float(profile.get_int('weight')) / 10.0).encode('ascii') common[self.GAME_COMMON_CHARACTER_OFFSET] = acehex(profile.get_int('character')).encode('ascii') usergamedata[ptype]['strdata'] = b','.join(common) if ptype == "OPTION": # Return user settings for frontend option = usergamedata[ptype]['strdata'].split(b',') option[self.GAME_OPTION_FAST_SLOW_OFFSET] = acehex(profile.get_int('early_late')).encode('ascii') option[self.GAME_OPTION_COMBO_POSITION_OFFSET] = acehex(profile.get_int('combo')).encode('ascii') option[self.GAME_OPTION_ARROW_SKIN_OFFSET] = acehex(profile.get_int('arrowskin')).encode('ascii') option[self.GAME_OPTION_GUIDELINE_OFFSET] = acehex(profile.get_int('guidelines')).encode('ascii') option[self.GAME_OPTION_FILTER_OFFSET] = acehex(profile.get_int('filter')).encode('ascii') usergamedata[ptype]['strdata'] = b','.join(option) if ptype == "LAST": # Return the number of calories expended in the last day workouts = self.data.local.user.get_time_based_achievements( self.game, self.version, userid, achievementtype='workout', since=Time.now() - Time.SECONDS_IN_DAY, ) total = sum([w.data.get_int('calories') for w in workouts]) last = usergamedata[ptype]['strdata'].split(b',') last[self.GAME_LAST_CALORIES_OFFSET] = acehex(total).encode('ascii') usergamedata[ptype]['strdata'] = b','.join(last) if ptype == "RIVAL": # Fill in the DDR code and active status of the three active # rivals. rival = usergamedata[ptype]['strdata'].split(b',') lastdict = profile.get_dict('last') friends: Dict[int, Optional[Profile]] = {} for link in links: if link.type[:7] != 'friend_': continue pos = int(link.type[7:]) friends[pos] = self.get_profile(link.other_userid) for rivalno in [1, 2, 3]: activeslot = { 1: self.GAME_RIVAL_SLOT_1_ACTIVE_OFFSET, 2: self.GAME_RIVAL_SLOT_2_ACTIVE_OFFSET, 3: self.GAME_RIVAL_SLOT_3_ACTIVE_OFFSET, }[rivalno] whichfriend = lastdict.get_int(f'rival{rivalno}') - 1 if whichfriend < 0: # This rival isn't active rival[activeslot] = b'0' continue friendprofile = friends.get(whichfriend) if friendprofile is None: # This rival doesn't exist rival[activeslot] = b'0' continue ddrcodeslot = { 1: self.GAME_RIVAL_SLOT_1_DDRCODE_OFFSET, 2: self.GAME_RIVAL_SLOT_2_DDRCODE_OFFSET, 3: self.GAME_RIVAL_SLOT_3_DDRCODE_OFFSET, }[rivalno] rival[activeslot] = acehex(rivalno).encode('ascii') rival[ddrcodeslot] = acehex(friendprofile.extid).encode('ascii') usergamedata[ptype]['strdata'] = b','.join(rival) dnode = Node.string('d', base64.b64encode(usergamedata[ptype]['strdata']).decode('ascii')) dnode.add_child(Node.string('bin1', base64.b64encode(usergamedata[ptype]['bindata']).decode('ascii'))) record.add_child(dnode) player.add_child(Node.u32('record_num', records)) playerdata.add_child(Node.s32('result', 0)) return playerdata def handle_system_convcardnumber_request(self, request: Node) -> Node: cardid = request.child_value('data/card_id') cardnumber = CardCipher.encode(cardid) system = Node.void('system') data = Node.void('data') system.add_child(data) system.add_child(Node.s32('result', 0)) data.add_child(Node.string('card_number', cardnumber)) return system