# vim: set fileencoding=utf-8 import copy import random from typing import Any, Dict, List, Optional, Set, Tuple from bemani.backend.jubeat.base import JubeatBase from bemani.backend.jubeat.common import ( JubeatDemodataGetHitchartHandler, JubeatDemodataGetNewsHandler, JubeatGamendRegisterHandler, JubeatGametopGetMeetingHandler, JubeatLobbyCheckHandler, JubeatLoggerReportHandler, ) from bemani.backend.jubeat.qubell import JubeatQubell from bemani.backend.base import Status from bemani.common import Time, ValidatedDict, VersionConstants from bemani.data import Data, Achievement, Score, Song, UserID from bemani.protocol import Node class JubeatClan( JubeatDemodataGetHitchartHandler, JubeatDemodataGetNewsHandler, JubeatGamendRegisterHandler, JubeatGametopGetMeetingHandler, JubeatLobbyCheckHandler, JubeatLoggerReportHandler, JubeatBase, ): name = 'Jubeat Clan' version = VersionConstants.JUBEAT_CLAN JBOX_EMBLEM_NORMAL = 1 JBOX_EMBLEM_PREMIUM = 2 EVENT_STATUS_OPEN = 0x1 EVENT_STATUS_COMPLETE = 0x2 EVENTS = { 5: { 'enabled': False, }, 6: { 'enabled': False, }, 15: { 'enabled': True, }, 22: { 'enabled': False, }, 23: { 'enabled': False, }, 34: { 'enabled': False, }, } FIVE_PLAYS_UNLOCK_EVENT_SONG_IDS = set(range(80000301, 80000348)) COURSE_STATUS_SEEN = 0x01 COURSE_STATUS_PLAYED = 0x02 COURSE_STATUS_CLEARED = 0x04 COURSE_TYPE_PERMANENT = 1 COURSE_TYPE_TIME_BASED = 2 COURSE_CLEAR_SCORE = 1 COURSE_CLEAR_COMBINED_SCORE = 2 COURSE_CLEAR_HAZARD = 3 COURSE_HAZARD_EXC1 = 1 COURSE_HAZARD_EXC2 = 2 COURSE_HAZARD_EXC3 = 3 COURSE_HAZARD_FC1 = 4 COURSE_HAZARD_FC2 = 5 COURSE_HAZARD_FC3 = 6 def previous_version(self) -> Optional[JubeatBase]: return JubeatQubell(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. Skip a particular song range since these are all a single song ID. # Jubeat Clan has an unlock event where you have to play different charts for the same song, and the charts are # loaded in based on the cabinet's prefecture. So, no matter where you are, you will only see one song within this # range, but it will be a different ID depending on the prefecture set in settings. This means its not safe to send # these song IDs, so we explicitly exclude them. 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 song.id not in cls.FIVE_PLAYS_UNLOCK_EVENT_SONG_IDS) 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 def __get_course_list(self) -> List[Dict[str, Any]]: return [ # Papricapcap courses { 'id': 1, 'name': 'Thank You Merry Christmas', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 3, 'score': 700000, 'music': [ [(50000077, 0), (50000077, 1), (50000077, 2)], [(80000080, 0), (80000080, 1), (80000080, 2)], [(50000278, 0), (50000278, 1), (50000278, 2)], ], }, { 'id': 2, 'name': 'はじめての山道', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 1, 'score': 700000, 'music': [ [(20000002, 0), (20000022, 0), (30000108, 0)], [(70000035, 0), (70000069, 0), (80000020, 0)], [(50000116, 0), (50000120, 0), (50000383, 0)], ], }, { 'id': 3, 'name': 'NOBOLOT検定 第1の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 1, 'score': 700000, 'music': [ [(20000109, 0), (50000218, 0), (60000100, 0)], [(50000228, 0), (70000125, 0)], [(70000109, 0)], ], }, { 'id': 4, 'name': 'アニメハイキング', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 2, 'score': 750000, 'music': [ [(70000028, 0)], [(70000030, 0)], [(80001009, 0)], ], }, { 'id': 5, 'name': 'しりとり山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 3, 'score': 750000, 'music': [ [(10000068, 0), (50000089, 0), (60000078, 0)], [(50000059, 0), (50000147, 0), (50000367, 0)], [(50000202, 0), (70000144, 0), (70000156, 0)], ], }, { 'id': 6, 'name': 'NOBOLOT検定 第2の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 3, 'score': 800000, 'music': [ [(50000268, 0), (70000039, 0), (70000160, 0)], [(60000080, 1), (80000014, 0)], [(60000053, 0)], ], }, # Harapenya-na courses { 'id': 11, 'name': 'おためし!い~あみゅちゃん', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 4, 'score': 700000, 'music': [ [(50000207, 0), (50000207, 1), (50000207, 2)], [(50000111, 0), (50000111, 1), (50000111, 2)], [(60000009, 0), (60000009, 1), (60000009, 2)], ], }, { 'id': 12, 'name': 'NOBOLOT検定 第3の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 4, 'score': 850000, 'music': [ [(40000110, 1), (70000059, 1), (70000131, 1)], [(30000004, 1), (80000035, 1)], [(40000051, 1)], ], }, { 'id': 13, 'name': '頂上から見えるお月様', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 5, 'score': 850000, 'music': [ [(50000245, 1)], [(60000051, 1)], [(80001011, 1)], ], }, { 'id': 14, 'name': 'ヒッチハイクでGO!GO!', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 5, 'score': 850000, 'music': [ [(10000053, 1), (80000038, 1)], [(30000123, 1), (50000086, 1), (70000119, 1)], [(50000196, 1), (60000006, 1), (70000153, 1)], ], }, { 'id': 15, 'name': '今日の一文字', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 6, 'score': 2600000, 'music': [ [(50000071, 1)], [(40000053, 1)], [(70000107, 1)], ], }, { 'id': 16, 'name': 'NOBOLOT検定 第4の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 6, 'score': 2650000, 'music': [ [(50000085, 2), (50000176, 2), (70000055, 2)], [(50000157, 2), (60001008, 2)], [(10000068, 2)], ], }, # Tillhorn courses { 'id': 21, 'name': 'ちくわの山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 7, 'score': 870000, 'music': [ [(70000099, 2)], [(50000282, 2), (60000106, 2), (80000041, 2)], [(50000234, 2)], ], }, { 'id': 22, 'name': 'NOBOLOT検定 第5の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 7, 'score': 2650000, 'music': [ [(50000233, 2), (50000242, 1), (80000032, 2)], [(60000027, 2), (60000045, 2)], [(20000038, 2)], ], }, { 'id': 23, 'name': '初めてのHARD MODE', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'hard': True, 'difficulty': 8, 'score': 835000, 'music': [ [(50000247, 2)], [(70000071, 2)], [(20000042, 2)], ], }, { 'id': 24, 'name': '雪山の上のお姫様', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 8, 'score': 2700000, 'music': [ [(50000101, 2)], [(50000119, 2), (50000174, 2), (60000009, 2)], [(80001010, 2)], ], }, { 'id': 25, 'name': 'なが~い山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 9, 'score': 2800000, 'music': [ [(70000170, 2), (80000013, 2)], [(70000161, 2), (80000057, 2)], [(80000043, 2)], ], }, { 'id': 26, 'name': 'NOBOLOT検定 第6の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 9, 'score': 2750000, 'music': [ [(50000034, 2), (50000252, 2), (50000347, 2)], [(70000117, 2), (70000138, 2)], [(50000078, 2)], ], }, # Bahaneroy courses { 'id': 31, 'name': '挑戦!い~あみゅちゃん', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 12, 'score': 2823829, 'music': [ [(50000207, 2)], [(50000111, 2)], [(60001006, 2)], ], }, { 'id': 32, 'name': '更なる高みを目指して', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'difficulty': 10, 'score': 920000, 'music': [ [(50000210, 2)], [(50000122, 2)], [(70000022, 2)], ], }, { 'id': 33, 'name': 'NOBOLOT検定 第7の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 10, 'score': 2800000, 'music': [ [(60000059, 2), (60000079, 2), (70000006, 2)], [(50000060, 2), (50000127, 2)], [(60000073, 2)], ], }, { 'id': 34, 'name': '崖っぷち! スリーチャレンジ!', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_HAZARD, 'hazard_type': self.COURSE_HAZARD_FC3, 'difficulty': 11, 'music': [ [(10000036, 2), (30000049, 2), (50000172, 2)], [(30000044, 2), (40000044, 2), (60000028, 2)], [(60000074, 2)], ], }, { 'id': 35, 'name': '芽吹いて咲いて', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 11, 'score': 2800000, 'music': [ [(60001003, 2)], [(70000097, 2)], [(80001013, 2)], ], }, { 'id': 36, 'name': '1! 2! Party Night!', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 12, 'score': 2800000, 'music': [ [(70000174, 2)], [(60000081, 2)], [(30000048, 2)], ], }, { 'id': 37, 'name': 'NOBOLOT検定 第8の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 12, 'score': 2820000, 'music': [ [(50000124, 2)], [(50000291, 2)], [(60000065, 2)], ], }, # Jolokili courses { 'id': 41, 'name': 'The 7th KAC 1st Stage 個人部門', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_SCORE, 'hard': True, 'difficulty': 13, 'score': 700000, 'music': [ [(80000076, 2)], [(80000025, 2)], [(60000073, 2)], ], }, { 'id': 42, 'name': 'The 7th KAC 2nd Stage 個人部門', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_SCORE, 'hard': True, 'difficulty': 13, 'score': 700000, 'music': [ [(80000081, 2)], [(70000145, 2)], [(80001013, 2)], ], }, { 'id': 43, 'name': 'The 7th KAC 団体部門', 'course_type': self.COURSE_TYPE_TIME_BASED, 'end_time': Time.end_of_this_week() + Time.SECONDS_IN_WEEK, 'clear_type': self.COURSE_CLEAR_SCORE, 'hard': True, 'difficulty': 13, 'score': 700000, 'music': [ [(70000162, 2)], [(70000134, 2)], [(70000173, 1)], ], }, { 'id': 44, 'name': 'ハードモード de ホームラン?!', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 13, 'score': 2750000, 'music': [ [(50000259, 2)], [(50000255, 2)], [(50000266, 2)], ], }, { 'id': 45, 'name': 'NOBOLOT検定 第9の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'difficulty': 13, 'score': 2830000, 'music': [ [(50000022, 2)], [(50000023, 2)], [(50000323, 2)], ], }, { 'id': 46, 'name': '崖っぷちスリーチャレンジ!その2', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_HAZARD, 'hazard_type': self.COURSE_HAZARD_EXC3, 'difficulty': 14, 'music': [ [(50000024, 2), (50000160, 2), (70000065, 2)], [(30000122, 2), (50000178, 2), (50000383, 2)], [(50000122, 2), (50000261, 2), (80000010, 2)], ], }, { 'id': 47, 'name': 'もう一つの姿を求めて', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_SCORE, 'hard': True, 'difficulty': 14, 'score': 920000, 'music': [ [(60001009, 2)], [(80001006, 2)], [(80001015, 2)], ], }, { 'id': 48, 'name': 'NOBOLOT検定 第10の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 14, 'score': 2820000, 'music': [ [(50000202, 2), (50000203, 2), (70000108, 2)], [(40000046, 2), (40000057, 2)], [(50000134, 2)], ], }, # Calorest courses { 'id': 51, 'name': '流れに身を任せて', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 15, 'score': 2850000, 'music': [ [(60000001, 2)], [(80000022, 2)], [(50000108, 2)], ], }, { 'id': 52, 'name': '【挑戦】NOBOLOT検定 神の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 15, 'score': 2850000, 'music': [ [(40000057, 2)], [(60000076, 2)], [(50000102, 2)], ], }, { 'id': 53, 'name': '伝説の伝導師の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 16, 'score': 2960000, 'music': [ [(80000028, 2)], [(80000023, 2)], [(80000087, 2)], ], }, { 'id': 54, 'name': 'EXCELLENT MASTER', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_HAZARD, 'hazard_type': self.COURSE_HAZARD_EXC1, 'difficulty': 16, 'music': [ [(20000125, 2), (50000330, 2), (40000060, 2)], [(30000127, 2), (50000206, 2), (50000253, 2)], [(70000011, 2)], ], }, { 'id': 55, 'name': '【挑戦】NOBOLOT検定 英雄の山', 'course_type': self.COURSE_TYPE_PERMANENT, 'clear_type': self.COURSE_CLEAR_COMBINED_SCORE, 'hard': True, 'difficulty': 16, 'score': 2980000, 'music': [ [(50000100, 2)], [(70000110, 2)], [(50000208, 2)], ], }, ] def __get_global_info(self) -> Node: info = Node.void('info') # Event info. 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', self.EVENT_STATUS_OPEN 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) genre_def_music = Node.void('genre_def_music') info.add_child(genre_def_music) info.add_child(Node.s32_array( 'black_jacket_list', [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], )) # 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)) # 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')) expert_option = Node.void('expert_option') info.add_child(expert_option) expert_option.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)) team = Node.void('team') all_music_matching.add_child(team) team.add_child(Node.s32('default_flag', 0)) team.add_child(Node.s32('redbelk_flag', 0)) team.add_child(Node.s32('cyanttle_flag', 0)) team.add_child(Node.s32('greenesia_flag', 0)) team.add_child(Node.s32('plumpark_flag', 0)) question_list = Node.void('question_list') info.add_child(question_list) drop_list = Node.void('drop_list') info.add_child(drop_list) daily_bonus_list = Node.void('daily_bonus_list') info.add_child(daily_bonus_list) department = Node.void('department') info.add_child(department) department.add_child(Node.void('pack_list')) # Set up NOBOLOT course requirements clan_course_list = Node.void('clan_course_list') info.add_child(clan_course_list) valid_courses: Set[int] = set() for course in self.__get_course_list(): if course['id'] < 1: raise Exception(f"Invalid course ID {course['id']} found in course list!") if course['id'] in valid_courses: raise Exception(f"Duplicate ID {course['id']} found in course list!") if course['clear_type'] == self.COURSE_CLEAR_HAZARD and 'hazard_type' not in course: raise Exception(f"Need 'hazard_type' set in course {course['id']}!") if course['course_type'] == self.COURSE_TYPE_TIME_BASED and 'end_time' not in course: raise Exception(f"Need 'end_time' set in course {course['id']}!") if course['clear_type'] in [self.COURSE_CLEAR_SCORE, self.COURSE_CLEAR_COMBINED_SCORE] and 'score' not in course: raise Exception(f"Need 'score' set in course {course['id']}!") if course['clear_type'] == self.COURSE_CLEAR_SCORE and course['score'] > 1000000: raise Exception(f"Invalid per-coure score in course {course['id']}!") if course['clear_type'] == self.COURSE_CLEAR_COMBINED_SCORE and course['score'] <= 1000000: raise Exception(f"Invalid combined score in course {course['id']}!") valid_courses.add(course['id']) # Basics clan_course = Node.void('clan_course') clan_course_list.add_child(clan_course) clan_course.set_attribute('release_code', '2017062600') clan_course.set_attribute('version_id', '0') clan_course.set_attribute('id', str(course['id'])) clan_course.set_attribute('course_type', str(course['course_type'])) clan_course.add_child(Node.s32('difficulty', course['difficulty'])) clan_course.add_child(Node.u64('etime', (course['end_time'] if 'end_time' in course else 0) * 1000)) clan_course.add_child(Node.string('name', course['name'])) # List of included songs tune_list = Node.void('tune_list') clan_course.add_child(tune_list) for order, charts in enumerate(course['music']): tune = Node.void('tune') tune_list.add_child(tune) tune.set_attribute('no', str(order + 1)) seq_list = Node.void('seq_list') tune.add_child(seq_list) for songid, chart in charts: seq = Node.void('seq') seq_list.add_child(seq) seq.add_child(Node.s32('music_id', songid)) seq.add_child(Node.s32('difficulty', chart)) seq.add_child(Node.bool('is_secret', False)) # Clear criteria clear = Node.void('clear') clan_course.add_child(clear) ex_option = Node.void('ex_option') clear.add_child(ex_option) ex_option.add_child(Node.bool('is_hard', course['hard'] if 'hard' in course else False)) ex_option.add_child(Node.s32('hazard_type', course['hazard_type'] if 'hazard_type' in course else 0)) clear.set_attribute('type', str(course['clear_type'])) clear.add_child(Node.s32('score', course['score'] if 'score' in course else 0)) reward_list = Node.void('reward_list') clear.add_child(reward_list) # Set up NOBOLOT category display category_list = Node.void('category_list') clan_course_list.add_child(category_list) # Each category has one of the following nodes categories: List[Tuple[int, int]] = [ (1, 3), (4, 6), (7, 9), (10, 12), (13, 14), (15, 16), ] for categoryid, (min_level, max_level) in enumerate(categories): category = Node.void('category') category_list.add_child(category) category.set_attribute('id', str(categoryid + 1)) category.add_child(Node.bool('is_secret', False)) category.add_child(Node.s32('level_min', min_level)) category.add_child(Node.s32('level_max', max_level)) 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_demodata_get_info_request(self, request: Node) -> Node: root = Node.void('demodata') data = Node.void('data') root.add_child(data) info = Node.void('info') data.add_child(info) info.add_child(Node.s32_array( 'black_jacket_list', [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], )) return root def handle_demodata_get_jbox_list_request(self, request: Node) -> Node: root = Node.void('demodata') return root def handle_jbox_get_agreement_request(self, request: Node) -> Node: root = Node.void('jbox') root.add_child(Node.bool('is_agreement', True)) return root def handle_jbox_get_list_request(self, request: Node) -> Node: root = Node.void('jbox') root.add_child(Node.void('selection_list')) return root 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) 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_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_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_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: profile.replace_int_array('emblem_list', 96, item.child_value('emblem_list')) # 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_scores(self, userid: UserID, profile: ValidatedDict, 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.get_int('extid'))) playdata = Node.void('mdata_list') player.add_child(playdata) music = ValidatedDict() for score in scores: data = music.get_dict(str(score.id)) play_cnt = data.get_int_array('play_cnt', 3) clear_cnt = data.get_int_array('clear_cnt', 3) clear_flags = data.get_int_array('clear_flags', 3) fc_cnt = data.get_int_array('fc_cnt', 3) ex_cnt = data.get_int_array('ex_cnt', 3) points = data.get_int_array('points', 3) # Replace data for this chart type play_cnt[score.chart] = score.plays clear_cnt[score.chart] = score.data.get_int('clear_count') fc_cnt[score.chart] = score.data.get_int('full_combo_count') ex_cnt[score.chart] = score.data.get_int('excellent_count') points[score.chart] = score.points # Format the clear flags clear_flags[score.chart] = self.GAME_FLAG_BIT_PLAYED if score.data.get_int('clear_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_CLEARED if score.data.get_int('full_combo_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_FULL_COMBO if score.data.get_int('excellent_count') > 0: clear_flags[score.chart] |= self.GAME_FLAG_BIT_EXCELLENT # Save chart data back data.replace_int_array('play_cnt', 3, play_cnt) data.replace_int_array('clear_cnt', 3, clear_cnt) data.replace_int_array('clear_flags', 3, clear_flags) data.replace_int_array('fc_cnt', 3, fc_cnt) data.replace_int_array('ex_cnt', 3, ex_cnt) data.replace_int_array('points', 3, points) # Update the ghost (untyped) ghost = data.get('ghost', [None, None, None]) ghost[score.chart] = score.data.get('ghost') data['ghost'] = ghost # Save it back if score.id in self.FIVE_PLAYS_UNLOCK_EVENT_SONG_IDS: # Mirror it to every version so the score shows up regardless of # prefecture setting. for prefecture_id in self.FIVE_PLAYS_UNLOCK_EVENT_SONG_IDS: music.replace_dict(str(prefecture_id), data) else: # Regular copy. music.replace_dict(str(score.id), data) for scoreid in music: scoredata = music.get_dict(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))) for i, ghost in enumerate(scoredata.get('ghost', [None, None, None])): if ghost is None: continue bar = Node.u8_array('bar', ghost) musicdata.add_child(bar) bar.set_attribute('seq', str(i)) return root def format_profile(self, userid: UserID, profile: ValidatedDict) -> Node: root = Node.void('gametop') data = Node.void('data') root.add_child(data) # Jubeat Clan appears to allow full event overrides per-player data.add_child(self.__get_global_info()) player = Node.void('player') data.add_child(player) # Basic profile info player.add_child(Node.string('name', profile.get_str('name', 'なし'))) player.add_child(Node.s32('jid', profile.get_int('extid'))) # Miscelaneous crap player.add_child(Node.s32('session_id', 1)) player.add_child(Node.u64('event_flag', profile.get_int('event_flag'))) # Player info and statistics info = Node.void('info') player.add_child(info) 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'))) # 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'))) 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.s16_array('emblem', lastdict.get_int_array('emblem', 5))) settings.add_child(Node.s8('matching', lastdict.get_int('matching'))) settings.add_child(Node.s8('hard', lastdict.get_int('hard'))) settings.add_child(Node.s8('hazard', lastdict.get_int('hazard'))) # 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', profile.get_int_array('secret_list', 64, [-1] * 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', profile.get_int_array('emblem_list', 96, [-1] * 96))) item.add_child(Node.s32_array('commu_list', profile.get_int_array('commu_list', 16, [-1] * 16))) new = Node.void('new') item.add_child(new) new.add_child(Node.s32_array('secret_list', profile.get_int_array('secret_list_new', 64, [-1] * 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.get_int('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.version, userid, entry.get_int('today', -1), timelimit=start_time) whim_attempts = self.data.local.music.get_all_attempts(self.game, self.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. official_news = Node.void('official_news') player.add_child(official_news) news_list = Node.void('news_list') official_news.add_child(news_list) # 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)) # Player status for events event_info = Node.void('event_info') player.add_child(event_info) achievements = self.data.local.user.get_achievements(self.game, self.version, userid) event_completion: Dict[int, bool] = {} course_completion: Dict[int, ValidatedDict] = {} for achievement in achievements: if achievement.type == 'event': event_completion[achievement.id] = achievement.data.get_bool('is_completed') if achievement.type == 'course': course_completion[achievement.id] = achievement.data for eventid, eventdata in self.EVENTS.items(): # There are two significant bits here, bit 0 and bit 1, 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(eventid)) state = 0x0 state |= self.EVENT_STATUS_OPEN if eventdata['enabled'] else 0 state |= self.EVENT_STATUS_COMPLETE if event_completion.get(eventid, False) else 0 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. gameitems = self.data.local.game.get_items(self.game, self.version) normalemblems: Set[int] = set() premiumemblems: Set[int] = set() for gameitem in gameitems: if gameitem.type == 'emblem': if gameitem.data.get_int('rarity') in {1, 2, 3}: normalemblems.add(gameitem.id) if gameitem.data.get_int('rarity') in {3, 4, 5}: premiumemblems.add(gameitem.id) # Default to some emblems in case the catalog is not available. normalindex = 2 premiumindex = 1 if normalemblems: normalindex = random.sample(normalemblems, 1)[0] if premiumemblems: premiumindex = random.sample(premiumemblems, 1)[0] normal.add_child(Node.s16('index', normalindex)) premium.add_child(Node.s16('index', premiumindex)) # New Music stuff new_music = Node.void('new_music') player.add_child(new_music) navi = Node.void('navi') player.add_child(navi) navi.add_child(Node.u64('flag', profile.get_int('navi_flag'))) # 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) # Player Jubility jubility = Node.void('jubility') player.add_child(jubility) jubility.set_attribute('param', str(profile.get_int('jubility'))) target_music_list = Node.void('target_music_list') jubility.add_child(target_music_list) # Calculate top 30 songs contributing to jubility. jubeat_entries: List[ValidatedDict] = [] for achievement in achievements: if achievement.type != 'jubility': continue # Figure out for each song, what's the highest value jubility and # keep that. bestentry = ValidatedDict() for chart in [0, 1, 2]: entry = achievement.data.get_dict(str(chart)) if entry.get_int("value") >= bestentry.get_int("value"): bestentry = copy.deepcopy(entry) bestentry.replace_int("songid", achievement.id) bestentry.replace_int("chart", chart) jubeat_entries.append(bestentry) jubeat_entries = sorted(jubeat_entries, key=lambda entry: entry.get_int("value"), reverse=True) # Now, give the game the list. for i, entry in enumerate(jubeat_entries): # The game only reads the top 30 anyway, so skip extra network traffic. if i >= 30: break target_music = Node.void("target_music") target_music_list.add_child(target_music) target_music.add_child(Node.s32("music_id", entry.get_int("songid"))) target_music.add_child(Node.s8("seq", entry.get_int("chart"))) target_music.add_child(Node.s32("score", entry.get_int("score"))) target_music.add_child(Node.s32("value", entry.get_int("value"))) target_music.add_child(Node.bool("is_hard_mode", entry.get_bool("hard_mode"))) # Team stuff team = Node.void('team') teamdict = profile.get_dict('team') player.add_child(team) team.set_attribute('id', str(teamdict.get_int('id'))) team.add_child(Node.s32("section", teamdict.get_int('section'))) team.add_child(Node.s32("street", teamdict.get_int('street'))) team.add_child(Node.s32("house_number_1", teamdict.get_int('house_1'))) team.add_child(Node.s32("house_number_2", teamdict.get_int('house_2'))) # Set up where the player moves to (random) after their first play move = Node.void('move') team.add_child(move) # 1 - Redbelk, 2 - Cyantle, 3 - Greenesia, 4 - Plumpark move.set_attribute('id', str(random.choice([1, 2, 3, 4]))) move.set_attribute("section", str(random.choice([1, 2, 3, 4, 5]))) move.set_attribute("street", str(random.choice([1, 2, 3, 4, 5, 6]))) move.set_attribute("house_number_1", str(random.choice(range(10, 100)))) move.set_attribute("house_number_2", str(random.choice(range(10, 100)))) # Union Battle union_battle = Node.void('union_battle') player.add_child(union_battle) union_battle.set_attribute('id', "-1") union_battle.add_child(Node.s32("power", 0)) # Some server node server = Node.void('server') player.add_child(server) # Another unknown gift list? eamuse_gift_list = Node.void('eamuse_gift_list') player.add_child(eamuse_gift_list) # Clan Course List Progress clan_course_list = Node.void('clan_course_list') player.add_child(clan_course_list) # Each course that we have completed has one of the following nodes. for course in self.__get_course_list(): status_dict = course_completion.get(course['id'], ValidatedDict()) status = 0 status |= self.COURSE_STATUS_SEEN if status_dict.get_bool('seen') else 0 status |= self.COURSE_STATUS_PLAYED if status_dict.get_bool('played') else 0 status |= self.COURSE_STATUS_CLEARED if status_dict.get_bool('cleared') else 0 clan_course = Node.void('clan_course') clan_course_list.add_child(clan_course) clan_course.set_attribute('id', str(course['id'])) clan_course.add_child(Node.s8('status', status)) category_list = Node.void('category_list') player.add_child(category_list) # Each category has one of the following nodes for categoryid in range(1, 7): category = Node.void('category') category_list.add_child(category) category.set_attribute('id', str(categoryid)) category.add_child(Node.bool('is_display', True)) # Drop list drop_list = Node.void('drop_list') player.add_child(drop_list) dropachievements: Dict[int, Achievement] = {} for achievement in achievements: if achievement.type == 'drop': dropachievements[achievement.id] = achievement for dropid in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]: if dropid in dropachievements: dropdata = dropachievements[dropid].data else: dropdata = ValidatedDict() drop = Node.void('drop') drop_list.add_child(drop) drop.set_attribute('id', str(dropid)) drop.add_child(Node.s32('exp', dropdata.get_int('exp', -1))) drop.add_child(Node.s32('flag', dropdata.get_int('flag', 0))) item_list = Node.void('item_list') drop.add_child(item_list) for itemid in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]: item = Node.void('item') item_list.add_child(item) item.set_attribute('id', str(itemid)) item.add_child(Node.s32('num', dropdata.get_int(f'item_{itemid}'))) # Fill in category fill_in_category = Node.void('fill_in_category') player.add_child(fill_in_category) fill_in_category.add_child(Node.s32_array('no_gray_flag_list', profile.get_int_array('no_gray_flag_list', 16, [-1] * 16))) fill_in_category.add_child(Node.s32_array('all_yellow_flag_list', profile.get_int_array('all_yellow_flag_list', 16, [-1] * 16))) fill_in_category.add_child(Node.s32_array('full_combo_flag_list', profile.get_int_array('full_combo_flag_list', 16, [-1] * 16))) fill_in_category.add_child(Node.s32_array('excellent_flag_list', profile.get_int_array('excellent_flag_list', 16, [-1] * 16))) # Daily Bonus daily_bonus_list = Node.void('daily_bonus_list') player.add_child(daily_bonus_list) # Tickets ticket_list = Node.void('ticket_list') player.add_child(ticket_list) return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict: newprofile = copy.deepcopy(oldprofile) data = request.child('data') # 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('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('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('secret_list', 64, item.child_value('secret_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')) newprofile.replace_int_array('emblem_list', 96, item.child_value('emblem_list')) newprofile.replace_int_array('commu_list', 96, item.child_value('commu_list')) newitem = item.child('new') if newitem is not None: newprofile.replace_int_array('secret_list_new', 64, newitem.child_value('secret_list')) 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')) # Grab categories stuff fill_in_category = player.child('fill_in_category') if fill_in_category is not None: newprofile.replace_int_array('no_gray_flag_list', 16, fill_in_category.child_value('no_gray_flag_list')) newprofile.replace_int_array('all_yellow_flag_list', 16, fill_in_category.child_value('all_yellow_flag_list')) newprofile.replace_int_array('full_combo_flag_list', 16, fill_in_category.child_value('full_combo_flag_list')) newprofile.replace_int_array('excellent_flag_list', 16, fill_in_category.child_value('excellent_flag_list')) # 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) # Team stuff team = player.child('team') teamdict = newprofile.get_dict('team') if team is not None: teamdict.replace_int('id', int(team.attribute('id'))) teamdict.replace_int('section', team.child_value('section')) teamdict.replace_int('street', team.child_value('street')) teamdict.replace_int('house_1', team.child_value('house_number_1')) teamdict.replace_int('house_2', team.child_value('house_number_2')) newprofile.replace_dict('team', teamdict) # Drop list drop_list = player.child('drop_list') if drop_list is not None: for drop in drop_list.children: try: dropid = int(drop.attribute('id')) except TypeError: # Unrecognized drop continue exp = drop.child_value('exp') flag = drop.child_value('flag') items: Dict[int, int] = {} item_list = drop.child('item_list') if item_list is not None: for item in item_list.children: try: itemid = int(item.attribute('id')) except TypeError: # Unrecognized item continue items[itemid] = item.child_value('num') olddrop = self.data.local.user.get_achievement( self.game, self.version, userid, dropid, 'drop', ) if olddrop is None: # Create a new event structure for this olddrop = ValidatedDict() olddrop.replace_int('exp', exp) olddrop.replace_int('flag', flag) for itemid, num in items.items(): olddrop.replace_int(f'item_{itemid}', num) # Save it as an achievement self.data.local.user.put_achievement( self.game, self.version, userid, dropid, 'drop', olddrop, ) # event stuff newprofile.replace_int('event_flag', player.child_value('event_flag')) 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, ) # 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') # Fix mapping to song IDs for the song with seven billion charts # due to the prefecture unlock event. songid = tune.child_value('music') if songid in self.FIVE_PLAYS_UNLOCK_EVENT_SONG_IDS: songid = 80000301 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 jubility jubility = player.child('jubility') if jubility is not None: newprofile.replace_int('jubility', int(jubility.attribute('param'))) target_music_list = jubility.child('target_music_list') if target_music_list is not None: for target_music in target_music_list.children: if target_music.name != "target_music": continue songid = target_music.child_value("music_id") chart = target_music.child_value("seq") score = target_music.child_value("score") value = target_music.child_value("value") hard_mode = target_music.child_value("is_hard_mode") # Update jubility value tracking oldjubility = self.data.local.user.get_achievement( self.game, self.version, userid, songid, 'jubility', ) if oldjubility is None: # Create a new jubility structure for this oldjubility = ValidatedDict() # Grab the entry for this sequence entry = oldjubility.get_dict(str(chart)) if value >= entry.get_int("value"): entry.replace_int('score', score) entry.replace_int('value', value) entry.replace_bool('hard_mode', hard_mode) oldjubility.replace_dict(str(chart), entry) # Save it as an achievement self.data.local.user.put_achievement( self.game, self.version, userid, songid, 'jubility', oldjubility, ) # Clan course saving clan_course_list = player.child('clan_course_list') if clan_course_list is not None: for course in clan_course_list.children: if course.name != 'clan_course': continue courseid = int(course.attribute('id')) status = course.child_value('status') is_seen = (status & self.COURSE_STATUS_SEEN) != 0 is_played = (status & self.COURSE_STATUS_PLAYED) != 0 # Update seen status and played status oldcourse = self.data.local.user.get_achievement( self.game, self.version, userid, courseid, 'course', ) if oldcourse is None: # Create a new course structure for this oldcourse = ValidatedDict() oldcourse.replace_bool('seen', oldcourse.get_bool('seen') or is_seen) oldcourse.replace_bool('played', oldcourse.get_bool('played') or is_played) # Save it as an achievement self.data.local.user.put_achievement( self.game, self.version, userid, courseid, 'course', oldcourse, ) select_course = player.child('select_course') if select_course is not None: try: courseid = int(select_course.attribute('id')) except Exception: courseid = 0 cleared = select_course.child_value('is_cleared') if courseid > 0 and cleared: # Update course cleared status oldcourse = self.data.local.user.get_achievement( self.game, self.version, userid, courseid, 'course', ) if oldcourse is None: # Create a new course structure for this oldcourse = ValidatedDict() oldcourse.replace_bool('cleared', True) # Save it as an achievement self.data.local.user.put_achievement( self.game, self.version, userid, courseid, 'course', oldcourse, ) # Save back last information gleaned from results newprofile.replace_dict('last', last) # Keep track of play statistics self.update_play_statistics(userid) return newprofile