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): diff --git a/bemani/backend/popn/base.py b/bemani/backend/popn/base.py index 128d3dd..18b6b55 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 bda2165..db1acfe 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') @@ -458,6 +462,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..3ba3f65 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,101 @@ 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', + }, + }, + ], + '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: - # 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 +252,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 +289,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 +303,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 +364,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 +380,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 +571,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, @@ -474,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: @@ -481,6 +684,20 @@ 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 + + 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)) @@ -518,6 +735,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: @@ -584,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'): @@ -594,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, @@ -698,6 +948,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 a5e79a9..c1dc11b 100644 --- a/bemani/backend/popn/usaneko.py +++ b/bemani/backend/popn/usaneko.py @@ -167,7 +167,7 @@ class PopnMusicUsaNeko(PopnMusicBase): phase.add_child(Node.s16('event_id', phaseid)) phase.add_child(Node.s16('phase', phase_value)) - # 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), @@ -555,6 +555,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') @@ -617,6 +619,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) @@ -757,6 +761,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 0cf472d..53362fc 100644 --- a/bemani/common/constants.py +++ b/bemani/common/constants.py @@ -240,6 +240,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/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(): 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 e318609..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='/pnm', + url_prefix=f'/{GameConstants.POPN_MUSIC.value}', template_folder=templates_location, static_folder=static_location, ) 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", 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, )