diff --git a/bemani/backend/popn/base.py b/bemani/backend/popn/base.py index a52af3d..d23a737 100644 --- a/bemani/backend/popn/base.py +++ b/bemani/backend/popn/base.py @@ -18,6 +18,7 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base): game = GameConstants.POPN_MUSIC # Play medals, as saved into/loaded from the DB + PLAY_MEDAL_NO_PLAY = DBConstants.POPN_MUSIC_PLAY_MEDAL_NO_PLAY PLAY_MEDAL_CIRCLE_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED PLAY_MEDAL_DIAMOND_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED PLAY_MEDAL_STAR_FAILED = DBConstants.POPN_MUSIC_PLAY_MEDAL_STAR_FAILED @@ -188,6 +189,7 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base): """ # Range check medals if medal not in [ + self.PLAY_MEDAL_NO_PLAY, self.PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED, diff --git a/bemani/backend/popn/eclale.py b/bemani/backend/popn/eclale.py index 53b1a63..a9546a9 100644 --- a/bemani/backend/popn/eclale.py +++ b/bemani/backend/popn/eclale.py @@ -276,6 +276,8 @@ class PopnMusicEclale(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue points = score.points medal = score.data.get_int('medal') @@ -364,6 +366,8 @@ class PopnMusicEclale(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue points = score.points medal = score.data.get_int('medal') @@ -454,6 +458,8 @@ class PopnMusicEclale(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue music = Node.void('music') root.add_child(music) diff --git a/bemani/backend/popn/fantasia.py b/bemani/backend/popn/fantasia.py index 937ff0d..aa61834 100644 --- a/bemani/backend/popn/fantasia.py +++ b/bemani/backend/popn/fantasia.py @@ -198,6 +198,8 @@ class PopnMusicFantasia(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) hiscore_index = (score.id * 4) + { @@ -289,6 +291,8 @@ class PopnMusicFantasia(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) hiscore_index = (score.id * 4) + { @@ -567,6 +571,8 @@ class PopnMusicFantasia(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) hiscore_index = (score.id * 4) + { diff --git a/bemani/backend/popn/lapistoria.py b/bemani/backend/popn/lapistoria.py index 71b5ef7..ffdb056 100644 --- a/bemani/backend/popn/lapistoria.py +++ b/bemani/backend/popn/lapistoria.py @@ -1,12 +1,12 @@ # vim: set fileencoding=utf-8 import copy -from typing import Dict, List +from typing import Any, Dict, List from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.sunnypark import PopnMusicSunnyPark from bemani.backend.base import Status -from bemani.common import Profile, VersionConstants, ID +from bemani.common import ValidatedDict, Profile, VersionConstants, ID from bemani.data import UserID, Link from bemani.protocol import Node @@ -23,6 +23,7 @@ class PopnMusicLapistoria(PopnMusicBase): GAME_CHART_TYPE_EX = 3 # Medal type, as returned from the game + GAME_PLAY_MEDAL_NO_PLAY = 0 GAME_PLAY_MEDAL_CIRCLE_FAILED = 1 GAME_PLAY_MEDAL_DIAMOND_FAILED = 2 GAME_PLAY_MEDAL_STAR_FAILED = 3 @@ -41,48 +42,93 @@ class PopnMusicLapistoria(PopnMusicBase): def previous_version(self) -> PopnMusicBase: return PopnMusicSunnyPark(self.data, self.config, self.model) + @classmethod + def get_settings(cls) -> Dict[str, Any]: + """ + Return all of our front-end modifiably settings. + """ + return { + 'ints': [ + { + 'name': 'Story Mode', + 'tip': 'Story mode phase for all players.', + 'category': 'game_config', + 'setting': 'story_phase', + 'values': { + 0: 'Disabled', + 1: 'Phase 1', + 2: 'Phase 2', + 3: 'Phase 3', + 4: 'Phase 4', + 5: 'Phase 5', + 6: 'Phase 6', + 7: 'Phase 7', + 8: 'Phase 8', + 9: 'Phase 9', + 10: 'Phase 10', + 11: 'Phase 11', + 12: 'Phase 12', + 13: 'Phase 13', + 14: 'Phase 14', + 15: 'Phase 15', + 16: 'Phase 16', + 17: 'Phase 17', + 18: 'Phase 18', + 19: 'Phase 19', + 20: 'Phase 20', + 21: 'Phase 21', + 22: 'Phase 22', + 23: 'Phase 23', + 24: 'Phase 24', + }, + }, + ], + } + def handle_info22_common_request(self, request: Node) -> Node: - # TODO: Hook these up to config so we can change this + game_config = self.get_game_config() + story_phase = game_config.get_int('story_phase') + phases = { - # Unknown event (0-16) - 0: 0, - # Unknown event (0-11) - 1: 0, + # Default song phase availability (0-16) + 0: 16, + # Card phase (0-11) + 1: 11, # Pop'n Aura, max (0-11) (remove all aura requirements) 2: 11, # Story (0-24) - 3: 1, - # BEMANI ruins Discovery! (0-2) + 3: story_phase, + # BEMANI ruins Discovery! 0 = off, 1 = active, 2 = off 4: 0, # Unknown event, something to do with net taisen (0-2) - 5: 0, + 5: 2, # Unknown event (0-1) - 6: 0, + 6: 1, # Unknown event (0-1) - 7: 0, + 7: 1, # Unknown event (0-1) - 8: 0, - # Unknown event (0-11) - 9: 0, - # Unknown event (0-2) + 8: 1, + # Course mode phase (0-11) + 9: 11, + # Pon's Fate Purification Plan, 0 = off, 1 = active, 2 = off 10: 0, # Unknown event (0-3) - 11: 0, + 11: 3, # Unknown event (0-1) - 12: 0, - # Unknown event (0-2) - 13: 0, + 12: 1, + # Appears to be unlocks for course mode including KAC stuff. + 13: 2, # Unknown event (0-4) - 14: 0, + 14: 4, # Unknown event (0-2) - 15: 0, + 15: 2, # Unknown event (0-2) - 16: 0, + 16: 2, # Unknown event (0-12) 17: 0, # Unknown event (0-2) - 18: 0, - # Unknown event (0-7) + 18: 2, + # Bemani Summer Diary, 0 = off, 1-6 are phases, 7 = off 19: 0, } @@ -198,6 +244,7 @@ class PopnMusicLapistoria(PopnMusicBase): rivalid = links[no].other_userid rivalprofile = profiles[rivalid] scores = self.data.remote.music.get_scores(self.game, self.version, rivalid) + achievements = self.data.local.user.get_achievements(self.game, self.version, rivalid) # First, output general profile info. friend = Node.void('friend') @@ -234,6 +281,7 @@ class PopnMusicLapistoria(PopnMusicBase): }[score.chart])) music.set_attribute('score', str(points)) music.set_attribute('clearmedal', str({ + self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY, self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED, @@ -247,6 +295,27 @@ class PopnMusicLapistoria(PopnMusicBase): self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT, }[medal])) + for course in achievements: + if course.type == 'course': + total_score = course.data.get_int('total_score') + clear_medal = course.data.get_int('clear_medal') + clear_norma = course.data.get_int('clear_norma') + stage1_score = course.data.get_int('stage1_score') + stage2_score = course.data.get_int('stage2_score') + stage3_score = course.data.get_int('stage3_score') + stage4_score = course.data.get_int('stage4_score') + + coursenode = Node.void('course') + friend.add_child(coursenode) + coursenode.set_attribute('course_id', str(course.id)) + coursenode.set_attribute('clear_medal', str(clear_medal)) + coursenode.set_attribute('clear_norma', str(clear_norma)) + coursenode.set_attribute('stage1_score', str(stage1_score)) + coursenode.set_attribute('stage2_score', str(stage2_score)) + coursenode.set_attribute('stage3_score', str(stage3_score)) + coursenode.set_attribute('stage4_score', str(stage4_score)) + coursenode.set_attribute('total_score', str(total_score)) + return root def handle_player22_conversion_request(self, request: Node) -> Node: @@ -287,6 +356,7 @@ class PopnMusicLapistoria(PopnMusicBase): 'bad': request.child_value('bad') } medal = { + self.GAME_PLAY_MEDAL_NO_PLAY: self.PLAY_MEDAL_NO_PLAY, self.GAME_PLAY_MEDAL_CIRCLE_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED, self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED, self.GAME_PLAY_MEDAL_STAR_FAILED: self.PLAY_MEDAL_STAR_FAILED, @@ -302,6 +372,119 @@ class PopnMusicLapistoria(PopnMusicBase): self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats) return root + def handle_player22_write_course_request(self, request: Node) -> Node: + refid = request.child_value('ref_id') + + root = Node.void('player22') + if refid is None: + return root + + userid = self.data.remote.user.from_refid(self.game, self.version, refid) + if userid is None: + return root + + # Grab info that we want to update + total_score = request.child_value('total_score') or 0 + course_id = request.child_value('course_id') + if course_id is not None: + machine = self.data.local.machine.get_machine(self.config.machine.pcbid) + pref = request.child_value('pref') or 51 + profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0) + + course = self.data.local.user.get_achievement( + self.game, + self.version, + userid, + course_id, + "course", + ) or ValidatedDict({}) + + stage_scores: Dict[int, int] = {} + for child in request.children: + if child.name != 'stage': + continue + + stage = child.child_value('stage') + score = child.child_value('score') + + if isinstance(stage, int) and isinstance(score, int): + stage_scores[stage] = score + + # Update the scores if this was a new high score. + if total_score > course.get_int('total_score'): + course.replace_int('total_score', total_score) + course.replace_int('stage1_score', stage_scores.get(0, 0)) + course.replace_int('stage2_score', stage_scores.get(1, 0)) + course.replace_int('stage3_score', stage_scores.get(2, 0)) + course.replace_int('stage4_score', stage_scores.get(3, 0)) + + # Only update ojamas used if this was an updated score. + course.replace_int('clear_norma', request.child_value('clear_norma')) + + # Only udpate what location and prefecture this was scored in + # if we updated our score. + course.replace_int('pref', pref) + course.replace_int('lid', machine.arcade) + + # Update medal and combo values. + course.replace_int('max_combo', max(course.get_int('max_combo'), request.child_value('max_combo'))) + course.replace_int('clear_medal', max(course.get_int('clear_medal'), request.child_value('clear_medal'))) + + # Add one to the play count for this course. + course.increment_int('play_cnt') + + self.data.local.user.put_achievement( + self.game, + self.version, + userid, + course_id, + "course", + course, + ) + + # Now, attempt to calculate ranking for this user for this run. + all_courses = self.data.local.user.get_all_achievements(self.game, self.version, course_id, "course") + global_ranking = sorted(all_courses, key=lambda entry: entry[1].data.get_int('total_score'), reverse=True) + pref_ranking = [c for c in global_ranking if c[1].data.get_int('pref') == pref] + local_ranking = [c for c in global_ranking if c[1].data.get_int('lid') == machine.arcade] + + global_rank = len(global_ranking) + pref_rank = len(pref_ranking) + local_rank = len(local_ranking) + + for i, rank in enumerate(global_ranking): + if userid == rank[0]: + global_rank = i + 1 + break + for i, rank in enumerate(pref_ranking): + if userid == rank[0]: + pref_rank = i + 1 + break + for i, rank in enumerate(local_ranking): + if userid == rank[0]: + local_rank = i + 1 + break + + # Now, return it all. + for rank_type, personal_rank, count in [ + ('all_ranking', global_rank, len(global_ranking)), + ('pref_ranking', pref_rank, len(pref_ranking)), + ('location_ranking', local_rank, len(local_ranking)), + ]: + ranknode = Node.void(rank_type) + root.add_child(ranknode) + ranknode.add_child(Node.string('name', profile.get_str('name', 'なし'))) + ranknode.add_child(Node.s16('chara_num', profile.get_int('chara', -1))) + ranknode.add_child(Node.s32('stage1_score', stage_scores.get(0, 0))) + ranknode.add_child(Node.s32('stage2_score', stage_scores.get(1, 0))) + ranknode.add_child(Node.s32('stage3_score', stage_scores.get(2, 0))) + ranknode.add_child(Node.s32('stage4_score', stage_scores.get(3, 0))) + ranknode.add_child(Node.s32('total_score', total_score)) + ranknode.add_child(Node.s16('player_count', count)) + ranknode.add_child(Node.s16('player_rank', personal_rank)) + + return root + def format_profile(self, userid: UserID, profile: Profile) -> Node: root = Node.void('player22') @@ -380,6 +563,7 @@ class PopnMusicLapistoria(PopnMusicBase): music.add_child(Node.s16('cnt', score.plays)) music.add_child(Node.s32('score', points)) music.add_child(Node.u8('clear_type', { + self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY, self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED, @@ -481,6 +665,16 @@ class PopnMusicLapistoria(PopnMusicBase): itemtype = achievement.data.get_int('type') param = achievement.data.get_int('param') + # Maximum for each type is as follows: + # 0, 1423 - These are song unlocks as far as I can tell, matches Eclale/UsaNeko. + # 1, 2040 + # 2, 510 + # 3, 173 + # 4, 40 + # 5, 24 + # 6, 24 + # 7, 4158 + item = Node.void('item') root.add_child(item) item.add_child(Node.u8('type', itemtype)) @@ -518,6 +712,34 @@ class PopnMusicLapistoria(PopnMusicBase): story.add_child(Node.bool('is_cleared', cleared)) story.add_child(Node.u32('clear_chapter', clear_chapter)) + elif achievement.type == 'course': + total_score = achievement.data.get_int('total_score') + max_combo = achievement.data.get_int('max_combo') + play_cnt = achievement.data.get_int('play_cnt') + clear_medal = achievement.data.get_int('clear_medal') + clear_norma = achievement.data.get_int('clear_norma') + stage1_score = achievement.data.get_int('stage1_score') + stage2_score = achievement.data.get_int('stage2_score') + stage3_score = achievement.data.get_int('stage3_score') + stage4_score = achievement.data.get_int('stage4_score') + + course = Node.void('course') + root.add_child(course) + course.add_child(Node.s16('course_id', achievement.id)) + course.add_child(Node.u8('clear_medal', clear_medal)) + course.add_child(Node.u8('clear_norma', clear_norma)) + course.add_child(Node.s32('stage1_score', stage1_score)) + course.add_child(Node.s32('stage2_score', stage2_score)) + course.add_child(Node.s32('stage3_score', stage3_score)) + course.add_child(Node.s32('stage4_score', stage4_score)) + course.add_child(Node.s32('total_score', total_score)) + course.add_child(Node.s16('max_cmbo', max_combo)) # Yes, it is misspelled. + course.add_child(Node.s16('play_cnt', play_cnt)) + course.add_child(Node.s16('all_rank', 1)) # Unclear what this does. + + # There are also course_rank nodes, but it doesn't appear they get displayed + # to the user anywhere. + return root def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile: @@ -698,6 +920,7 @@ class PopnMusicLapistoria(PopnMusicBase): music.add_child(Node.u8('clear_type', 0)) music.add_child(Node.s32('old_score', points)) music.add_child(Node.u8('old_clear_type', { + self.PLAY_MEDAL_NO_PLAY: self.GAME_PLAY_MEDAL_NO_PLAY, self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED, diff --git a/bemani/backend/popn/sunnypark.py b/bemani/backend/popn/sunnypark.py index 5485501..0007b89 100644 --- a/bemani/backend/popn/sunnypark.py +++ b/bemani/backend/popn/sunnypark.py @@ -175,6 +175,8 @@ class PopnMusicSunnyPark(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) hiscore_index = (score.id * 4) + { @@ -348,6 +350,8 @@ class PopnMusicSunnyPark(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) @@ -653,6 +657,8 @@ class PopnMusicSunnyPark(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score) hiscore_index = (score.id * 4) + { diff --git a/bemani/backend/popn/tunestreet.py b/bemani/backend/popn/tunestreet.py index 09adc26..7237774 100644 --- a/bemani/backend/popn/tunestreet.py +++ b/bemani/backend/popn/tunestreet.py @@ -238,6 +238,8 @@ class PopnMusicTuneStreet(PopnMusicBase): self.CHART_TYPE_EASY, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue flags = self.__format_flags_for_score(score) diff --git a/bemani/backend/popn/usaneko.py b/bemani/backend/popn/usaneko.py index aecf89d..59ce6c3 100644 --- a/bemani/backend/popn/usaneko.py +++ b/bemani/backend/popn/usaneko.py @@ -166,7 +166,7 @@ class PopnMusicUsaNeko(PopnMusicBase): phase.add_child(Node.s16('event_id', phaseid)) phase.add_child(Node.s16('phase', phases[phaseid])) - # Gather course informatino and course ranking for users. + # Gather course information and course ranking for users. course_infos, achievements, profiles = Parallel.execute([ lambda: self.data.local.game.get_all_time_sensitive_settings(self.game, self.version, 'course'), lambda: self.data.local.user.get_all_achievements(self.game, self.version), @@ -554,6 +554,8 @@ class PopnMusicUsaNeko(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue points = score.points medal = score.data.get_int('medal') @@ -616,6 +618,8 @@ class PopnMusicUsaNeko(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue music = Node.void('music') root.add_child(music) @@ -752,6 +756,8 @@ class PopnMusicUsaNeko(PopnMusicBase): self.CHART_TYPE_EX, ]: continue + if score.data.get_int('medal') == self.PLAY_MEDAL_NO_PLAY: + continue music = Node.void('music') root.add_child(music) diff --git a/bemani/client/popn/lapistoria.py b/bemani/client/popn/lapistoria.py index ec4f0cc..185e321 100644 --- a/bemani/client/popn/lapistoria.py +++ b/bemani/client/popn/lapistoria.py @@ -94,6 +94,7 @@ class PopnMusicLapistoriaClient(BaseClient): # Extract and return score data medals: Dict[int, List[int]] = {} scores: Dict[int, List[int]] = {} + courses: Dict[int, Dict[str, int]] = {} for child in resp.child('player22').children: if child.name == 'music': songid = child.child_value('music_num') @@ -108,7 +109,27 @@ class PopnMusicLapistoriaClient(BaseClient): scores[songid] = [0, 0, 0, 0] scores[songid][chart] = points - return {'medals': medals, 'scores': scores} + if child.name == "course": + courseid = child.child_value('course_id') + medal = child.child_value('clear_medal') + combo = child.child_value('max_cmbo') + stage1 = child.child_value('stage1_score') + stage2 = child.child_value('stage2_score') + stage3 = child.child_value('stage3_score') + stage4 = child.child_value('stage4_score') + total = child.child_value('total_score') + courses[courseid] = { + 'id': courseid, + 'medal': medal, + 'combo': combo, + 'stage1': stage1, + 'stage2': stage2, + 'stage3': stage3, + 'stage4': stage4, + 'total': total, + } + + return {'medals': medals, 'scores': scores, 'courses': courses} else: raise Exception(f'Unrecognized message type \'{msg_type}\'') @@ -164,6 +185,49 @@ class PopnMusicLapistoriaClient(BaseClient): resp = self.exchange('', call) self.assert_path(resp, "response/player22/@status") + def verify_player22_write_course(self, ref_id: str, course: Dict[str, int]) -> None: + call = self.call_node() + + # Construct node + player22 = Node.void('player22') + call.add_child(player22) + player22.set_attribute('method', 'write_course') + player22.add_child(Node.s16('pref', 51)) + player22.add_child(Node.string('location_id', 'JP-1')) + player22.add_child(Node.string('ref_id', ref_id)) + player22.add_child(Node.string('data_id', ref_id)) + player22.add_child(Node.string('name', self.NAME)) + player22.add_child(Node.s16('chara_num', 1543)) + player22.add_child(Node.s32('play_id', 0)) + player22.add_child(Node.s16('course_id', course['id'])) + player22.add_child(Node.s16('stage1_music_num', 148)) + player22.add_child(Node.u8('stage1_sheet_num', 1)) + player22.add_child(Node.s16('stage2_music_num', 550)) + player22.add_child(Node.u8('stage2_sheet_num', 1)) + player22.add_child(Node.s16('stage3_music_num', 1113)) + player22.add_child(Node.u8('stage3_sheet_num', 1)) + player22.add_child(Node.s16('stage4_music_num', 341)) + player22.add_child(Node.u8('stage4_sheet_num', 1)) + player22.add_child(Node.u8('norma_type', 2)) + player22.add_child(Node.s32('norma_1_num', 5)) + player22.add_child(Node.s32('norma_2_num', 0)) + player22.add_child(Node.u8('clear_medal', course['medal'])) + player22.add_child(Node.u8('clear_norma', 2)) + player22.add_child(Node.s32('total_score', course['total'])) + player22.add_child(Node.s16('max_combo', course['combo'])) + + for stage, music in enumerate([148, 550, 1113, 341]): + stagenode = Node.void('stage') + player22.add_child(stagenode) + stagenode.add_child(Node.u8('stage', stage)) + stagenode.add_child(Node.s16('music_num', music)) + stagenode.add_child(Node.u8('sheet_num', 1)) + stagenode.add_child(Node.s32('score', course[f'stage{stage + 1}'])) + + # Swap with server + resp = self.exchange('', call) + self.assert_path(resp, "response/player22/@status") + def verify_player22_new(self, ref_id: str) -> None: call = self.call_node() @@ -250,6 +314,8 @@ class PopnMusicLapistoriaClient(BaseClient): for i in range(4): if score[i] != 0: raise Exception('Got nonzero scores count on a new card!') + for _ in scores['courses']: + raise Exception('Got nonzero courses count on a new card!') for phase in [1, 2]: if phase == 1: @@ -338,6 +404,29 @@ class PopnMusicLapistoriaClient(BaseClient): # Sleep so we don't end up putting in score history on the same second time.sleep(1) + + # Write a random course so we know we can retrieve them. + course = { + 'id': random.randint(1, 100), + 'medal': 2, + 'combo': random.randint(10, 100), + 'stage1': random.randint(70000, 100000), + 'stage2': random.randint(70000, 100000), + 'stage3': random.randint(70000, 100000), + 'stage4': random.randint(70000, 100000), + } + course['total'] = sum(course[f'stage{i + 1}'] for i in range(4)) + self.verify_player22_write_course(ref_id, course) + + # Now, grab the profile one more time and see that it is there. + scores = self.verify_player22_read(ref_id, msg_type='query') + if len(scores['courses']) != 1: + raise Exception("Did not get a course back after saving!") + if course['id'] not in scores['courses']: + raise Exception("Did not get expected course back after saving!") + for key in ['medal', 'combo', 'stage1', 'stage2', 'stage3', 'stage4', 'total']: + if course[key] != scores['courses'][course['id']][key]: + raise Exception(f'Expected a {key} of \'{course[key]}\' but got \'{scores["courses"][course["id"]][key]}\'') else: print("Skipping score checks for existing card") diff --git a/bemani/common/constants.py b/bemani/common/constants.py index 9fa865a..dc6e6d6 100644 --- a/bemani/common/constants.py +++ b/bemani/common/constants.py @@ -237,6 +237,7 @@ class DBConstants: MUSECA_CLEAR_TYPE_CLEARED: Final[int] = 200 MUSECA_CLEAR_TYPE_FULL_COMBO: Final[int] = 300 + POPN_MUSIC_PLAY_MEDAL_NO_PLAY: Final[int] = 50 POPN_MUSIC_PLAY_MEDAL_CIRCLE_FAILED: Final[int] = 100 POPN_MUSIC_PLAY_MEDAL_DIAMOND_FAILED: Final[int] = 200 POPN_MUSIC_PLAY_MEDAL_STAR_FAILED: Final[int] = 300 diff --git a/bemani/frontend/popn/popn.py b/bemani/frontend/popn/popn.py index 570cd3c..08b9cef 100644 --- a/bemani/frontend/popn/popn.py +++ b/bemani/frontend/popn/popn.py @@ -26,7 +26,7 @@ class PopnMusicFrontend(FrontendBase): VersionConstants.POPN_MUSIC_TUNE_STREET: 0, VersionConstants.POPN_MUSIC_FANTASIA: 2, VersionConstants.POPN_MUSIC_SUNNY_PARK: 2, - VersionConstants.POPN_MUSIC_LAPISTORIA: 4, + VersionConstants.POPN_MUSIC_LAPISTORIA: 2, VersionConstants.POPN_MUSIC_ECLALE: 4, VersionConstants.POPN_MUSIC_USANEKO: 4, } @@ -40,6 +40,7 @@ class PopnMusicFrontend(FrontendBase): formatted_score['combo'] = score.data.get_int('combo', -1) formatted_score['medal'] = score.data.get_int('medal') formatted_score['status'] = { + PopnMusicBase.PLAY_MEDAL_NO_PLAY: "No Play", PopnMusicBase.PLAY_MEDAL_CIRCLE_FAILED: "○ Failed", PopnMusicBase.PLAY_MEDAL_DIAMOND_FAILED: "◇ Failed", PopnMusicBase.PLAY_MEDAL_STAR_FAILED: "☆ Failed",