# vim: set fileencoding=utf-8 import random from typing import Any, Dict, List, Optional, Set, Tuple from typing_extensions import Final 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.prop import JubeatProp from bemani.common import Profile, ValidatedDict, VersionConstants from bemani.data import Data, Score, Song, UserID from bemani.protocol import Node class JubeatQubell( JubeatDemodataGetHitchartHandler, JubeatDemodataGetNewsHandler, JubeatGamendRegisterHandler, JubeatGametopGetMeetingHandler, JubeatLobbyCheckHandler, JubeatLoggerReportHandler, JubeatBase, ): name: str = "Jubeat Qubell" version: int = VersionConstants.JUBEAT_QUBELL JBOX_EMBLEM_NORMAL: Final[int] = 1 JBOX_EMBLEM_PREMIUM: Final[int] = 2 ENABLE_GARNET: Final[bool] = False EVENTS: Dict[int, Dict[str, bool]] = { 5: { "enabled": False, }, 6: { "enabled": False, }, 15: { "enabled": False, }, 19: { "enabled": False, }, } def previous_version(self) -> Optional[JubeatBase]: return JubeatProp(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) ) if len(all_songs) >= 2: daily_songs = random.sample(all_songs, 2) data.local.game.put_time_sensitive_settings( cls.game, cls.version, "fc_challenge", { "start_time": start_time, "end_time": end_time, "today": daily_songs[0], "whim": daily_songs[1], }, ) events.append( ( "jubeat_fc_challenge_charts", { "version": cls.version, "today": daily_songs[0], "whim": daily_songs[1], }, ) ) # Mark that we did some actual work here. data.local.network.mark_scheduled( cls.game, cls.version, "fc_challenge", "daily" ) return events @classmethod def get_settings(cls) -> Dict[str, Any]: """ Return all of our front-end modifiably settings. """ return { "bools": [ { "name": "Force Unlock All Songs", "tip": "Forces all songs to be available by default", "category": "game_config", "setting": "force_song_unlock", }, ], } def __get_global_info(self) -> Node: info = Node.void("info") # Event info. Valid event IDs are 5, 6, 15, 19 event_info = Node.void("event_info") info.add_child(event_info) for event in self.EVENTS: evt = Node.void("event") event_info.add_child(evt) evt.set_attribute("type", str(event)) evt.add_child(Node.u8("state", 1 if self.EVENTS[event]["enabled"] else 0)) # Each of the following two sections should have zero or more child nodes (no # particular name) which look like the following: # # songid # start time? # end time? # # Share music? share_music = Node.void("share_music") info.add_child(share_music) # Bonus music? bonus_music = Node.void("bonus_music") info.add_child(bonus_music) # Some sort of music DB whitelist info.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, -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, ], ) ) info.add_child( Node.s32_array( "white_marker_list", [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], ) ) info.add_child( Node.s32_array( "white_theme_list", [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, ], ) ) info.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, -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, ], ) ) info.add_child( Node.s32_array( "shareable_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, -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, ], ) ) jbox = Node.void("jbox") info.add_child(jbox) jbox.add_child(Node.s32("point", 0)) emblem = Node.void("emblem") jbox.add_child(emblem) normal = Node.void("normal") emblem.add_child(normal) premium = Node.void("premium") emblem.add_child(premium) normal.add_child(Node.s16("index", 2)) premium.add_child(Node.s16("index", 1)) born = Node.void("born") info.add_child(born) born.add_child(Node.s8("status", 0)) born.add_child(Node.s16("year", 0)) digdig = Node.void("digdig") info.add_child(digdig) stage_list = Node.void("stage_list") digdig.add_child(stage_list) # Stage numbers are between 1 and 13 inclusive. for i in range(1, 14): stage = Node.void("stage") stage_list.add_child(stage) stage.set_attribute("number", str(i)) stage.add_child(Node.u8("state", 0x1)) # Collection list values should look like: # # songid # start time? # end time? # collection = Node.void("collection") info.add_child(collection) collection.add_child(Node.void("rating_s")) # Additional digdig nodes that aren't the main event generic_dig = Node.void("generic_dig") info.add_child(generic_dig) map_list = Node.void("map_list") generic_dig.add_child(map_list) # DigDig nodes here have the following format: # # start time? # end time? # # # # point # norma num # # # point # # type # music_id # seq_difficulty # rating # stage_level # goal # # # 32 bytes long title # # # # # # point # # type # music_id # bonus_tune_point # title_id # marker_id # background_id # # # # # # # # # # music_id # seq_difficulty # # SECRET|empty # # # # # RISKY|empty # # # type # # rating # score # # true/false # # type # # # # # type # # rating # score # # true/false # # type # # # # # # # # # 64 byte string # # # # # # # # # # # # # unknown # # # expert_option = Node.void("expert_option") info.add_child(expert_option) expert_option.add_child(Node.bool("is_available", True)) tsumtsum = Node.void("tsumtsum") info.add_child(tsumtsum) tsumtsum.add_child(Node.bool("is_available", True)) nagatanien = Node.void("nagatanien") info.add_child(nagatanien) nagatanien.add_child(Node.bool("is_available", True)) all_music_matching = Node.void("all_music_matching") info.add_child(all_music_matching) all_music_matching.add_child(Node.bool("is_available", True)) question_list = Node.void("question_list") info.add_child(question_list) return info 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("tax_phase", 1)) facility = Node.void("facility") data.add_child(facility) facility.add_child(Node.u32("exist", 1)) data.add_child(self.__get_global_info()) return shopinfo def handle_recommend_get_recommend_request(self, request: Node) -> Node: recommend = Node.void("recommend") data = Node.void("data") recommend.add_child(data) player = Node.void("player") data.add_child(player) music_list = Node.void("music_list") player.add_child(music_list) # TODO: Might be a way to figure out who plays what song and then offer # recommendations based on that. There should be 12 songs returned here. recommended_songs: List[Song] = [] for i, song in enumerate(recommended_songs): music = Node.void("music") music_list.add_child(music) music.set_attribute("order", str(i)) music.add_child(Node.s32("music_id", song.id)) music.add_child(Node.s8("seq", song.chart)) return recommend def handle_gametop_regist_request(self, request: Node) -> Node: data = request.child("data") player = data.child("player") refid = player.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") refid = player.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") mdata_ver = player.child_value( "mdata_ver" ) # Game requests mdata 3 times per profile for some reason if mdata_ver != 1: 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", extid)) playdata = Node.void("mdata_list") player.add_child(playdata) return root 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_info_request(self, request: Node) -> Node: root = Node.void("gametop") data = Node.void("data") root.add_child(data) data.add_child(self.__get_global_info()) return root def handle_gameend_final_request(self, request: Node) -> Node: data = request.child("data") player = data.child("player") if player is not None: refid = player.child_value("refid") else: refid = None if refid is not None: userid = self.data.remote.user.from_refid(self.game, self.version, refid) else: userid = None if userid is not None: profile = self.get_profile(userid) # Grab unlock progress item = player.child("item") if item is not None: owned_emblems = self.calculate_owned_items( item.child_value("emblem_list") ) for index in owned_emblems: self.data.local.user.put_achievement( self.game, self.version, userid, index, "emblem", {}, ) # jbox stuff jbox = player.child("jbox") jboxdict = profile.get_dict("jbox") if jbox is not None: jboxdict.replace_int("point", jbox.child_value("point")) emblemtype = jbox.child_value("emblem/type") index = jbox.child_value("emblem/index") if emblemtype == self.JBOX_EMBLEM_NORMAL: jboxdict.replace_int("normal_index", index) elif emblemtype == self.JBOX_EMBLEM_PREMIUM: jboxdict.replace_int("premium_index", index) profile.replace_dict("jbox", jboxdict) # Born stuff born = player.child("born") if born is not None: profile.replace_int("born_status", born.child_value("status")) profile.replace_int("born_year", born.child_value("year")) else: profile = None if userid is not None and profile is not None: self.put_profile(userid, profile) return Node.void("gameend") def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void("gametop") data = Node.void("data") root.add_child(data) # Figure out if we're force-unlocking songs. game_config = self.get_game_config() force_unlock = game_config.get_bool("force_song_unlock") achievements = self.data.local.user.get_achievements( self.game, self.version, userid ) owned_songs: Set[int] = set() owned_secrets: Set[int] = set() owned_emblems: Set[int] = set() for achievement in achievements: if achievement.type == "emblem": owned_emblems.add(achievement.id) elif achievement.type == "song": owned_songs.add(achievement.id) elif achievement.type == "secret": owned_secrets.add(achievement.id) # Make sure we grant ownership of default main parts. default_emblems = self.default_select_jbox() owned_emblems.update(default_emblems) default_main = next(iter(default_emblems)) if default_emblems else 0 # Jubeat Prop appears to allow full event overrides per-player data.add_child(self.__get_global_info()) player = Node.void("player") data.add_child(player) # Some server node server = Node.void("server") player.add_child(server) # Basic profile info player.add_child(Node.string("name", profile.get_str("name", "なし"))) player.add_child(Node.s32("jid", profile.extid)) # Miscelaneous crap player.add_child(Node.s32("session_id", 1)) player.add_child(Node.u64("event_flag", 0)) # 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("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"))) info.add_child( Node.s32("bonus_tune_points", profile.get_int("bonus_tune_points")) ) info.add_child( Node.bool("is_bonus_tune_played", profile.get_bool("is_bonus_tune_played")) ) # 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") and not profile.get_bool("saved"), ) ) # 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)) # 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.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"))) last.add_child(Node.s32("music_id", lastdict.get_int("music_id"))) last.add_child(Node.s8("seq_id", lastdict.get_int("seq_id"))) last.add_child(Node.s8("sort", lastdict.get_int("sort"))) last.add_child(Node.s8("category", lastdict.get_int("category"))) last.add_child(Node.s8("expert_option", lastdict.get_int("expert_option"))) last.add_child(Node.s32("dig_select", lastdict.get_int("dig_select"))) settings = Node.void("settings") last.add_child(settings) settings.add_child(Node.s8("marker", lastdict.get_int("marker"))) settings.add_child(Node.s8("theme", lastdict.get_int("theme"))) settings.add_child(Node.s16("title", lastdict.get_int("title"))) settings.add_child(Node.s16("parts", lastdict.get_int("parts"))) settings.add_child(Node.s8("rank_sort", lastdict.get_int("rank_sort"))) settings.add_child(Node.s8("combo_disp", lastdict.get_int("combo_disp"))) settings.add_child(Node.s8("matching", lastdict.get_int("matching"))) settings.add_child(Node.s8("hazard", lastdict.get_int("hazard"))) settings.add_child(Node.s8("hard", lastdict.get_int("hard"))) # Hack to make the default emblem appear properly. partslist = lastdict.get_int_array("emblem", 5, [0, default_main, 0, 0, 0]) if partslist[1] == 0: partslist[1] = default_main settings.add_child(Node.s16_array("emblem", partslist)) # Secret unlocks item = Node.void("item") player.add_child(item) item.add_child( Node.s32_array( "music_list", profile.get_int_array("music_list", 64, [-1] * 64) ) ) item.add_child( Node.s32_array( "secret_list", ([-1] * 64) if force_unlock else self.create_owned_items(owned_songs, 64), ) ) item.add_child( Node.s32_array( "theme_list", profile.get_int_array("theme_list", 16, [-1] * 16) ) ) item.add_child( Node.s32_array( "marker_list", profile.get_int_array("marker_list", 16, [-1] * 16) ) ) item.add_child( Node.s32_array( "title_list", profile.get_int_array("title_list", 160, [-1] * 160) ) ) item.add_child( Node.s32_array( "parts_list", profile.get_int_array("parts_list", 160, [-1] * 160) ) ) item.add_child( Node.s32_array("emblem_list", self.create_owned_items(owned_emblems, 96)) ) new = Node.void("new") item.add_child(new) new.add_child( Node.s32_array( "secret_list", ([-1] * 64) if force_unlock else self.create_owned_items(owned_secrets, 64), ) ) new.add_child( Node.s32_array( "theme_list", profile.get_int_array("theme_list_new", 16, [-1] * 16) ) ) new.add_child( Node.s32_array( "marker_list", profile.get_int_array("marker_list_new", 16, [-1] * 16) ) ) # 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.extid)) rival.add_child(Node.string("name", rprofile.get_str("name"))) # This looks like a carry-over from prop's career and isn't displayed. career = Node.void("career") rival.add_child(career) career.add_child(Node.s16("level", 1)) # Lazy way of keeping track of rivals, since we can only have 3 # or the game with throw up. rivalcount += 1 if rivalcount >= 3: break rivallist.set_attribute("count", str(rivalcount)) lab_edit_seq = Node.void("lab_edit_seq") player.add_child(lab_edit_seq) lab_edit_seq.set_attribute("count", "0") # Full combo 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.music_version, userid, entry.get_int("today", -1), timelimit=start_time, ) whim_attempts = self.data.local.music.get_all_attempts( self.game, self.music_version, userid, entry.get_int("whim", -1), timelimit=start_time, ) fc_challenge = Node.void("fc_challenge") player.add_child(fc_challenge) today = Node.void("today") fc_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)) whim = Node.void("whim") fc_challenge.add_child(whim) whim.add_child(Node.s32("music_id", entry.get_int("whim", -1))) whim.add_child(Node.u8("state", 0x40 if len(whim_attempts) > 0 else 0x0)) # No news, ever. news = Node.void("news") player.add_child(news) news.add_child(Node.s16("checked", 0)) news.add_child(Node.u32("checked_flag", 0)) # Sane defaults for unknown/who cares nodes history = Node.void("history") player.add_child(history) history.set_attribute("count", "0") free_first_play = Node.void("free_first_play") player.add_child(free_first_play) free_first_play.add_child(Node.bool("is_available", False)) navi = Node.void("navi") player.add_child(navi) navi.add_child(Node.u64("flag", profile.get_int("navi_flag"))) # Player status for events event_info = Node.void("event_info") player.add_child(event_info) for achievement in achievements: if achievement.type == "event": # There are two significant bits here, 0x1 and 0x2, I think the first # one is whether the event is started, second is if its finished? event = Node.void("event") event_info.add_child(event) event.set_attribute("type", str(achievement.id)) state = 0x0 state = ( state + 0x2 if achievement.data.get_bool("is_completed") else 0x0 ) event.add_child(Node.u8("state", state)) # JBox stuff jbox = Node.void("jbox") jboxdict = profile.get_dict("jbox") player.add_child(jbox) jbox.add_child(Node.s32("point", jboxdict.get_int("point"))) emblem = Node.void("emblem") jbox.add_child(emblem) normal = Node.void("normal") emblem.add_child(normal) premium = Node.void("premium") emblem.add_child(premium) # Calculate a random index for normal and premium to give to player # as a gatcha. normalindex, premiumindex = self.random_select_jbox(owned_emblems) normal.add_child(Node.s16("index", normalindex)) premium.add_child(Node.s16("index", premiumindex)) # Digdig stuff digdig = Node.void("digdig") digdigdict = profile.get_dict("digdig") eternaldict = digdigdict.get_dict("eternal") olddict = digdigdict.get_dict("old") player.add_child(digdig) digdig.add_child(Node.u64("flag", digdigdict.get_int("flag"))) # Emerald main stages main = Node.void("main") digdig.add_child(main) stage = Node.void("stage") main.add_child(stage) stage.set_attribute("number", str(digdigdict.get_int("stage_number", 1))) stage.add_child(Node.s32("point", digdigdict.get_int("point"))) stage.add_child( Node.s32_array("param", digdigdict.get_int_array("param", 12, [0] * 12)) ) # Emerald eternal stages eternal = Node.void("eternal") digdig.add_child(eternal) eternal.add_child(Node.s32("ratio", 1)) eternal.add_child(Node.s64("used_point", eternaldict.get_int("used_point"))) eternal.add_child(Node.s64("point", eternaldict.get_int("point"))) eternal.add_child( Node.s64("excavated_point", eternaldict.get_int("excavated_point")) ) cube = Node.void("cube") eternal.add_child(cube) cube.add_child( Node.s8_array("state", eternaldict.get_int_array("state", 12, [0] * 12)) ) item = Node.void("item") cube.add_child(item) item.add_child( Node.s32_array("kind", eternaldict.get_int_array("item_kind", 12, [0] * 12)) ) item.add_child( Node.s32_array( "value", eternaldict.get_int_array("item_value", 12, [0] * 12) ) ) norma = Node.void("norma") cube.add_child(norma) norma.add_child(Node.s64_array("till_time", [0] * 12)) norma.add_child( Node.s32_array( "kind", eternaldict.get_int_array("norma_kind", 12, [0] * 12) ) ) norma.add_child( Node.s32_array( "value", eternaldict.get_int_array("norma_value", 12, [0] * 12) ) ) norma.add_child( Node.s32_array( "param", eternaldict.get_int_array("norma_param", 12, [0] * 12) ) ) if self.ENABLE_GARNET: # Garnet old = Node.void("old") digdig.add_child(old) old.add_child(Node.s32("need_point", olddict.get_int("need_point"))) old.add_child(Node.s32("point", olddict.get_int("point"))) old.add_child( Node.s32_array( "excavated_point", olddict.get_int_array("excavated_point", 5, [0] * 5), ) ) old.add_child( Node.s32_array( "excavated", olddict.get_int_array("excavated", 5, [0] * 5) ) ) old.add_child( Node.s32_array("param", olddict.get_int_array("param", 5, [0] * 5)) ) # This should have a bunch of sub-nodes with the following format. Note that only # the first ten nodes are saved even if more are read. Presumably this is the list # of old songs we are allowing the player to unlock? Doesn't matter, we're disabling # Garnet anyway.: # # id # old.add_child(Node.void("music_list")) # Unlock event, turns on unlock challenge for a particular stage. unlock = Node.void("unlock") player.add_child(unlock) main = Node.void("main") unlock.add_child(main) stage_list = Node.void("stage_list") main.add_child(stage_list) # Stage numbers are between 1 and 13 inclusive. for i in range(1, 14): stage_flags = self.data.local.user.get_achievement( self.game, self.version, userid, i, "stage" ) if stage_flags is None: stage_flags = ValidatedDict() stage = Node.void("stage") stage_list.add_child(stage) stage.set_attribute("number", str(i)) stage.add_child(Node.u8("state", stage_flags.get_int("state"))) # DigDig event for server-controlled cubes (basically anything not Garnet or Emerald) generic_dig = Node.void("generic_dig") player.add_child(generic_dig) map_list = Node.void("map_list") generic_dig.add_child(map_list) # Map list consists of up to 9 of the following structures: # # points # points # stage # # # # 0 0 0 0 0 0 0 0 0 0 0 0 # # # 0 # # # # # New Music stuff new_music = Node.void("new_music") player.add_child(new_music) # Gift list, maybe from other players? gift_list = Node.void("gift_list") player.add_child(gift_list) # If we had gifts, they look like this: # # ?? # # Birthday event? born = Node.void("born") player.add_child(born) born.add_child(Node.s8("status", profile.get_int("born_status"))) born.add_child(Node.s16("year", profile.get_int("born_year"))) # More crap question_list = Node.void("question_list") player.add_child(question_list) return root def format_scores( self, userid: UserID, profile: Profile, scores: List[Score] ) -> Node: 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.extid)) playdata = Node.void("mdata_list") player.add_child(playdata) music = ValidatedDict() for score in scores: # Ignore festo-and-above chart types. if score.chart not in { self.CHART_TYPE_BASIC, self.CHART_TYPE_ADVANCED, self.CHART_TYPE_EXTREME, }: continue 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 def unformat_profile( self, userid: UserID, request: Node, oldprofile: Profile ) -> Profile: newprofile = oldprofile.clone() newprofile.replace_bool("saved", True) data = request.child("data") # Figure out if we're force-unlocking songs. If we are, we don't want to persist # secret stuff otherwise the game will accidentally unlock everything in the profile. game_config = self.get_game_config() force_unlock = game_config.get_bool("force_song_unlock") # Grab system information sysinfo = data.child("info") # Grab player information player = data.child("player") # Grab result information result = data.child("result") # Grab last information. Lots of this will be filled in while grabbing scores last = newprofile.get_dict("last") if sysinfo is not None: last.replace_int("play_time", sysinfo.child_value("time_gameend")) last.replace_str("shopname", sysinfo.child_value("shopname")) last.replace_str("areaname", sysinfo.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("ex_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("mynews_cnt", info.child_value("mynews_cnt")) newprofile.replace_int( "bonus_tune_points", info.child_value("bonus_tune_points") ) newprofile.replace_bool( "is_bonus_tune_played", info.child_value("is_bonus_tune_played") ) # Grab last settings lastnode = player.child("last") if lastnode is not None: last.replace_int("expert_option", lastnode.child_value("expert_option")) last.replace_int("dig_select", lastnode.child_value("dig_select")) last.replace_int("sort", lastnode.child_value("sort")) last.replace_int("category", lastnode.child_value("category")) settings = lastnode.child("settings") if settings is not None: last.replace_int("matching", settings.child_value("matching")) last.replace_int("hazard", settings.child_value("hazard")) last.replace_int("hard", settings.child_value("hard")) last.replace_int("marker", settings.child_value("marker")) last.replace_int("theme", settings.child_value("theme")) last.replace_int("title", settings.child_value("title")) last.replace_int("parts", settings.child_value("parts")) last.replace_int("rank_sort", settings.child_value("rank_sort")) last.replace_int("combo_disp", settings.child_value("combo_disp")) last.replace_int_array("emblem", 5, settings.child_value("emblem")) # Grab unlock progress item = player.child("item") if item is not None: newprofile.replace_int_array( "music_list", 64, item.child_value("music_list") ) newprofile.replace_int_array( "theme_list", 16, item.child_value("theme_list") ) newprofile.replace_int_array( "marker_list", 16, item.child_value("marker_list") ) newprofile.replace_int_array( "title_list", 160, item.child_value("title_list") ) newprofile.replace_int_array( "parts_list", 160, item.child_value("parts_list") ) if not force_unlock: # Don't persist if we're force-unlocked, this data will be bogus. owned_songs = self.calculate_owned_items( item.child_value("secret_list") ) for index in owned_songs: self.data.local.user.put_achievement( self.game, self.version, userid, index, "song", {}, ) owned_emblems = self.calculate_owned_items(item.child_value("emblem_list")) for index in owned_emblems: self.data.local.user.put_achievement( self.game, self.version, userid, index, "emblem", {}, ) newitem = item.child("new") if newitem is not None: newprofile.replace_int_array( "theme_list_new", 16, newitem.child_value("theme_list") ) newprofile.replace_int_array( "marker_list_new", 16, newitem.child_value("marker_list") ) if not force_unlock: # Don't persist if we're force-unlocked, this data will be bogus. owned_secrets = self.calculate_owned_items( newitem.child_value("secret_list") ) for index in owned_secrets: self.data.local.user.put_achievement( self.game, self.version, userid, index, "secret", {}, ) # jbox stuff jbox = player.child("jbox") jboxdict = newprofile.get_dict("jbox") if jbox is not None: jboxdict.replace_int("point", jbox.child_value("point")) emblemtype = jbox.child_value("emblem/type") index = jbox.child_value("emblem/index") if emblemtype == self.JBOX_EMBLEM_NORMAL: jboxdict.replace_int("normal_index", index) elif emblemtype == self.JBOX_EMBLEM_PREMIUM: jboxdict.replace_int("premium_index", index) newprofile.replace_dict("jbox", jboxdict) # event stuff event_info = player.child("event_info") if event_info is not None: for child in event_info.children: try: eventid = int(child.attribute("type")) except TypeError: # Event is empty continue is_completed = child.child_value("is_completed") # Figure out if we should update the rating/scores or not oldevent = self.data.local.user.get_achievement( self.game, self.version, userid, eventid, "event", ) if oldevent is None: # Create a new event structure for this oldevent = ValidatedDict() oldevent.replace_bool("is_completed", is_completed) # Save it as an achievement self.data.local.user.put_achievement( self.game, self.version, userid, eventid, "event", oldevent, ) # DigDig stuff digdig = player.child("digdig") digdigdict = newprofile.get_dict("digdig") if digdig is not None: digdigdict.replace_int("flag", digdig.child_value("flag")) main = digdig.child("main") if main is not None: stage = main.child("stage") stage_num = int(stage.attribute("number")) digdigdict.replace_int("stage_number", stage_num) digdigdict.replace_int("point", stage.child_value("point")) digdigdict.replace_int_array("param", 12, stage.child_value("param")) if stage.child_value("uc_available") is True: # We should enable unlock challenge for this node because the game # doesn't do it for us automatically. stage_flags = self.data.local.user.get_achievement( self.game, self.version, userid, stage_num, "stage" ) if stage_flags is None: stage_flags = ValidatedDict() stage_flags.replace_int("state", stage_flags.get_int("state") | 0x2) self.data.local.user.put_achievement( self.game, self.version, userid, stage_num, "stage", stage_flags, ) eternal = digdig.child("eternal") eternaldict = digdigdict.get_dict("eternal") if eternal is not None: eternaldict.replace_int("used_point", eternal.child_value("used_point")) eternaldict.replace_int("point", eternal.child_value("point")) eternaldict.replace_int( "excavated_point", eternal.child_value("excavated_point") ) eternaldict.replace_int_array( "state", 12, eternal.child_value("cube/state") ) eternaldict.replace_int_array( "item_kind", 12, eternal.child_value("cube/item/kind") ) eternaldict.replace_int_array( "item_value", 12, eternal.child_value("cube/item/value") ) eternaldict.replace_int_array( "norma_kind", 12, eternal.child_value("cube/norma/kind") ) eternaldict.replace_int_array( "norma_value", 12, eternal.child_value("cube/norma/value") ) eternaldict.replace_int_array( "norma_param", 12, eternal.child_value("cube/norma/param") ) digdigdict.replace_dict("eternal", eternaldict) if self.ENABLE_GARNET: old = digdig.child("old") olddict = digdigdict.get_dict("old") if old is not None: olddict.replace_int("need_point", old.child_value("need_point")) olddict.replace_int("point", old.child_value("point")) olddict.replace_int_array( "excavated_point", 5, old.child_value("excavated_point") ) olddict.replace_int_array( "excavated", 5, old.child_value("excavated") ) olddict.replace_int_array("param", 5, old.child_value("param")) digdigdict.replace_dict("old", olddict) # DigDig unlock event unlock = player.child("unlock") if unlock is not None: stage = unlock.child("main/stage") if stage is not None: stage_num = int(stage.attribute("number")) state = stage.child_value("state") # Just overwrite the state with this value self.data.local.user.put_achievement( self.game, self.version, userid, stage_num, "stage", {"state": state}, ) # If they cleared stage 13, we need to unlock eternal mode if stage_num == 13 and (state & 0x18) > 0: digdigdict.replace_int("flag", digdigdict.get_int("flag") | 0x2) # Save this back now that we've parsed everything newprofile.replace_dict("digdig", digdigdict) # Still don't know what this is for lol newprofile.replace_int("navi_flag", player.child_value("navi/flag")) # Grab scores and save those if result is not None: for tune in result.children: if tune.name != "tune": continue result = tune.child("player") songid = tune.child_value("music") timestamp = tune.child_value("timestamp") / 1000 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") stats = { "perfect": result.child_value("nr_perfect"), "great": result.child_value("nr_great"), "good": result.child_value("nr_good"), "poor": result.child_value("nr_poor"), "miss": result.child_value("nr_miss"), } # 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, stats ) # Born stuff born = player.child("born") if born is not None: newprofile.replace_int("born_status", born.child_value("status")) newprofile.replace_int("born_year", born.child_value("year")) # Save back last information gleaned from results newprofile.replace_dict("last", last) # Keep track of play statistics self.update_play_statistics(userid) return newprofile