1
0
mirror of synced 2024-11-27 23:50:47 +01:00

Add Pnm Kaimei riddles support

This commit is contained in:
cracrayol 2022-09-19 00:36:42 +02:00 committed by Jennifer Taylor
parent b2edbdce24
commit 29cf43dc5c
9 changed files with 1291 additions and 2 deletions

View File

@ -28,6 +28,7 @@ from bemani.backend.popn.lapistoria import PopnMusicLapistoria
from bemani.backend.popn.eclale import PopnMusicEclale
from bemani.backend.popn.usaneko import PopnMusicUsaNeko
from bemani.backend.popn.peace import PopnMusicPeace
from bemani.backend.popn.kaimei import PopnMusicKaimei
from bemani.common import Model, VersionConstants
from bemani.data import Config, Data
@ -60,6 +61,7 @@ class PopnMusicFactory(Factory):
PopnMusicEclale,
PopnMusicUsaNeko,
PopnMusicPeace,
PopnMusicKaimei,
]
@classmethod
@ -79,8 +81,10 @@ class PopnMusicFactory(Factory):
return VersionConstants.POPN_MUSIC_ECLALE
if date >= 2016121400 and date < 2018101700:
return VersionConstants.POPN_MUSIC_USANEKO
if date >= 2018101700:
if date >= 2018101700 and date < 2021042600:
return VersionConstants.POPN_MUSIC_PEACE
if date >= 2021042600:
return VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES
return None
if model.gamecode == 'G15':
@ -113,6 +117,8 @@ class PopnMusicFactory(Factory):
return PopnMusicEclale(data, config, model)
if parentversion == VersionConstants.POPN_MUSIC_PEACE:
return PopnMusicUsaNeko(data, config, model)
if parentversion == VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES:
return PopnMusicPeace(data, config, model)
# Unknown older version
return None
@ -128,6 +134,8 @@ class PopnMusicFactory(Factory):
return PopnMusicUsaNeko(data, config, model)
if version == VersionConstants.POPN_MUSIC_PEACE:
return PopnMusicPeace(data, config, model)
if version == VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES:
return PopnMusicKaimei(data, config, model)
# Unknown game version
return None

View File

