from typing import Optional, Dict, Any from typing_extensions import Final from bemani.backend.ess import EventLogHandler from bemani.backend.museca.base import MusecaBase from bemani.backend.museca.common import ( MusecaGameFrozenHandler, MusecaGameHiscoreHandler, MusecaGameNewHandler, MusecaGamePlayEndHandler, MusecaGameSaveHandler, MusecaGameSaveMusicHandler, MusecaGameShopHandler, ) from bemani.backend.museca.museca1 import Museca1 from bemani.common import VersionConstants, Profile, ID from bemani.data import UserID from bemani.protocol import Node class Museca1Plus( EventLogHandler, MusecaGameFrozenHandler, MusecaGameHiscoreHandler, MusecaGameNewHandler, MusecaGamePlayEndHandler, MusecaGameSaveHandler, MusecaGameSaveMusicHandler, MusecaGameShopHandler, MusecaBase, ): name: str = "MÚSECA 1+1/2" version: int = VersionConstants.MUSECA_1_PLUS GAME_LIMITED_LOCKED: Final[int] = 1 GAME_LIMITED_UNLOCKABLE: Final[int] = 2 GAME_LIMITED_UNLOCKED: Final[int] = 3 GAME_CATALOG_TYPE_SONG: Final[int] = 0 GAME_CATALOG_TYPE_GRAFICA: Final[int] = 15 GAME_CATALOG_TYPE_MISSION: Final[int] = 16 GAME_GRADE_DEATH: Final[int] = 0 GAME_GRADE_POOR: Final[int] = 1 GAME_GRADE_MEDIOCRE: Final[int] = 2 GAME_GRADE_GOOD: Final[int] = 3 GAME_GRADE_GREAT: Final[int] = 4 GAME_GRADE_EXCELLENT: Final[int] = 5 GAME_GRADE_SUPERB: Final[int] = 6 GAME_GRADE_MASTERPIECE: Final[int] = 7 GAME_GRADE_PERFECT: Final[int] = 8 GAME_CLEAR_TYPE_FAILED: Final[int] = 1 GAME_CLEAR_TYPE_CLEARED: Final[int] = 2 GAME_CLEAR_TYPE_FULL_COMBO: Final[int] = 4 def previous_version(self) -> Optional[MusecaBase]: return Museca1(self.data, self.config, self.model) @classmethod def get_settings(cls) -> Dict[str, Any]: """ Return all of our front-end modifiably settings. """ return { "bools": [ { "name": "Disable Online Matching", "tip": "Disable online matching between games.", "category": "game_config", "setting": "disable_matching", }, { "name": "Force Song Unlock", "tip": "Force unlock all songs.", "category": "game_config", "setting": "force_unlock_songs", }, ], } def game_to_db_clear_type(self, clear_type: int) -> int: return { self.GAME_CLEAR_TYPE_FAILED: self.CLEAR_TYPE_FAILED, self.GAME_CLEAR_TYPE_CLEARED: self.CLEAR_TYPE_CLEARED, self.GAME_CLEAR_TYPE_FULL_COMBO: self.CLEAR_TYPE_FULL_COMBO, }[clear_type] def db_to_game_clear_type(self, clear_type: int) -> int: return { self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_FAILED, self.CLEAR_TYPE_CLEARED: self.GAME_CLEAR_TYPE_CLEARED, self.CLEAR_TYPE_FULL_COMBO: self.GAME_CLEAR_TYPE_FULL_COMBO, }[clear_type] def game_to_db_grade(self, grade: int) -> int: return { self.GAME_GRADE_DEATH: self.GRADE_DEATH, self.GAME_GRADE_POOR: self.GRADE_POOR, self.GAME_GRADE_MEDIOCRE: self.GRADE_MEDIOCRE, self.GAME_GRADE_GOOD: self.GRADE_GOOD, self.GAME_GRADE_GREAT: self.GRADE_GREAT, self.GAME_GRADE_EXCELLENT: self.GRADE_EXCELLENT, self.GAME_GRADE_SUPERB: self.GRADE_SUPERB, self.GAME_GRADE_MASTERPIECE: self.GRADE_MASTERPIECE, self.GAME_GRADE_PERFECT: self.GRADE_PERFECT, }[grade] def db_to_game_grade(self, grade: int) -> int: return { self.GRADE_DEATH: self.GAME_GRADE_DEATH, self.GRADE_POOR: self.GAME_GRADE_POOR, self.GRADE_MEDIOCRE: self.GAME_GRADE_MEDIOCRE, self.GRADE_GOOD: self.GAME_GRADE_GOOD, self.GRADE_GREAT: self.GAME_GRADE_GREAT, self.GRADE_EXCELLENT: self.GAME_GRADE_EXCELLENT, self.GRADE_SUPERB: self.GAME_GRADE_SUPERB, self.GRADE_MASTERPIECE: self.GAME_GRADE_MASTERPIECE, self.GRADE_PERFECT: self.GAME_GRADE_PERFECT, }[grade] def handle_game_3_common_request(self, request: Node) -> Node: game = Node.void("game_3") limited = Node.void("music_limited") game.add_child(limited) # Song unlock config game_config = self.get_game_config() if game_config.get_bool("force_unlock_songs"): ids = set() songs = self.data.local.music.get_all_songs(self.game, self.music_version) for song in songs: if song.data.get_int("limited") in ( self.GAME_LIMITED_LOCKED, self.GAME_LIMITED_UNLOCKABLE, ): ids.add((song.id, song.chart)) for songid, chart in ids: info = Node.void("info") limited.add_child(info) info.add_child(Node.s32("music_id", songid)) info.add_child(Node.u8("music_type", chart)) info.add_child(Node.u8("limited", self.GAME_LIMITED_UNLOCKED)) # Event config event = Node.void("event") game.add_child(event) def enable_event(eid: int) -> None: evt = Node.void("info") event.add_child(evt) evt.add_child(Node.u32("event_id", eid)) if not game_config.get_bool("disable_matching"): enable_event(143) # Matching enabled # These events are meant specifically for Museca Plus museca_plus_events = [ 140, # Agetta Moratta (vmlink_phase 3 in musicdb) 211, # News 1 212, # News 2 ] event_ids = [ 1, # Extended pedal options (no effect on Museca 1+1/2) 56, # Generator grafica icon 83, # Paseli Light Start 86, # Generator grafica icon 98, # Caption 2 notice (grs_grafica_caption_2.png) 105, # Makes the "Number of Layers" option visible in game settings 130, # Curator Rank 141, # Coconatsu & Mukipara grafica effects 145, # MUKIPARA UNLOCKS 146, # MUKIPARA UNLOCKS 147, # MUKIPARA UNLOCKS 148, # MUKIPARA UNLOCKS 149, # MUKIPARA UNLOCKS 195, # Fictional Curator (foot pedal options) ] for evtid in event_ids: enable_event(evtid) if self.omnimix: for evtid in museca_plus_events: enable_event(evtid) # List of known event IDs: # 56, # Generator grafica icon # 83, # Paseli Light Start # 86, # Generator grafica icon # 98, # Caption 2 notice (grs_grafica_caption_2.png) # 100, # DJ YOSHITAKA EXHIBITION 2016 # 103, # HATSUNE MIKU EXHIBITION 2016 - PART 1 # 104, # HATSUNE MIKU EXHIBITION 2016 - PART 2 # 105, # Makes the "Number of Layers" option visible in game settings # 106, # HATSUNE MIKU EXHIBITION 2016 - PART 3 # 117, # NEW GENERATION METEOR DIFFUSE FESTA 2016 / RYUSEI FESTA TRIGGER # 129, # COCONATSU EXHIBITION 2016 # 130, # Curator Rank # 97, # Agetta Moratta (vmlink_phase 1 in musicdb) # 114, # Agetta Moratta (vmlink_phase 2 in musicdb) # 140, # Agetta Moratta (vmlink_phase 3 in musicdb) # 141, # Coconatsu & Mukipara grafica effects # 143, # Matching # 144, # BEMANI ARCHAEOLOGICAL EXHIBITION # 163, # TUTORIAL SNOW # 169, # SHIORI FUJISAKI EXHIBITION 2017 - PART 1 # 174, # SHIORI FUJISAKI EXHIBITION 2017 - PART 2 # 182, # Mute illil's voice? # 192, # GREAT REPRINT FESTIVAL: MIKU + DJ YOSHITAKA # 194, # Continue # 195, # Fictional Curator (foot pedal options) # 211, #News 1 # 212, #News 2 # 213, #News 3 # 214, #News 4 # 217, #News 5 # 218, #News 6 # 219, #News 7 # 220, #News 8 # 221, # GRAFICA PRESENTATION CAMPAIGN “THE PRIMITIVE LIFE EXHIBITION” # 222, # GRAFICA PRESENTATION CAMPAIGN "NOISE" # 223, # GRAFICA PRESENTATION CAMPAIGN "PATISSERIE ROUGE" # 224, # GRAFICA PRESENTATION CAMPAIGN "GUNSLINGER" # 145, # MUKIPARA UNLOCKS # 146, # MUKIPARA UNLOCKS # 147, # MUKIPARA UNLOCKS # 148, # MUKIPARA UNLOCKS # 149, # MUKIPARA UNLOCKS # Makes special missions available on grafica that have them. extend = Node.void("extend") game.add_child(extend) info = Node.void("info") extend.add_child(info) info.add_child(Node.u32("extend_id", 1)) info.add_child(Node.u32("extend_type", 9)) info.add_child(Node.s32("param_num_1", 2)) info.add_child(Node.s32("param_num_2", 50)) info.add_child(Node.s32("param_num_3", 59)) info.add_child(Node.s32("param_num_4", 64)) info.add_child(Node.s32("param_num_5", 86)) info.add_child(Node.string("param_str_1", "available_ex: 1")) info.add_child(Node.string("param_str_2", "available_ex: 1")) info.add_child(Node.string("param_str_3", "available_ex: 1")) info.add_child(Node.string("param_str_4", "available_ex: 1")) info.add_child(Node.string("param_str_5", "available_ex: 1")) if self.omnimix: info = Node.void("info") extend.add_child(info) info.add_child(Node.u32("extend_id", 2)) info.add_child(Node.u32("extend_type", 9)) info.add_child(Node.s32("param_num_1", 210)) info.add_child(Node.s32("param_num_2", 0)) info.add_child(Node.s32("param_num_3", 0)) info.add_child(Node.s32("param_num_4", 0)) info.add_child(Node.s32("param_num_5", 0)) info.add_child(Node.string("param_str_1", "")) info.add_child(Node.string("param_str_2", "")) info.add_child(Node.string("param_str_3", "")) info.add_child(Node.string("param_str_4", "")) info.add_child(Node.string("param_str_5", "")) return game def handle_game_3_lounge_request(self, request: Node) -> Node: game = Node.void("game_3") # Refresh interval in seconds. game.add_child(Node.u32("interval", 10)) return game def handle_game_3_exception_request(self, request: Node) -> Node: return Node.void("game_3") def handle_game_3_load_request(self, request: Node) -> Node: refid = request.child_value("refid") root = self.get_profile_by_refid(refid) if root is not None: return root # Figure out if this user has an older profile or not userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is not None: previous_game = self.previous_version() else: previous_game = None if previous_game is not None: profile = previous_game.get_profile(userid) else: profile = None if profile is not None: # Return the previous formatted profile to the game. return previous_game.format_profile(userid, profile) else: root = Node.void("game_3") root.add_child(Node.u8("result", 1)) return root def handle_game_3_load_m_request(self, request: Node) -> Node: refid = request.child_value("dataid") 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: scores = self.data.remote.music.get_scores(self.game, self.music_version, userid) else: scores = [] # Output to the game game = Node.void("game_3") new = Node.void("new") game.add_child(new) for score in scores: music = Node.void("music") new.add_child(music) music.add_child(Node.u32("music_id", score.id)) music.add_child(Node.u32("music_type", score.chart)) music.add_child(Node.u32("score", score.points)) music.add_child(Node.u32("cnt", score.plays)) music.add_child(Node.u32("combo", score.data.get_int("combo"))) music.add_child( Node.u32( "clear_type", self.db_to_game_clear_type(score.data.get_int("clear_type")), ) ) music.add_child(Node.u32("score_grade", self.db_to_game_grade(score.data.get_int("grade")))) stats = score.data.get_dict("stats") music.add_child(Node.u32("btn_rate", stats.get_int("btn_rate"))) music.add_child(Node.u32("long_rate", stats.get_int("long_rate"))) music.add_child(Node.u32("vol_rate", stats.get_int("vol_rate"))) return game def format_profile(self, userid: UserID, profile: Profile) -> Node: game = Node.void("game_3") # Generic profile stuff game.add_child(Node.string("name", profile.get_str("name"))) game.add_child(Node.string("code", ID.format_extid(profile.extid))) game.add_child(Node.u32("gamecoin_packet", profile.get_int("packet"))) game.add_child(Node.u32("gamecoin_block", profile.get_int("block"))) game.add_child(Node.s16("skill_name_id", profile.get_int("skill_name_id", -1))) game.add_child(Node.s32_array("hidden_param", profile.get_int_array("hidden_param", 20))) game.add_child(Node.u32("blaster_energy", profile.get_int("blaster_energy"))) game.add_child(Node.u32("blaster_count", profile.get_int("blaster_count"))) # Enable Ryusei Festa ryusei_festa = Node.void("ryusei_festa") game.add_child(ryusei_festa) ryusei_festa.add_child(Node.bool("ryusei_festa_trigger", True)) # Play statistics statistics = self.get_play_statistics(userid) game.add_child(Node.u32("play_count", statistics.total_plays)) game.add_child(Node.u32("daily_count", statistics.today_plays)) game.add_child(Node.u32("play_chain", statistics.consecutive_days)) # Last played stuff if "last" in profile: lastdict = profile.get_dict("last") last = Node.void("last") game.add_child(last) last.add_child(Node.s32("music_id", lastdict.get_int("music_id", -1))) last.add_child(Node.u8("music_type", lastdict.get_int("music_type"))) last.add_child(Node.u8("sort_type", lastdict.get_int("sort_type"))) last.add_child(Node.u8("narrow_down", lastdict.get_int("narrow_down"))) last.add_child(Node.u8("headphone", lastdict.get_int("headphone"))) last.add_child(Node.u16("appeal_id", lastdict.get_int("appeal_id", 1001))) last.add_child(Node.u16("comment_id", lastdict.get_int("comment_id"))) last.add_child(Node.u8("gauge_option", lastdict.get_int("gauge_option"))) # Item unlocks itemnode = Node.void("item") game.add_child(itemnode) game_config = self.get_game_config() achievements = self.data.local.user.get_achievements(self.game, self.version, userid) for item in achievements: if item.type[:5] != "item_": continue itemtype = int(item.type[5:]) if game_config.get_bool("force_unlock_songs") and itemtype == self.GAME_CATALOG_TYPE_SONG: # Don't echo unlocked songs, we will add all of them later continue info = Node.void("info") itemnode.add_child(info) info.add_child(Node.u8("type", itemtype)) info.add_child(Node.u32("id", item.id)) info.add_child(Node.u32("param", item.data.get_int("param"))) if "diff_param" in item.data: info.add_child(Node.s32("diff_param", item.data.get_int("diff_param"))) if game_config.get_bool("force_unlock_songs"): ids: Dict[int, int] = {} songs = self.data.local.music.get_all_songs(self.game, self.music_version) for song in songs: if song.id not in ids: ids[song.id] = 0 if song.data.get_int("difficulty") > 0: ids[song.id] = ids[song.id] | (1 << song.chart) for itemid in ids: if ids[itemid] == 0: continue info = Node.void("info") itemnode.add_child(info) info.add_child(Node.u8("type", self.GAME_CATALOG_TYPE_SONG)) info.add_child(Node.u32("id", itemid)) info.add_child(Node.u32("param", ids[itemid])) return game def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: newprofile = oldprofile.clone() # Update blaster energy and in-game currencies earned_gamecoin_packet = request.child_value("earned_gamecoin_packet") if earned_gamecoin_packet is not None: newprofile.replace_int("packet", newprofile.get_int("packet") + earned_gamecoin_packet) earned_gamecoin_block = request.child_value("earned_gamecoin_block") if earned_gamecoin_block is not None: newprofile.replace_int("block", newprofile.get_int("block") + earned_gamecoin_block) earned_blaster_energy = request.child_value("earned_blaster_energy") if earned_blaster_energy is not None: newprofile.replace_int( "blaster_energy", newprofile.get_int("blaster_energy") + earned_blaster_energy, ) # Miscelaneous stuff newprofile.replace_int("blaster_count", request.child_value("blaster_count")) newprofile.replace_int("skill_name_id", request.child_value("skill_name_id")) newprofile.replace_int_array("hidden_param", 20, request.child_value("hidden_param")) # Update user's unlock status if we aren't force unlocked game_config = self.get_game_config() if request.child("item") is not None: for child in request.child("item").children: if child.name != "info": continue item_id = child.child_value("id") item_type = child.child_value("type") param = child.child_value("param") diff_param = child.child_value("diff_param") if game_config.get_bool("force_unlock_songs") and item_type == self.GAME_CATALOG_TYPE_SONG: # Don't save back songs, because they were force unlocked continue if diff_param is not None: paramvals = { "diff_param": diff_param, "param": param, } else: paramvals = { "param": param, } self.data.local.user.put_achievement( self.game, self.version, userid, item_id, f"item_{item_type}", paramvals, ) # Grab last information. lastdict = newprofile.get_dict("last") lastdict.replace_int("headphone", request.child_value("headphone")) lastdict.replace_int("appeal_id", request.child_value("appeal_id")) lastdict.replace_int("comment_id", request.child_value("comment_id")) lastdict.replace_int("music_id", request.child_value("music_id")) lastdict.replace_int("music_type", request.child_value("music_type")) lastdict.replace_int("sort_type", request.child_value("sort_type")) lastdict.replace_int("narrow_down", request.child_value("narrow_down")) lastdict.replace_int("gauge_option", request.child_value("gauge_option")) # Save back last information gleaned from results newprofile.replace_dict("last", lastdict) # Keep track of play statistics self.update_play_statistics(userid) return newprofile