diff --git a/README.md b/README.md index c27f09b..bafc241 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ This should be given the same config file as "api", "frontend" and "services". Development version of an eAmusement protocol server using flask and the protocol libraries also used in "bemanishark" and "trafficgen". Currently it lets most modern BEMANI games boot and supports full profile and events for Beatmania IIDX 20-26, -Pop'n Music 19-24, Jubeat Saucer, Saucer Fulfill, Prop, Qubell and Clan, Sound Voltex +Pop'n Music 19-25, Jubeat Saucer, Saucer Fulfill, Prop, Qubell and Clan, Sound Voltex 1, 2, 3 Season 1/2 and 4, Dance Dance Revolution X2, X3, 2013, 2014 and Ace, MÚSECA 1, MÚSECA 1+1/2, MÚSECA Plus, Reflec Beat, Limelight, Colette, groovin'!! Upper, Volzza 1 and Volzza 2, Metal Gear Arcade, and finally The\*BishiBashi. @@ -251,7 +251,7 @@ this will run through and attempt to verify simple operation of that service. No guarantees are made on the accuracy of the emulation though I've strived to be correct. In some cases, I will verify the response, and in other cases I will simply verify that certain things exist so as not to crash a real client. This -currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-24, Jubeat +currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-25, Jubeat Saucer, Fulfill, Prop, Qubell and Clan, Sound Voltex 1, 2, 3 Season 1/2 and 4, Dance Dance Revolution X2, X3, 2013, 2014 and Ace, The\*BishiBashi, MÚSECA 1 and MÚSECA 1+1/2, Reflec Beat, Reflec Beat Limelight, Reflec Beat Colette, groovin'!! Upper, Volzza 1 and @@ -398,7 +398,7 @@ do that. ### Pop'n Music For Pop'n Music, get the game DLL from the version of the game you want to import and -run a command like so. This network supports versions 19-24 so you will want to run this +run a command like so. This network supports versions 19-25 so you will want to run this command once for every version, giving the correct DLL file: ``` diff --git a/bemani/backend/popn/peace.py b/bemani/backend/popn/peace.py index 4053ecd..ae66e6c 100644 --- a/bemani/backend/popn/peace.py +++ b/bemani/backend/popn/peace.py @@ -1,13 +1,74 @@ # vim: set fileencoding=utf-8 +from typing import Dict + from bemani.backend.popn.base import PopnMusicBase from bemani.backend.popn.usaneko import PopnMusicUsaNeko from bemani.common import VersionConstants -class PopnMusicPeace(PopnMusicBase): +class PopnMusicPeace(PopnMusicUsaNeko): name = "Pop'n Music peace" version = VersionConstants.POPN_MUSIC_PEACE + # Biggest ID in the music DB + GAME_MAX_MUSIC_ID = 1877 + def previous_version(self) -> PopnMusicBase: return PopnMusicUsaNeko(self.data, self.config, self.model) + + def get_phases(self) -> Dict[int, int]: + # Event phases + # TODO: Hook event mode settings up to the front end. + return { + # Default song phase availability (0-23) + 0: 23, + # Unknown event (0-2) + 1: 2, + # Unknown event (0-2) + 2: 2, + # Unknown event (0-4) + 3: 4, + # Unknown event (0-1) + 4: 1, + # Enable Net Taisen, including win/loss display on song select (0-1) + 5: 1, + # Enable NAVI-kun shunkyoku toujou, allows song 1608 to be unlocked (0-1) + 6: 1, + # Unknown event (0-1) + 7: 1, + # Unknown event (0-2) + 8: 2, + # Daily Mission (0-2) + 9: 2, + # NAVI-kun Song phase availability (0-30) + 10: 30, + # Unknown event (0-1) + 11: 1, + # Unknown event (0-2) + 12: 2, + # Enable Pop'n Peace preview song (0-1) + 13: 1, + # Unknown event (0-39) + 14: 39, + # Unknown event (0-2) + 15: 2, + # Unknown event (0-3) + 16: 3, + # Unknown event (0-8) + 17: 8, + # Unknown event (0-1) + 28: 1, + # Unknown event (0-1) + 19: 1, + # Unknown event (0-13) + 20: 13, + # Pop'n event archive song phase availability (0-20) + 21: 20, + # Unknown event (0-2) + 22: 2, + # Unknown event (0-1) + 23: 1, + # Unknown event (0-1) + 24: 1, + } \ No newline at end of file diff --git a/bemani/backend/popn/usaneko.py b/bemani/backend/popn/usaneko.py index 2b2f951..3d94f97 100644 --- a/bemani/backend/popn/usaneko.py +++ b/bemani/backend/popn/usaneko.py @@ -126,10 +126,10 @@ class PopnMusicUsaNeko(PopnMusicBase): self.update_machine_name(request.child_value('pcb_setting/name')) return Node.void('pcb24') - def __construct_common_info(self, root: Node) -> None: + def get_phases(self) -> Dict[int, int]: # Event phases # TODO: Hook event mode settings up to the front end. - phases = { + return { # Default song phase availability (0-11) 0: 11, # Unknown event (0-2) @@ -160,11 +160,12 @@ class PopnMusicUsaNeko(PopnMusicBase): 13: 1, } - for phaseid in phases: + def __construct_common_info(self, root: Node) -> None: + for phaseid in self.get_phases(): phase = Node.void('phase') root.add_child(phase) phase.add_child(Node.s16('event_id', phaseid)) - phase.add_child(Node.s16('phase', phases[phaseid])) + phase.add_child(Node.s16('phase', self.get_phases()[phaseid])) # Gather course informatino and course ranking for users. course_infos, achievements, profiles = Parallel.execute([ diff --git a/bemani/utils/read.py b/bemani/utils/read.py index ffb3cca..416c966 100644 --- a/bemani/utils/read.py +++ b/bemani/utils/read.py @@ -371,6 +371,7 @@ class ImportPopn(ImportBase): '22': VersionConstants.POPN_MUSIC_LAPISTORIA, '23': VersionConstants.POPN_MUSIC_ECLALE, '24': VersionConstants.POPN_MUSIC_USANEKO, + '25': VersionConstants.POPN_MUSIC_PEACE, }.get(version, -1) if actual_version == VersionConstants.POPN_MUSIC_TUNE_STREET: @@ -382,7 +383,7 @@ class ImportPopn(ImportBase): # Newer pop'n has charts for easy, normal, hyper, another self.charts = [0, 1, 2, 3] else: - raise Exception("Unsupported Pop'n Music version, expected one of the following: 19, 20, 21, 22, 23, 24!") + raise Exception("Unsupported Pop'n Music version, expected one of the following: 19, 20, 21, 22, 23, 24, 25!") super().__init__(config, GameConstants.POPN_MUSIC, actual_version, no_combine, update) @@ -957,6 +958,104 @@ class ImportPopn(ImportBase): 'I' ) + # Decoding function for chart masks + def available_charts(mask: int) -> Tuple[bool, bool, bool, bool, bool, bool]: + return ( + mask & 0x0080000 > 0, # Easy chart bit + True, # Always a normal chart + mask & 0x1000000 > 0, # Hyper chart bit + mask & 0x2000000 > 0, # Ex chart bit + True, # Always a battle normal chart + mask & 0x4000000 > 0, # Battle hyper chart bit + ) + + elif self.version == VersionConstants.POPN_MUSIC_PEACE: + # Based on M39:J:A:A:2020092800 + + # Normal offset for music DB, size + offset = 0x2C7C78 + step = 172 + length = 1877 + + # Offset and step of file DB + file_offset = 0x2B8010 + file_step = 32 + + # Standard lookups + genre_offset = 0 + title_offset = 1 + artist_offset = 2 + comment_offset = 3 + english_title_offset = 4 + english_artist_offset = 5 + extended_genre_offset = -1 + charts_offset = 8 + folder_offset = 9 + + # Offsets for normal chart difficulties + easy_offset = 12 + normal_offset = 13 + hyper_offset = 14 + ex_offset = 15 + + # Offsets for battle chart difficulties + battle_normal_offset = 16 + battle_hyper_offset = 17 + + # Offsets into which offset to seek to for file lookups + easy_file_offset = 18 + normal_file_offset = 19 + hyper_file_offset = 20 + ex_file_offset = 21 + battle_normal_file_offset = 22 + battle_hyper_file_offset = 23 + + packedfmt = ( + '<' + 'I' # Genre + 'I' # Title + 'I' # Artist + 'I' # Comment + 'I' # English Title + 'I' # English Artist + 'H' # ?? + 'H' # ?? + 'I' # Available charts mask + 'I' # Folder + 'I' # Event unlocks? + 'I' # Event unlocks? + 'B' # Easy difficulty + 'B' # Normal difficulty + 'B' # Hyper difficulty + 'B' # EX difficulty + 'B' # Battle normal difficulty + 'B' # Battle hyper difficulty + 'xx' # Unknown pointer + 'H' # Easy chart pointer + 'H' # Normal chart pointer + 'H' # Hyper chart pointer + 'H' # EX chart pointer + 'H' # Battle normal pointer + 'H' # Battle hyper pointer + 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + ) + + # Offsets into file DB for finding file and folder. + file_folder_offset = 0 + file_name_offset = 1 + + filefmt = ( + '<' + 'I' # Folder + 'I' # Filename + 'I' + 'I' + 'I' + 'I' + 'I' + 'I' + ) + # Decoding function for chart masks def available_charts(mask: int) -> Tuple[bool, bool, bool, bool, bool, bool]: return ( diff --git a/bemani/utils/trafficgen.py b/bemani/utils/trafficgen.py index e8d2e9c..faddeaf 100644 --- a/bemani/utils/trafficgen.py +++ b/bemani/utils/trafficgen.py @@ -94,6 +94,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A pcbid, config, ) + if game == 'pnm-peace': + return PopnMusicUsaNekoClient( + proto, + pcbid, + config, + ) if game == 'jubeat-saucer': return JubeatSaucerClient( proto, @@ -322,6 +328,12 @@ def mainloop(address: str, port: int, configfile: str, action: str, game: str, c 'old_profile_model': "M39:J:B:A", 'avs': "2.15.8 r6631", }, + 'pnm-peace': { + 'name': "Pop'n Music peace", + 'model': "M39:J:B:A:2020092800", + 'old_profile_model': "M39:J:B:A", + 'avs': "2.15.8 r6631", + }, 'jubeat-saucer': { 'name': "Jubeat Saucer", 'model': "L44:J:A:A:2014012802", @@ -534,6 +546,7 @@ def main() -> None: 'pnm-22': 'pnm-lapistoria', 'pnm-23': 'pnm-eclale', 'pnm-24': 'pnm-usaneko', + 'pnm-25': 'pnm-peace', 'iidx-20': 'iidx-tricoro', 'iidx-21': 'iidx-spada', 'iidx-22': 'iidx-pendual', diff --git a/bootstrap b/bootstrap index d56ac70..8c0d5ac 100755 --- a/bootstrap +++ b/bootstrap @@ -12,6 +12,7 @@ set -e ./read --series pnm --version 22 "$@" ./read --series pnm --version 23 "$@" ./read --series pnm --version 24 "$@" +./read --series pnm --version 25 "$@" # Init Jubeat ./read --series jubeat --version saucer "$@"