@ -0,0 +1,491 @@
# vim: set fileencoding=utf-8
import math
import random
from typing import Any, Dict, List, Tuple
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.common import PopnMusicModernBase
from bemani.backend.popn.peace import PopnMusicPeace
from bemani.common import VersionConstants
from bemani.common.validateddict import Profile
from bemani.data.types import UserID
from bemani.protocol.node import Node
class PopnMusicKaimei(PopnMusicModernBase):
name: str = "Pop'n Music 解明リドルズ"
version: int = VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES
# Biggest ID in the music DB
GAME_MAX_MUSIC_ID: int = 2019
# Biggest deco part ID in the game
GAME_MAX_DECO_ID: int = 133
# Item limits are as follows:
# 0: 1877 - ID is the music ID that the player purchased/unlocked.
# 1: 2344
# 2: 3
# 3: 133 - ID points at a character part that can be purchased on the character screen.
# 4: 1
# 5: 1
# 6: 60
def previous_version(self) -> PopnMusicBase:
return PopnMusicPeace(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': 'Music Open Phase',
'tip': 'Default music phase for all players.',
'category': 'game_config',
'setting': 'music_phase',
'values': {
# The value goes to 30 now, but it starts where usaneko left off at 23
# Unlocks a total of 10 songs
23: 'No music unlocks',
24: 'Phase 1',
25: 'Phase 2',
26: 'Phase 3',
27: 'Phase 4',
28: 'Phase 5',
29: 'Phase 6',
30: 'Phase MAX',
}
},
{
'name': 'Kaimei! MN tanteisha event Phase',
'tip': 'Kaimei! MN tanteisha event phase for all players.',
'category': 'game_config',
'setting': 'mn_tanteisha_phase',
'values': {
0: 'Disabled',
1: 'Roki',
2: 'shiroro',
3: 'PIERRE&JILL',
4: 'ROSA',
5: 'taoxiang',
6: 'TangTang',
7: 'OTOBEAR',
8: 'kaorin',
9: 'CHARLY',
10: 'ALOE',
11: 'RIE♥chan',
12: 'hina',
13: 'PAPYRUS',
14: '雷蔵, miho, RIE♥chan, Ryusei Honey',
15: 'Murasaki',
16: 'Lucifelle',
17: '',
18: 'stella',
19: 'ちせ',
20: 'LISA',
21: 'SUMIRE',
22: 'SHISHITUGU',
23: 'WALKER',
24: 'Candy',
25: 'Jade',
26: 'AYA',
27: 'kaorin',
28: 'Lord Meh',
29: 'HAMANOV',
30: 'Agent',
31: 'Yima',
32: 'ikkei',
33: 'echidna',
34: 'lithos',
35: 'SMOKE',
36: 'the KING',
37: 'Kicoro',
38: 'DEBORAH',
39: 'Teruo',
40: 'the TOWER',
41: 'Mamoru-kun',
42: 'Canopus',
43: 'Mimi Nyami',
44: 'iO-LOWER',
45: 'BOY',
46: 'Sergei',
47: 'SAPPHIRE',
48: 'Chocky',
49: 'HAPPPY',
50: 'SHOLLKEE',
51: 'CHARA-O',
52: 'Hugh, GRIM, SUMIKO',
53: 'Peetan',
54: 'SHARK',
55: 'Nakajima-san',
56: 'KIKYO',
57: 'SUMIRE',
58: 'NAKAJI',
59: 'moi moi',
60: 'TITICACA',
61: 'MASAMUNE',
62: 'YUMMY'
},
},
{
# For festive times, it's possible to change the welcome greeting. I'm not sure why you would want to change this, but now you can.
'name': 'Holiday Greeting',
'tip': 'Changes the payment selection confirmation sound.',
'category': 'game_config',
'setting': 'holiday_greeting',
'values': {
0: 'Okay!',
1: 'Merry Christmas!',
2: 'Happy New Year!',
}
},
{
# peace soundtrack hatsubai kinen SP event, 0 = off, 1 = active, 2 = off (0-2)
'name': 'peace soundtrack hatsubai kinen SP',
'tip': 'peace soundtrack hatsubai kinen SP for all players.',
'category': 'game_config',
'setting': 'peace_soundtrack',
'values': {
0: 'Not stated',
1: 'Active',
2: 'Ended',
}
},
{
'name': 'MZD no kimagure tanteisha joshu',
'tip': 'Boost increasing the Clarification Level, if four or more Requests still unresolved.',
'category': 'game_config',
'setting': 'tanteisha_joshu',
'values': {
0: 'Not stated',
1: 'Active',
2: 'Ended',
}
},
{
# Shutchou! pop'n quest Lively II event
'name': 'Shutchou! pop\'n quest Lively phase',
'tip': 'Shutchou! pop\'n quest Lively phase for all players.',
'category': 'game_config',
'setting': 'popn_quest_lively',
'values': {
0: 'Not started',
1: 'fes 1',
2: 'fes 2',
3: 'fes FINAL',
4: 'fes EXTRA',
5: 'Ended',
}
},
{
# Shutchou! pop'n quest Lively II event
'name': 'Shutchou! pop\'n quest Lively II phase',
'tip': 'Shutchou! pop\'n quest Lively II phase for all players.',
'category': 'game_config',
'setting': 'popn_quest_lively_2',
'values': {
0: 'Not started',
1: 'fes 1',
2: 'fes 2',
3: 'fes FINAL',
4: 'fes EXTRA',
5: 'fes THE END',
6: 'Ended',
}
},
],
'bools': [
# We don't currently support lobbies or anything, so this is commented out until
# somebody gets around to implementing it.
# {
# 'name': 'Net Taisen',
# 'tip': 'Enable Net Taisen, including win/loss display on song select',
# 'category': 'game_config',
# 'setting': 'enable_net_taisen',
# },
{
'name': 'Force Song Unlock',
'tip': 'Force unlock all songs.',
'category': 'game_config',
'setting': 'force_unlock_songs',
},
],
}
def get_common_config(self) -> Tuple[Dict[int, int], bool]:
game_config = self.get_game_config()
music_phase = game_config.get_int('music_phase')
holiday_greeting = game_config.get_int('holiday_greeting')
enable_net_taisen = False # game_config.get_bool('enable_net_taisen')
mn_tanteisha_phase = game_config.get_int('mn_tanteisha_phase')
peace_soundtrack = game_config.get_int('peace_soundtrack')
tanteisha_joshu = game_config.get_int('tanteisha_joshu')
popn_quest_lively = game_config.get_int('popn_quest_lively')
popn_quest_lively_2 = game_config.get_int('popn_quest_lively_2')
# Event phases
return (
{
# Default song phase availability (0-30)
# The following songs are unlocked when the phase is at or above the number specified:
# For 23 and before, see usaneko/peace
# 24 - 1929, 1930
# 25 - 1964
# 26 - 1966, 1967
# 27 - 1975
# 28 - 1994
# 29 - 1995, 1996
# 30 - 1997
0: music_phase,
# Unknown event (0-4)
1: 4,
# Holiday Greeting (0-2)
2: holiday_greeting,
# 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 if enable_net_taisen else 0,
# 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,
# Stamp Card Rally (0-39)
14: 39,
# Unknown event (0-2)
15: 2,
# Unknown event (0-3)
16: 3,
# Unknown event (0-8)
17: 8,
# FLOOR INFECTION event (0-1)
18: 1,
# pop'n music × NOSTALGIA kyouenkai (0-1)
19: 1,
# Event archive event (0-13)
20: 13,
# Pop'n event archive song phase availability (0-20)
21: 20,
# バンめし♪ ふるさとグランプリunlocks (split into two rounds) (0-2)
22: 2,
# いちかのBEMANI投票選抜戦2019 unlocks (0-1)
23: 1,
# ダンキラ!!! × pop'n music unlocks (0-1)
24: 1,
# Kaimei riddles events starts here
# Kaimei! MN tanteisha event Phase (0-62)
# When active, the following songs are available for unlock
# 1: 1914
# 2: 195
# 3: 1915
# 4: 1916
# 5: 1896
# 6: 1908
# 7: 1931
# 8: 1924
# 9: 1925
# 10: 1894
# 11: 1926
# 12: 1927
# 13: 1928
# 14: 1932, 1933, 1934, 1935
# 15: 521
# 16: 1936
# 17: 1943
# 18: 1937
# 19: 1938
# 20: 1939
# 21: 1943
# 22: 1941
# 23: 1942
# 24: 323
# 25: 1946
# 26: 575
# 27: 1947
# 28: 1955
# 29: 1957
# 30: 1958
# 31: 1959
# 32: 1960
# 33: 1961
# 34: 1963
# 35: 1962
# 36: 1968
# 37: 1969
# 38: 1965
# 39: 1970
# 40: 1976
# 41: 1977
# 42: 1978
# 43: 1945
# 44: 1944
# 45: 1999
# 46: 2000
# 47: 2001
# 48: 2002
# 49: 2003
# 50: 2004
# 51: 2005
# 52: 267, 1998, 2006
# 53: 2011
# 54: 2007
# 55: 2008
# 56: 2009
# 57: 2010
# 58: 2016
# 59: 2012
# 60: 2018
# 61: 2013
# 62: 2015
25: mn_tanteisha_phase,
# Unknown event (0-3)
26: 3,
# peace soundtrack hatsubai kinen SP (0-2)
# When active, the following songs are available for unlock: 1971, 1972, 1973
27: peace_soundtrack,
# MZD no kimagure tanteisha joshu (0-2)
28: tanteisha_joshu,
# Shutchou! pop'n quest Lively (0-5)
# When active, the following songs are available for unlock
# 1: 1917, 1918
# 2: 1919, 1921
# 3: 1920, 1922, 1923
# 4: 1974
29: popn_quest_lively,
# Shutchou! pop'n quest Lively II (0-6)
# When active, the following songs are available for unlock
# 1: 1989, 1990, 1991
# 2: 1984, 1985, 1992
# 3: 1982, 1983, 1988
# 4: 1986, 1987, 1993
# 5: 2017
30: popn_quest_lively_2,
},
False,
)
def format_profile(self, userid: UserID, profile: Profile) -> Node:
root = PopnMusicModernBase.format_profile(self, userid, profile)
account = root.child('account')
account.add_child(Node.s16('card_again_count', profile.get_int('point')))
account.add_child(Node.s16('sp_riddles_id', profile.get_int('step')))
# Kaimei riddles events
event2021 = Node.void('event2021')
root.add_child(event2021)
event2021.add_child(Node.u32('point', profile.get_int('point')))
event2021.add_child(Node.u8('step', profile.get_int('step')))
event2021.add_child(Node.u32_array('quest_point', profile.get_int_array('quest_point', 8, [0] * 8)))
event2021.add_child(Node.u8('step_nos', profile.get_int('step_nos')))
event2021.add_child(Node.u32_array('quest_point_nos', profile.get_int_array('quest_point_nos', 13, [0] * 13)))
riddles_data = Node.void('riddles_data')
root.add_child(riddles_data)
# Generate Short Riddles for MN tanteisha
randomRiddles: List[int] = []
for x in range(3):
riddle = 0
while True:
riddle = math.floor(random.randrange(1, 21, 1))
try:
randomRiddles.index(riddle)
except ValueError:
break
randomRiddles.append(riddle)
sh_riddles = Node.void('sh_riddles')
riddles_data.add_child(sh_riddles)
sh_riddles.add_child(Node.u32('sh_riddles_id', riddle))
# Set up kaimei riddles achievements
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
for achievement in achievements:
if achievement.type == 'riddle':
kaimei_gauge = achievement.data.get_int('kaimei_gauge')
is_cleared = achievement.data.get_bool('is_cleared')
riddles_cleared = achievement.data.get_bool('riddles_cleared')
select_count = achievement.data.get_int('select_count')
other_count = achievement.data.get_int('other_count')
sp_riddles = Node.void('sp_riddles')
riddles_data.add_child(sp_riddles)
sp_riddles.add_child(Node.u16('kaimei_gauge', kaimei_gauge))
sp_riddles.add_child(Node.bool('is_cleared', is_cleared))
sp_riddles.add_child(Node.bool('riddles_cleared', riddles_cleared))
sp_riddles.add_child(Node.u8('select_count', select_count))
sp_riddles.add_child(Node.u32('other_count', other_count))
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
newprofile = PopnMusicModernBase.unformat_profile(self, userid, request, oldprofile)
account = request.child('account')
if account is not None:
newprofile.replace_int('card_again_count', account.child_value('card_again_count'))
newprofile.replace_int('sp_riddles_id', account.child_value('sp_riddles_id'))
# Kaimei riddles events
event2021 = request.child('event2021')
if event2021 is not None:
newprofile.replace_int('point', event2021.child_value('point'))
newprofile.replace_int('step', event2021.child_value('step'))
newprofile.replace_int_array('quest_point', 8, event2021.child_value('quest_point'))
newprofile.replace_int('step_nos', event2021.child_value('step_nos'))
newprofile.replace_int_array('quest_point_nos', 13, event2021.child_value('quest_point_nos'))
# Extract kaimei riddles achievements
for node in request.children:
if node.name == 'riddles_data':
riddle_id = 0
playedRiddle = request.child('account').child_value('sp_riddles_id')
for riddle in node.children:
kaimei_gauge = riddle.child_value('kaimei_gauge')
is_cleared = riddle.child_value('is_cleared')
riddles_cleared = riddle.child_value('riddles_cleared')
select_count = riddle.child_value('select_count')
other_count = riddle.child_value('other_count')
if (riddles_cleared or select_count >= 3):
select_count = 3
elif (playedRiddle == riddle_id):
select_count += 1
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
riddle_id,
'riddle',
{
'kaimei_gauge': kaimei_gauge,
'is_cleared': is_cleared,
'riddles_cleared': riddles_cleared,
'select_count': select_count,
'other_count': other_count,
},
)
riddle_id += 1
return newprofile

