# 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