From aa10913a202a4e725e476408e4ddae28f342fe3f Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 6 Sep 2021 01:30:23 +0000 Subject: [PATCH 1/5] Allow optionally specifying achievement type and id for fetching activements for all users. --- bemani/data/mysql/user.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bemani/data/mysql/user.py b/bemani/data/mysql/user.py index 263a4bd..7329504 100644 --- a/bemani/data/mysql/user.py +++ b/bemani/data/mysql/user.py @@ -633,9 +633,9 @@ class UserData(BaseData): return [UserID(result['userid']) for result in cursor.fetchall()] - def get_all_achievements(self, game: GameConstants, version: int) -> List[Tuple[UserID, Achievement]]: + def get_all_achievements(self, game: GameConstants, version: int, achievementid: Optional[int] = None, achievementtype: Optional[str] = None) -> List[Tuple[UserID, Achievement]]: """ - Given a game/version, find all achievements for al players. + Given a game/version, find all achievements for all players. Parameters: game - Enum value identifier of the game looking up the user. @@ -649,7 +649,14 @@ class UserData(BaseData): "refid.userid AS userid FROM achievement, refid WHERE refid.game = :game AND " "refid.version = :version AND refid.refid = achievement.refid" ) - cursor = self.execute(sql, {'game': game.value, 'version': version}) + params: Dict[str, Any] = {'game': game.value, 'version': version} + if achievementtype is not None: + sql += " AND achievement.type = :type" + params['type'] = achievementtype + if achievementid is not None: + sql += " AND achievement.id = :id" + params['id'] = achievementid + cursor = self.execute(sql, params) achievements = [] for result in cursor.fetchall(): From 31e2ef2220a2dd1b9eb7b1fc4f080d36a84c9729 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 6 Sep 2021 01:30:43 +0000 Subject: [PATCH 2/5] Enable and support course mode for Lapistoria, enable story mode adjustments, document event flags better. --- bemani/backend/popn/base.py | 2 + bemani/backend/popn/eclale.py | 6 + bemani/backend/popn/fantasia.py | 6 + bemani/backend/popn/lapistoria.py | 273 +++++++++++++++++++++++++++--- bemani/backend/popn/sunnypark.py | 6 + bemani/backend/popn/tunestreet.py | 2 + bemani/backend/popn/usaneko.py | 8 +- bemani/client/popn/lapistoria.py | 91 +++++++++- bemani/common/constants.py | 1 + bemani/frontend/popn/popn.py | 3 +- 10 files changed, 370 insertions(+), 28 deletions(-) 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", From 50216b1d4548fbe2fc3aa2ae2c1b9ef287390dd5 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 6 Sep 2021 01:30:56 +0000 Subject: [PATCH 3/5] Implement force unlock songs flag for Lapistoria. --- bemani/backend/popn/lapistoria.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bemani/backend/popn/lapistoria.py b/bemani/backend/popn/lapistoria.py index ffdb056..3ba3f65 100644 --- a/bemani/backend/popn/lapistoria.py +++ b/bemani/backend/popn/lapistoria.py @@ -83,6 +83,14 @@ class PopnMusicLapistoria(PopnMusicBase): }, }, ], + 'bools': [ + { + 'name': 'Force Song Unlock', + 'tip': 'Force unlock all songs.', + 'category': 'game_config', + 'setting': 'force_unlock_songs', + }, + ], } def handle_info22_common_request(self, request: Node) -> Node: @@ -658,6 +666,17 @@ class PopnMusicLapistoria(PopnMusicBase): customize.add_child(Node.u16('comment_1', customize_dict.get_int('comment_1'))) customize.add_child(Node.u16('comment_2', customize_dict.get_int('comment_2'))) + game_config = self.get_game_config() + if game_config.get_bool('force_unlock_songs'): + songs = self.data.local.music.get_all_songs(self.game, self.version) + for song in songs: + item = Node.void('item') + root.add_child(item) + item.add_child(Node.u8('type', 0)) + item.add_child(Node.u16('id', song.id)) + item.add_child(Node.u16('param', 15)) + item.add_child(Node.bool('is_new', False)) + # Set up achievements achievements = self.data.local.user.get_achievements(self.game, self.version, userid) for achievement in achievements: @@ -675,6 +694,10 @@ class PopnMusicLapistoria(PopnMusicBase): # 6, 24 # 7, 4158 + if game_config.get_bool('force_unlock_songs') and itemtype == 0: + # We already sent song unlocks in the force unlock section above. + continue + item = Node.void('item') root.add_child(item) item.add_child(Node.u8('type', itemtype)) @@ -806,6 +829,7 @@ class PopnMusicLapistoria(PopnMusicBase): self.update_play_statistics(userid) # Extract achievements + game_config = self.get_game_config() for node in request.children: if node.name == 'item': if not node.child_value('is_new'): @@ -816,6 +840,10 @@ class PopnMusicLapistoria(PopnMusicBase): itemtype = node.child_value('type') param = node.child_value('param') + if game_config.get_bool('force_unlock_songs') and itemtype == 0: + # If we enabled force song unlocks, don't save songs to the profile. + continue + self.data.local.user.put_achievement( self.game, self.version, From 73f340947b067fa5d29bbb823b539554d8032758 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 6 Sep 2021 02:01:28 +0000 Subject: [PATCH 4/5] Use abstract base classes in backend base class and factory. --- bemani/backend/base.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/bemani/backend/base.py b/bemani/backend/base.py index b6c5db0..9c1169d 100644 --- a/bemani/backend/base.py +++ b/bemani/backend/base.py @@ -1,6 +1,7 @@ -from abc import ABC +from abc import ABC, abstractmethod import traceback from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type +from typing_extensions import Final from bemani.common import Model, ValidatedDict, Profile, PlayStatistics, GameConstants, Time from bemani.data import Config, Data, Machine, UserID, RemoteUser @@ -14,14 +15,14 @@ class Status: """ List of statuses we return to the game for various reasons. """ - SUCCESS = 0 - NO_PROFILE = 109 - NOT_ALLOWED = 110 - NOT_REGISTERED = 112 - INVALID_PIN = 116 + SUCCESS: Final[int] = 0 + NO_PROFILE: Final[int] = 109 + NOT_ALLOWED: Final[int] = 110 + NOT_REGISTERED: Final[int] = 112 + INVALID_PIN: Final[int] = 116 -class Factory: +class Factory(ABC): """ The base class every game factory inherits from. Defines a create method which should return some game class which can handle packets. Game classes @@ -29,16 +30,17 @@ class Factory: Dispatch will look up in order to handle calls. """ - MANAGED_CLASSES: List[Type["Base"]] = [] + MANAGED_CLASSES: List[Type["Base"]] @classmethod + @abstractmethod def register_all(cls) -> None: """ Subclasses of this class should use this function to register themselves with Base, using Base.register(). Factories specify the game code that they support, which Base will use when routing requests. """ - raise Exception('Override this in subclass!') + raise NotImplementedError('Override this in subclass!') @classmethod def run_scheduled_work(cls, data: Data, config: Config) -> None: @@ -84,6 +86,7 @@ class Factory: yield (game.game, game.version, game.get_settings()) @classmethod + @abstractmethod def create(cls, data: Data, config: Config, model: Model, parentmodel: Optional[Model]=None) -> Optional['Base']: """ Given a modelstring and an optional parent model, return an instantiated game class that can handle a packet. @@ -102,7 +105,7 @@ class Factory: A subclass of Base that hopefully has a handle__request method on it, for the particular call that Dispatch wants to resolve, or None if we can't look up a game. """ - raise Exception('Override this in subclass!') + raise NotImplementedError('Override this in subclass!') class Base(ABC): From 6d7bf082e646aa4686a70d396a67afe049222bf6 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 6 Sep 2021 02:01:57 +0000 Subject: [PATCH 5/5] Force url prefixes for games to match their game constant values. --- bemani/frontend/bishi/endpoints.py | 2 +- bemani/frontend/ddr/endpoints.py | 2 +- bemani/frontend/iidx/endpoints.py | 2 +- bemani/frontend/jubeat/endpoints.py | 2 +- bemani/frontend/museca/endpoints.py | 2 +- bemani/frontend/popn/endpoints.py | 2 +- bemani/frontend/reflec/endpoints.py | 2 +- bemani/frontend/sdvx/endpoints.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bemani/frontend/bishi/endpoints.py b/bemani/frontend/bishi/endpoints.py index 1c4c75b..b8ea29a 100644 --- a/bemani/frontend/bishi/endpoints.py +++ b/bemani/frontend/bishi/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g bishi_pages = Blueprint( 'bishi_pages', __name__, - url_prefix='/bishi', + url_prefix=f'/{GameConstants.BISHI_BASHI.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/ddr/endpoints.py b/bemani/frontend/ddr/endpoints.py index 8361ef2..f32ac83 100644 --- a/bemani/frontend/ddr/endpoints.py +++ b/bemani/frontend/ddr/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g ddr_pages = Blueprint( 'ddr_pages', __name__, - url_prefix='/ddr', + url_prefix=f'/{GameConstants.DDR.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/iidx/endpoints.py b/bemani/frontend/iidx/endpoints.py index 126f3cc..784e6ac 100644 --- a/bemani/frontend/iidx/endpoints.py +++ b/bemani/frontend/iidx/endpoints.py @@ -14,7 +14,7 @@ from bemani.frontend.types import g iidx_pages = Blueprint( 'iidx_pages', __name__, - url_prefix='/iidx', + url_prefix=f'/{GameConstants.IIDX.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/jubeat/endpoints.py b/bemani/frontend/jubeat/endpoints.py index 0be2396..d152d96 100644 --- a/bemani/frontend/jubeat/endpoints.py +++ b/bemani/frontend/jubeat/endpoints.py @@ -14,7 +14,7 @@ from bemani.frontend.types import g jubeat_pages = Blueprint( 'jubeat_pages', __name__, - url_prefix='/jubeat', + url_prefix=f'/{GameConstants.JUBEAT.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/museca/endpoints.py b/bemani/frontend/museca/endpoints.py index 67fb39d..938441e 100644 --- a/bemani/frontend/museca/endpoints.py +++ b/bemani/frontend/museca/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g museca_pages = Blueprint( 'museca_pages', __name__, - url_prefix='/museca', + url_prefix=f'/{GameConstants.MUSECA.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/popn/endpoints.py b/bemani/frontend/popn/endpoints.py index 3bea7a3..c9601bf 100644 --- a/bemani/frontend/popn/endpoints.py +++ b/bemani/frontend/popn/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g popn_pages = Blueprint( 'popn_pages', __name__, - url_prefix='/popn', + url_prefix=f'/{GameConstants.POPN_MUSIC.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/reflec/endpoints.py b/bemani/frontend/reflec/endpoints.py index 66b7ab0..99e9fc1 100644 --- a/bemani/frontend/reflec/endpoints.py +++ b/bemani/frontend/reflec/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g reflec_pages = Blueprint( 'reflec_pages', __name__, - url_prefix='/reflec', + url_prefix=f'/{GameConstants.REFLEC_BEAT.value}', template_folder=templates_location, static_folder=static_location, ) diff --git a/bemani/frontend/sdvx/endpoints.py b/bemani/frontend/sdvx/endpoints.py index 9cb377f..37bf4d0 100644 --- a/bemani/frontend/sdvx/endpoints.py +++ b/bemani/frontend/sdvx/endpoints.py @@ -15,7 +15,7 @@ from bemani.frontend.types import g sdvx_pages = Blueprint( 'sdvx_pages', __name__, - url_prefix='/sdvx', + url_prefix=f'/{GameConstants.SDVX.value}', template_folder=templates_location, static_folder=static_location, )