View File

@ -5,6 +5,7 @@ from bemani.client.popn.lapistoria import PopnMusicLapistoriaClient
from bemani.client.popn.eclale import PopnMusicEclaleClient
from bemani.client.popn.usaneko import PopnMusicUsaNekoClient
from bemani.client.popn.peace import PopnMusicPeaceClient
from bemani.client.popn.kaimei import PopnMusicKaimeiClient
__all__ = [
@ -15,4 +16,5 @@ __all__ = [
"PopnMusicEclaleClient",
"PopnMusicUsaNekoClient",
"PopnMusicPeaceClient",
"PopnMusicKaimeiClient",
]

View File

@ -0,0 +1,672 @@
import random
import time
from typing import Any, Dict, Optional
from bemani.client.base import BaseClient
from bemani.protocol import Node
class PopnMusicKaimeiClient(BaseClient):
NAME = ''
def verify_pcb24_boot(self, loc: str) -> None:
call = self.call_node()
# Construct node
pcb24 = Node.void('pcb24')
call.add_child(pcb24)
pcb24.set_attribute('method', 'boot')
pcb24.add_child(Node.string('loc_id', loc))
pcb24.add_child(Node.u8('loc_type', 0))
pcb24.add_child(Node.string('loc_name', ''))
pcb24.add_child(Node.string('country', 'US'))
pcb24.add_child(Node.string('region', '.'))
pcb24.add_child(Node.s16('pref', 51))
pcb24.add_child(Node.string('customer', ''))
pcb24.add_child(Node.string('company', ''))
pcb24.add_child(Node.ipv4('gip', '127.0.0.1'))
pcb24.add_child(Node.u16('gp', 10011))
pcb24.add_child(Node.string('rom_number', 'M39-JB-G01'))
pcb24.add_child(Node.u64('c_drive', 10028228608))
pcb24.add_child(Node.u64('d_drive', 47945170944))
pcb24.add_child(Node.u64('e_drive', 10394677248))
pcb24.add_child(Node.string('etc', ''))
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.assert_path(resp, "response/pcb24/@status")
def __verify_common(self, root: str, resp: Node) -> None:
self.assert_path(resp, f"response/{root}/phase/event_id")
self.assert_path(resp, f"response/{root}/phase/phase")
# Area stuff is not needed unless enabling events.
# self.assert_path(resp, f"response/{root}/area/area_id")
# self.assert_path(resp, f"response/{root}/area/end_date")
# self.assert_path(resp, f"response/{root}/area/medal_id")
# self.assert_path(resp, f"response/{root}/area/is_limit")
self.assert_path(resp, f"response/{root}/choco/choco_id")
self.assert_path(resp, f"response/{root}/choco/param")
self.assert_path(resp, f"response/{root}/goods/item_id")
self.assert_path(resp, f"response/{root}/goods/item_type")
self.assert_path(resp, f"response/{root}/goods/price")
self.assert_path(resp, f"response/{root}/goods/goods_type")
def verify_info24_common(self, loc: str) -> None:
call = self.call_node()
# Construct node
info24 = Node.void('info24')
call.add_child(info24)
info24.set_attribute('loc_id', loc)
info24.set_attribute('method', 'common')
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.__verify_common('info24', resp)
def verify_lobby24_getlist(self, loc: str) -> None:
call = self.call_node()
# Construct node
lobby24 = Node.void('lobby24')
call.add_child(lobby24)
lobby24.set_attribute('method', 'getList')
lobby24.add_child(Node.string('location_id', loc))
lobby24.add_child(Node.u8('net_version', 63))
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.assert_path(resp, "response/lobby24/@status")
def __verify_profile(self, resp: Node) -> None:
self.assert_path(resp, "response/player24/account/name")
self.assert_path(resp, "response/player24/account/g_pm_id")
self.assert_path(resp, "response/player24/account/tutorial")
self.assert_path(resp, "response/player24/account/area_id")
self.assert_path(resp, "response/player24/account/use_navi")
self.assert_path(resp, "response/player24/account/read_news")
self.assert_path(resp, "response/player24/account/nice")
self.assert_path(resp, "response/player24/account/favorite_chara")
self.assert_path(resp, "response/player24/account/special_area")
self.assert_path(resp, "response/player24/account/chocolate_charalist")
self.assert_path(resp, "response/player24/account/chocolate_sp_chara")
self.assert_path(resp, "response/player24/account/chocolate_pass_cnt")
self.assert_path(resp, "response/player24/account/chocolate_hon_cnt")
self.assert_path(resp, "response/player24/account/teacher_setting")
self.assert_path(resp, "response/player24/account/welcom_pack")
self.assert_path(resp, "response/player24/account/ranking_node")
self.assert_path(resp, "response/player24/account/chara_ranking_kind_id")
self.assert_path(resp, "response/player24/account/navi_evolution_flg")
self.assert_path(resp, "response/player24/account/ranking_news_last_no")
self.assert_path(resp, "response/player24/account/power_point")
self.assert_path(resp, "response/player24/account/player_point")
self.assert_path(resp, "response/player24/account/power_point_list")
self.assert_path(resp, "response/player24/account/staff")
self.assert_path(resp, "response/player24/account/item_type")
self.assert_path(resp, "response/player24/account/item_id")
self.assert_path(resp, "response/player24/account/is_conv")
self.assert_path(resp, "response/player24/account/license_data")
self.assert_path(resp, "response/player24/account/my_best")
self.assert_path(resp, "response/player24/account/latest_music")
self.assert_path(resp, "response/player24/account/total_play_cnt")
self.assert_path(resp, "response/player24/account/today_play_cnt")
self.assert_path(resp, "response/player24/account/consecutive_days")
self.assert_path(resp, "response/player24/account/total_days")
self.assert_path(resp, "response/player24/account/interval_day")
self.assert_path(resp, "response/player24/account/active_fr_num")
self.assert_path(resp, "response/player24/eaappli/relation")
self.assert_path(resp, "response/player24/info/ep")
self.assert_path(resp, "response/player24/config")
self.assert_path(resp, "response/player24/option")
self.assert_path(resp, "response/player24/custom_cate")
self.assert_path(resp, "response/player24/navi_data")
self.assert_path(resp, "response/player24/mission/mission_id")
self.assert_path(resp, "response/player24/mission/gauge_point")
self.assert_path(resp, "response/player24/mission/mission_comp")
self.assert_path(resp, "response/player24/netvs")
self.assert_path(resp, "response/player24/customize")
self.assert_path(resp, "response/player24/stamp/stamp_id")
self.assert_path(resp, "response/player24/stamp/cnt")
def verify_player24_read(self, ref_id: str, msg_type: str) -> Dict[str, Dict[int, Dict[str, int]]]:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'read')
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.s8('pref', 51))
# Swap with server
resp = self.exchange('', call)
if msg_type == 'new':
# Verify that response is correct
self.assert_path(resp, "response/player24/result")
status = resp.child_value('player24/result')
if status != 2:
raise Exception(f'Reference ID \'{ref_id}\' returned invalid status \'{status}\'')
return {
'items': {},
'characters': {},
'points': {},
}
elif msg_type == 'query':
# Verify that the response is correct
self.__verify_profile(resp)
self.assert_path(resp, "response/player24/result")
status = resp.child_value('player24/result')
if status != 0:
raise Exception(f'Reference ID \'{ref_id}\' returned invalid status \'{status}\'')
name = resp.child_value('player24/account/name')
if name != self.NAME:
raise Exception(f'Invalid name \'{name}\' returned for Ref ID \'{ref_id}\'')
# Medals and items
items: Dict[int, Dict[str, int]] = {}
charas: Dict[int, Dict[str, int]] = {}
courses: Dict[int, Dict[str, int]] = {}
for obj in resp.child('player24').children:
if obj.name == 'item':
items[obj.child_value('id')] = {
'type': obj.child_value('type'),
'param': obj.child_value('param'),
}
elif obj.name == 'chara_param':
charas[obj.child_value('chara_id')] = {
'friendship': obj.child_value('friendship'),
}
elif obj.name == 'course_data':
courses[obj.child_value('course_id')] = {
'clear_type': obj.child_value('clear_type'),
'clear_rank': obj.child_value('clear_rank'),
'total_score': obj.child_value('total_score'),
'count': obj.child_value('update_count'),
'sheet_num': obj.child_value('sheet_num'),
}
return {
'items': items,
'characters': charas,
'courses': courses,
'points': {0: {'points': resp.child_value('player24/account/player_point')}},
}
else:
raise Exception(f'Unrecognized message type \'{msg_type}\'')
def verify_player24_read_score(self, ref_id: str) -> Dict[str, Dict[int, Dict[int, int]]]:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'read_score')
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.s8('pref', 51))
# Swap with server
resp = self.exchange('', call)
# Verify defaults
self.assert_path(resp, "response/player24/@status")
# Grab scores
scores: Dict[int, Dict[int, int]] = {}
medals: Dict[int, Dict[int, int]] = {}
ranks: Dict[int, Dict[int, int]] = {}
for child in resp.child('player24').children:
if child.name != 'music':
continue
musicid = child.child_value('music_num')
chart = child.child_value('sheet_num')
score = child.child_value('score')
medal = child.child_value('clear_type')
rank = child.child_value('clear_rank')
if musicid not in scores:
scores[musicid] = {}
if musicid not in medals:
medals[musicid] = {}
if musicid not in ranks:
ranks[musicid] = {}
scores[musicid][chart] = score
medals[musicid][chart] = medal
ranks[musicid][chart] = rank
return {
'scores': scores,
'medals': medals,
'ranks': ranks,
}
def verify_player24_start(self, ref_id: str, loc: str) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('loc_id', loc)
player24.set_attribute('ref_id', ref_id)
player24.set_attribute('method', 'start')
player24.set_attribute('start_type', '0')
pcb_card = Node.void('pcb_card')
player24.add_child(pcb_card)
pcb_card.add_child(Node.s8('card_enable', 1))
pcb_card.add_child(Node.s8('card_soldout', 0))
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.__verify_common('player24', resp)
def verify_player24_update_ranking(self, ref_id: str, loc: str) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'update_ranking')
player24.add_child(Node.s16('pref', 51))
player24.add_child(Node.string('location_id', loc))
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.string('name', self.NAME))
player24.add_child(Node.s16('chara_num', 1))
player24.add_child(Node.s16('course_id', 12345))
player24.add_child(Node.s32('total_score', 86000))
player24.add_child(Node.s16('music_num', 1375))
player24.add_child(Node.u8('sheet_num', 2))
player24.add_child(Node.u8('clear_type', 7))
player24.add_child(Node.u8('clear_rank', 5))
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.assert_path(resp, "response/player24/all_ranking/name")
self.assert_path(resp, "response/player24/all_ranking/chara_num")
self.assert_path(resp, "response/player24/all_ranking/total_score")
self.assert_path(resp, "response/player24/all_ranking/clear_type")
self.assert_path(resp, "response/player24/all_ranking/clear_rank")
self.assert_path(resp, "response/player24/all_ranking/player_count")
self.assert_path(resp, "response/player24/all_ranking/player_rank")
def verify_player24_logout(self, ref_id: str) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('ref_id', ref_id)
player24.set_attribute('method', 'logout')
# Swap with server
resp = self.exchange('', call)
# Verify that response is correct
self.assert_path(resp, "response/player24/@status")
def verify_player24_write(
self,
ref_id: str,
item: Optional[Dict[str, int]]=None,
character: Optional[Dict[str, int]]=None,
) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'write')
player24.add_child(Node.string('ref_id', ref_id))
# Add required children
config = Node.void('config')
player24.add_child(config)
config.add_child(Node.s16('chara', 1543))
if item is not None:
itemnode = Node.void('item')
player24.add_child(itemnode)
itemnode.add_child(Node.u8('type', item['type']))
itemnode.add_child(Node.u16('id', item['id']))
itemnode.add_child(Node.u16('param', item['param']))
itemnode.add_child(Node.bool('is_new', False))
itemnode.add_child(Node.u64('get_time', 0))
if character is not None:
chara_param = Node.void('chara_param')
player24.add_child(chara_param)
chara_param.add_child(Node.u16('chara_id', character['id']))
chara_param.add_child(Node.u16('friendship', character['friendship']))
# Swap with server
resp = self.exchange('', call)
self.assert_path(resp, "response/player24/@status")
def verify_player24_buy(self, ref_id: str, item: Dict[str, int]) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'buy')
player24.add_child(Node.s32('play_id', 0))
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.u16('id', item['id']))
player24.add_child(Node.u8('type', item['type']))
player24.add_child(Node.u16('param', item['param']))
player24.add_child(Node.s32('lumina', item['points']))
player24.add_child(Node.u16('price', item['price']))
# Swap with server
resp = self.exchange('', call)
self.assert_path(resp, "response/player24/@status")
def verify_player24_write_music(self, ref_id: str, score: Dict[str, Any]) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'write_music')
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.string('data_id', ref_id))
player24.add_child(Node.string('name', self.NAME))
player24.add_child(Node.u8('stage', 0))
player24.add_child(Node.s16('music_num', score['id']))
player24.add_child(Node.u8('sheet_num', score['chart']))
player24.add_child(Node.u8('clear_type', score['medal']))
player24.add_child(Node.s32('score', score['score']))
player24.add_child(Node.s16('combo', 0))
player24.add_child(Node.s16('cool', 0))
player24.add_child(Node.s16('great', 0))
player24.add_child(Node.s16('good', 0))
player24.add_child(Node.s16('bad', 0))
# Swap with server
resp = self.exchange('', call)
self.assert_path(resp, "response/player24/@status")
def verify_player24_new(self, ref_id: str) -> None:
call = self.call_node()
# Construct node
player24 = Node.void('player24')
call.add_child(player24)
player24.set_attribute('method', 'new')
player24.add_child(Node.string('ref_id', ref_id))
player24.add_child(Node.string('name', self.NAME))
player24.add_child(Node.s8('pref', 51))
# Swap with server
resp = self.exchange('', call)
# Verify nodes
self.__verify_profile(resp)
def verify(self, cardid: Optional[str]) -> None:
# Verify boot sequence is okay
self.verify_services_get(
expected_services=[
'pcbtracker',
'pcbevent',
'local',
'message',
'facility',
'cardmng',
'package',
'posevent',
'pkglist',
'dlstatus',
'eacoin',
'lobby',
'ntp',
'keepalive'
]
)
paseli_enabled = self.verify_pcbtracker_alive()
self.verify_message_get()
self.verify_package_list()
location = self.verify_facility_get()
self.verify_pcbevent_put()
self.verify_pcb24_boot(location)
self.verify_info24_common(location)
self.verify_lobby24_getlist(location)
# Verify card registration and profile lookup
if cardid is not None:
card = cardid
else:
card = self.random_card()
print(f"Generated random card ID {card} for use.")
if cardid is None:
self.verify_cardmng_inquire(card, msg_type='unregistered', paseli_enabled=paseli_enabled)
ref_id = self.verify_cardmng_getrefid(card)
if len(ref_id) != 16:
raise Exception(f'Invalid refid \'{ref_id}\' returned when registering card')
if ref_id != self.verify_cardmng_inquire(card, msg_type='new', paseli_enabled=paseli_enabled):
raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')
self.verify_player24_read(ref_id, msg_type='new')
self.verify_player24_new(ref_id)
else:
print("Skipping new card checks for existing card")
ref_id = self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled)
# Verify pin handling and return card handling
self.verify_cardmng_authpass(ref_id, correct=True)
self.verify_cardmng_authpass(ref_id, correct=False)
if ref_id != self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled):
raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')
# Verify proper handling of basic stuff
self.verify_player24_read(ref_id, msg_type='query')
self.verify_player24_start(ref_id, location)
self.verify_player24_write(ref_id)
self.verify_player24_logout(ref_id)
if cardid is None:
# Verify unlocks/story mode work
unlocks = self.verify_player24_read(ref_id, msg_type='query')
for item in unlocks['items']:
if item in [1592, 1608]:
# Song unlocks after one play
continue
raise Exception('Got nonzero items count on a new card!')
for _ in unlocks['characters']:
raise Exception('Got nonzero characters count on a new card!')
for _ in unlocks['courses']:
raise Exception('Got nonzero course count on a new card!')
if unlocks['points'][0]['points'] != 300:
raise Exception('Got wrong default value for points on a new card!')
self.verify_player24_write(ref_id, item={'id': 4, 'type': 2, 'param': 69})
unlocks = self.verify_player24_read(ref_id, msg_type='query')
if 4 not in unlocks['items']:
raise Exception('Expecting to see item ID 4 in items!')
if unlocks['items'][4]['type'] != 2:
raise Exception('Expecting to see item ID 4 to have type 2 in items!')
if unlocks['items'][4]['param'] != 69:
raise Exception('Expecting to see item ID 4 to have param 69 in items!')
self.verify_player24_write(ref_id, character={'id': 5, 'friendship': 420})
unlocks = self.verify_player24_read(ref_id, msg_type='query')
if 5 not in unlocks['characters']:
raise Exception('Expecting to see chara ID 5 in characters!')
if unlocks['characters'][5]['friendship'] != 420:
raise Exception('Expecting to see chara ID 5 to have type 2 in characters!')
# Verify purchases work
self.verify_player24_buy(ref_id, item={'id': 6, 'type': 3, 'param': 8, 'points': 400, 'price': 250})
unlocks = self.verify_player24_read(ref_id, msg_type='query')
if 6 not in unlocks['items']:
raise Exception('Expecting to see item ID 6 in items!')
if unlocks['items'][6]['type'] != 3:
raise Exception('Expecting to see item ID 6 to have type 3 in items!')
if unlocks['items'][6]['param'] != 8:
raise Exception('Expecting to see item ID 6 to have param 8 in items!')
if unlocks['points'][0]['points'] != 150:
raise Exception(f'Got wrong value for points {unlocks["points"][0]["points"]} after purchase!')
# Verify course handling
self.verify_player24_update_ranking(ref_id, location)
unlocks = self.verify_player24_read(ref_id, msg_type='query')
if 12345 not in unlocks['courses']:
raise Exception('Expecting to see course ID 12345 in courses!')
if unlocks['courses'][12345]['clear_type'] != 7:
raise Exception('Expecting to see item ID 12345 to have clear_type 7 in courses!')
if unlocks['courses'][12345]['clear_rank'] != 5:
raise Exception('Expecting to see item ID 12345 to have clear_rank 5 in courses!')
if unlocks['courses'][12345]['total_score'] != 86000:
raise Exception('Expecting to see item ID 12345 to have total_score 86000 in courses!')
if unlocks['courses'][12345]['count'] != 1:
raise Exception('Expecting to see item ID 12345 to have count 1 in courses!')
if unlocks['courses'][12345]['sheet_num'] != 2:
raise Exception('Expecting to see item ID 12345 to have sheet_num 2 in courses!')
# Verify score handling
scores = self.verify_player24_read_score(ref_id)
for _ in scores['medals']:
raise Exception('Got nonzero medals count on a new card!')
for _ in scores['scores']:
raise Exception('Got nonzero scores count on a new card!')
for phase in [1, 2]:
if phase == 1:
dummyscores = [
# An okay score on a chart
{
'id': 987,
'chart': 2,
'medal': 5,
'score': 76543,
},
# A good score on an easier chart of the same song
{
'id': 987,
'chart': 0,
'medal': 6,
'score': 99999,
},
# A bad score on a hard chart
{
'id': 741,
'chart': 3,
'medal': 2,
'score': 45000,
},
# A terrible score on an easy chart
{
'id': 742,
'chart': 1,
'medal': 2,
'score': 1,
},
]
# Random score to add in
songid = random.randint(907, 950)
chartid = random.randint(0, 3)
score = random.randint(0, 100000)
medal = random.randint(1, 11)
dummyscores.append({
'id': songid,
'chart': chartid,
'medal': medal,
'score': score,
})
if phase == 2:
dummyscores = [
# A better score on the same chart
{
'id': 987,
'chart': 2,
'medal': 6,
'score': 98765,
},
# A worse score on another same chart
{
'id': 987,
'chart': 0,
'medal': 3,
'score': 12345,
'expected_score': 99999,
'expected_medal': 6,
},
]
for dummyscore in dummyscores:
self.verify_player24_write_music(ref_id, dummyscore)
scores = self.verify_player24_read_score(ref_id)
for expected in dummyscores:
newscore = scores['scores'][expected['id']][expected['chart']]
newmedal = scores['medals'][expected['id']][expected['chart']]
newrank = scores['ranks'][expected['id']][expected['chart']]
if 'expected_score' in expected:
expected_score = expected['expected_score']
else:
expected_score = expected['score']
if 'expected_medal' in expected:
expected_medal = expected['expected_medal']
else:
expected_medal = expected['medal']
if newscore < 50000:
expected_rank = 1
elif newscore < 62000:
expected_rank = 2
elif newscore < 72000:
expected_rank = 3
elif newscore < 82000:
expected_rank = 4
elif newscore < 90000:
expected_rank = 5
elif newscore < 95000:
expected_rank = 6
elif newscore < 98000:
expected_rank = 7
else:
expected_rank = 8
if newscore != expected_score:
raise Exception(f'Expected a score of \'{expected_score}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got score \'{newscore}\'')
if newmedal != expected_medal:
raise Exception(f'Expected a medal of \'{expected_medal}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got medal \'{newmedal}\'')
if newrank != expected_rank:
raise Exception(f'Expected a rank of \'{expected_rank}\' for song \'{expected["id"]}\' chart \'{expected["chart"]}\' but got rank \'{newrank}\'')
# Sleep so we don't end up putting in score history on the same second
time.sleep(1)
else:
print("Skipping score checks for existing card")
# Verify paseli handling
if paseli_enabled:
print("PASELI enabled for this PCBID, executing PASELI checks")
else:
print("PASELI disabled for this PCBID, skipping PASELI checks")
return
sessid, balance = self.verify_eacoin_checkin(card)
if balance == 0:
print("Skipping PASELI consume check because card has 0 balance")
else:
self.verify_eacoin_consume(sessid, balance, random.randint(0, balance))
self.verify_eacoin_checkout(sessid)

View File

@ -122,6 +122,7 @@ class VersionConstants:
POPN_MUSIC_ECLALE: Final[int] = 23
POPN_MUSIC_USANEKO: Final[int] = 24
POPN_MUSIC_PEACE: Final[int] = 25
POPN_MUSIC_KAIMEI_RIDDLES: Final[int] = 26
REFLEC_BEAT: Final[int] = 1
REFLEC_BEAT_LIMELIGHT: Final[int] = 2

View File

@ -372,6 +372,7 @@ class ImportPopn(ImportBase):
'23': VersionConstants.POPN_MUSIC_ECLALE,
'24': VersionConstants.POPN_MUSIC_USANEKO,
'25': VersionConstants.POPN_MUSIC_PEACE,
'26': VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES,
}.get(version, -1)
if actual_version == VersionConstants.POPN_MUSIC_TUNE_STREET:
@ -383,7 +384,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, 25!")
raise Exception("Unsupported Pop'n Music version, expected one of the following: 19, 20, 21, 22, 23, 24, 25, 26!")
super().__init__(config, GameConstants.POPN_MUSIC, actual_version, no_combine, update)
@ -1056,6 +1057,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_KAIMEI_RIDDLES:
# Based on M39:J:A:A:2022061300
# Normal offset for music DB, size
offset = 0x2DEA68
step = 172
length = 2019
# Offset and step of file DB
file_offset = 0x2CDB00
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 (

View File

@ -28,6 +28,7 @@ from bemani.client.popn import (
PopnMusicEclaleClient,
PopnMusicUsaNekoClient,
PopnMusicPeaceClient,
PopnMusicKaimeiClient,
)
from bemani.client.ddr import (
DDRX2Client,
@ -102,6 +103,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
pcbid,
config,
)
if game == 'pnm-kaimei':
return PopnMusicKaimeiClient(
proto,
pcbid,
config,
)
if game == 'jubeat-saucer':
return JubeatSaucerClient(
proto,
@ -342,6 +349,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-kaimei': {
'name': "Pop'n Music Kaimei riddles",
'model': "M39:J:B:A:2022061300",
'old_profile_model': "M39:J:B:A",
'avs': "2.15.8 r6631",
},
'jubeat-saucer': {
'name': "Jubeat Saucer",
'model': "L44:J:A:A:2014012802",
@ -560,6 +573,7 @@ def main() -> None:
'pnm-23': 'pnm-eclale',
'pnm-24': 'pnm-usaneko',
'pnm-25': 'pnm-peace',
'pnm-26': 'pnm-kaimei',
'iidx-20': 'iidx-tricoro',
'iidx-21': 'iidx-spada',
'iidx-22': 'iidx-pendual',

View File

@ -13,6 +13,7 @@ set -e
./read --series pnm --version 23 "$@"
./read --series pnm --version 24 "$@"
./read --series pnm --version 25 "$@"
./read --series pnm --version 26 "$@"
# Init Jubeat
./read --series jubeat --version saucer "$@"

View File

@ -10,6 +10,7 @@ declare -a arr=(
"pnm-23"
"pnm-24"
"pnm-25"
"pnm-26"
"iidx-20"
"iidx-21"
"iidx-22"