Pop'n Music 27 Unilab support (#94)

* Pop'n Music 27 Unilab support

Known issues:
I don't know how to trigger KAC Lab. This seems to be something that should be able to be accessed on appropriate versions of the dll but I can't seem to figure it out.
Rare softlock on pop'n quest Lively II event if you mess with the phase flags and put the game in an invalid state. In theory (and according to bemaniwiki) the entire event should be clearable on earlier Unilab builds.

Not an issue/will not fix:
狼弦暴威 does not appear in Awakening Elem when the event flag is set. The solution to this (for some reason) is to clear the other 10 events. This is not a bemaniutils issue.
This commit is contained in:
tyam 2025-01-21 16:21:54 -06:00 committed by GitHub
parent 61a2b19c71
commit 5fc9286ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 2457 additions and 5 deletions

View File

@ -280,7 +280,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 scores, profile and events for Beatmania IIDX 20-26,
Pop'n Music 19-26, Jubeat Saucer, Saucer Fulfill, Prop, Qubell, Clan and Festo, Sound
Pop'n Music 19-27, Jubeat Saucer, Saucer Fulfill, Prop, Qubell, Clan and Festo, 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. Note that it also
@ -328,7 +328,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-26, Jubeat
currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-27, Jubeat
Saucer, Fulfill, Prop, Qubell, Clan and Festo, 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,
@ -485,7 +485,7 @@ for how exactly to 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-26 so you will want to run this
run a command like so. This network supports versions 19-27 so you will want to run this
command once for every version, giving the correct DLL file. Note that there are several
versions of each game floating around and the "read" script attempts to support as many
as it can but you might encounter a version of the game which hasn't been mapped yet.

View File

@ -270,6 +270,7 @@ def lookup(protoversion: str, requestgame: str, requestversion: str) -> Dict[str
"24": VersionConstants.POPN_MUSIC_USANEKO,
"25": VersionConstants.POPN_MUSIC_PEACE,
"26": VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES,
"27": VersionConstants.POPN_MUSIC_UNILAB,
},
GameConstants.REFLEC_BEAT: {
"1": VersionConstants.REFLEC_BEAT,

View File

@ -29,6 +29,7 @@ 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.backend.popn.unilab import PopnMusicUnilab
from bemani.common import Model, VersionConstants
from bemani.data import Config, Data
@ -61,6 +62,7 @@ class PopnMusicFactory(Factory):
PopnMusicUsaNeko,
PopnMusicPeace,
PopnMusicKaimei,
PopnMusicUnilab,
]
@classmethod
@ -87,8 +89,10 @@ class PopnMusicFactory(Factory):
return VersionConstants.POPN_MUSIC_USANEKO
if date >= 2018101700 and date < 2021042600:
return VersionConstants.POPN_MUSIC_PEACE
if date >= 2021042600:
if date >= 2021042600 and date < 2022091300:
return VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES
if date >= 2022091300:
return VersionConstants.POPN_MUSIC_UNILAB
return None
if model.gamecode == "G15":
@ -131,6 +135,8 @@ class PopnMusicFactory(Factory):
return PopnMusicUsaNeko(data, config, model)
if parentversion == VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES:
return PopnMusicPeace(data, config, model)
if parentversion == VersionConstants.POPN_MUSIC_UNILAB:
return PopnMusicKaimei(data, config, model)
# Unknown older version
return None
@ -148,6 +154,8 @@ class PopnMusicFactory(Factory):
return PopnMusicPeace(data, config, model)
if version == VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES:
return PopnMusicKaimei(data, config, model)
if version == VersionConstants.POPN_MUSIC_UNILAB:
return PopnMusicUnilab(data, config, model)
# Unknown game version
return None

View File

@ -0,0 +1,642 @@
# 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.kaimei import PopnMusicKaimei
from bemani.common import VersionConstants
from bemani.common.validateddict import Profile
from bemani.data.types import UserID
from bemani.protocol.node import Node
class PopnMusicUnilab(PopnMusicModernBase):
name: str = "Pop'n Music Unilab"
version: int = VersionConstants.POPN_MUSIC_UNILAB
# Biggest ID in the music DB
GAME_MAX_MUSIC_ID: int = 2188
# Biggest deco part ID in the game
GAME_MAX_DECO_ID: int = 81
def previous_version(self) -> PopnMusicBase:
return PopnMusicKaimei(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
0: "No music unlocks",
1: "Phase 1",
2: "Phase 2",
3: "Phase 3",
4: "Phase 4",
5: "Phase 5",
6: "Phase MAX",
},
},
{
# 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",
},
},
{
"name": "Narunaru♪ UniLab jikkenshitsu! event Phase",
"tip": "Narunaru♪ UniLab jikkenshitsu! event Phase for all players.",
"category": "game_config",
"setting": "narunaru_phase",
"values": {
0: "Disabled",
1: "ラブケミ / 悪夢♡ショコラティエ",
2: "001 -どうしんのかいろ-",
3: "MA・TSU・RI / MOVE! (We Keep It Movin')",
4: "斑咲花 / ユメブキ",
5: "ホムンクルスレシピ",
6: "脳ミソ de 向上",
7: "Awakening Wings",
8: "カタルシスの月 (UPPER) / ちくわパフェだよ☆CKP (UPPER) / ホーンテッド★メイドランチ (UPPER)",
9: "HAGURUMA / ノープラン・デイズ / Sweet Illusion",
10: "左脳スパーク (UPPER)",
11: "東京メモリー",
12: "にゃんのパレードマーチ♪",
13: "明滅の果てに",
14: "Shout It Out",
15: "グランデーロの守り",
16: "恋するMonstro",
17: "Versa (UPPER)",
18: "Xジェネの逆襲",
19: "Engraved on my heart ft. 小林マナ",
20: "fallen leaves -IIDX edition-",
21: "Τέλος",
22: "Candy Crime Toe Shoes",
23: "High Speed Junkie!",
24: "Pure Rude",
25: "地方創生☆チクワクティクス (UPPER) / 乙女繚乱 舞い咲き誇れ (UPPER)",
26: "pastel@sweets labo(*'v'*) / 恋はどうモロ◎波動OK☆方程式 (UPPER) / Mecha Kawa Breaker!!",
27: "あまるがむ",
28: "勇猛無比",
29: "Unknown Region",
30: "unisonote",
31: "灰の羽搏",
32: "情熱タンデムRUNAWAY",
33: "Satan",
34: "粋 -IKI-",
35: "Treasure Hoard (UPPER)",
36: "SOLID STATE SQUAD -RISEN RELIC REMIX-",
37: "夏色のセーブデータ",
38: "革命パッショネイト (UPPER) / めうめうぺったんたん!! (UPPER)",
39: "Gabbalungang",
40: "Caldwell 99",
41: "葬送のエウロパ / ただ、それだけの理由で",
42: "ISERBROOK",
43: "Amulet of Enbarr",
44: "Sword of Vengeance",
45: "Caldwell 99",
46: "満漢全席火花ノ舞",
47: "mathematical good-bye / Hexer",
48: "F/S",
},
},
{
# Kakusei no Elem event Phase
"name": "Kakusei no Elem event Phase",
"tip": "Kakusei no Elem event Phase for all players.",
"category": "game_config",
"setting": "kakusei_phase",
"values": {
0: "Disabled",
1: "Tan♪Tan♪Tan♪",
2: "Keep the Faith",
3: "Lovin' You",
4: "Redemption Tears",
5: "Dancin' in シャングリラ",
6: "ココロコースター",
7: "ma plume / ma plume (UPPER)",
8: "いばら姫",
9: "螺旋",
10: "めうめうぺったんたん!! (ZAQUVA Remix) / ちくわパフェだよ☆CKP (Yvya Remix)",
11: "狼弦暴威",
12: "The Escape",
13: "謎情の雫 ft. Kanae Asaba",
14: "黒猫と珈琲",
15: "Head Scratcher",
16: "ドーナツホール (UPPER) / マトリョシカ (UPPER)",
17: "遊戯大熊猫",
18: "Stylus",
19: "Crazy Shuffle",
20: "speedstar[02]",
21: "少年A",
22: "what I wish",
23: "TAKE YOU AWAY",
24: "Dragon Blade -The Arrange-",
25: "Pump up dA CORE",
26: "TURBO BOOSTER",
27: "夜虹",
28: "天泣 ",
29: "オッタマゲッター",
30: "luck (UPPER) / 脳漿炸裂ガール (UPPER)",
31: "TYPHØN",
32: "REFLEXED MANIPULATION",
33: "オーバー ",
34: "Knockin' On Red Button",
35: "The Metalist",
36: "イマココ!この瞬間 ",
37: "チョコレートスマイル (UPPER)",
38: "キリステゴメン (UPPER)",
39: "Liar×Girl / Hades Doll",
40: "Jazz is Rad / アモ",
41: "encounter / 不可説不可説転",
42: "弾幕信仰 / 閉塞的フレーション / 残像ニ繋ガレタ追憶HIDEAWAY",
43: "ROBOROS OVERDIVE / Megalara Garuda",
44: "Megalara Garuda (UPPER)",
},
},
{
# Awakening Boost
"name": "Super Unilab BOOST!",
"tip": "Super Unilab BOOST! for all players.",
"category": "game_config",
"setting": "super_unilab_boost",
"values": {
0: "Disabled",
1: "Active",
2: "Ended",
},
},
{
# CanCan's Super Awakening Boost
"name": "CanCan's Super Awakening Boost",
"tip": "CanCan's Super Awakening Boost for all players.",
"category": "game_config",
"setting": "cancan_boost",
"values": {
0: "Disabled",
1: "Active",
2: "Ended",
},
},
# We don't currently support lobbies or anything, so this is commented out until
# somebody gets around to implementing it.
# {
# # Net Taisen and local mode
# "name": "Net Taisen / Local Mode",
# "tip": "Enable Net Taisen and Local Mode",
# "category": "game_config",
# "setting": "enable_net_taisen_local_mode",
# "values": {
# 0: "Disabled",
# 1: "Net Taisen",
# 2: "Net Taisen / Local Mode",
# },
# },
],
"bools": [
{
"name": "Force Song Unlock",
"tip": "Force unlock all songs.",
"category": "game_config",
"setting": "force_unlock_songs",
},
{
"name": "Force Deco Unlock",
"tip": "Force unlock all Deco parts.",
"category": "game_config",
"setting": "force_unlock_deco",
},
],
}
def get_common_config(self) -> Tuple[Dict[int, int], bool]:
game_config = self.get_game_config()
music_phase = game_config.get_int("music_phase")
narunaru_phase = game_config.get_int("narunaru_phase")
enable_net_taisen = False # game_config.get_bool('enable_net_taisen')
super_unilab_boost = game_config.get_int("super_unilab_boost")
cancan_boost = game_config.get_int("cancan_boost")
kakusei_phase = game_config.get_int("kakusei_phase")
popn_quest_lively_2 = game_config.get_int("popn_quest_lively_2")
# Enable event and mark complete
if game_config.get_bool("force_unlock_deco"):
kakusei_phase = 1
# Event phases
return (
{
# Default song phase availability (0-6)
# 1 - 2071 - Hopes and Dreams/夢と希望
# 2072 - MEGALOVANIA
# 2073 - Battle Against a True Hero/本物のヒーローとの戦い
# 2 - 2146 - ポラリスノウタ
# 3 - 2149 - 第ゼロ感
# 4 - 2150 - 強風オールバック
# 2151 - 恋愛パクチー
# 5 - 2172 - レイドバックジャーニー
# 6 - 2188 - Super Heroine
0: music_phase,
# Shutchou! pop'n quest Lively II (0-6)
# When active, the following songs are available for unlock
# 1 - 1989 - Ketter
# 1990 - Petit Queen
# 1991 - 波と凪の挟間で
# 2 - 1984 - コルドバの女
# 1985 - say...but in vain
# 1992 - Northern Cross
# 3 - 1982 - Surf on the Light
# 1983 - バッドエンド・シンドローム
# 1988 - Danza Pantera
# 4 - 1986 - virkatoの主題によるperson09風超絶技巧変奏曲
# 1987 - 水晶塔のオルカ
# 1993 - Un Happy Heart
# 5 - 2017 - virkatoの主題によるperson09風超絶技巧変奏曲 upper
# 6 - Event Ended
1: popn_quest_lively_2,
# Unknown event (0-4)
2: 4,
# Enable Net Taisen, including win/loss display on song select (0-2)
# 0 - Disable
# 1 - Net taisen
# 2 - Net taisen + Local mode
3: 1 if enable_net_taisen else 0,
# Unknown event (0-7)
4: 1,
# Narunaru♪ UniLab jikkenshitsu! (0-48)
# 6500 clear points are needed unless otherwise specified
# 1 - 2040 - ラブケミ - 1000 points
# 2043 - 悪夢♡ショコラティエ
# 2 - 2044 - 001 -どうしんのかいろ-
# 3 - 2050 - MA・TSU・RI
# 2051 - MOVE! (We Keep It Movin')
# 4 - 2052 - 斑咲花
# 2053 - ユメブキ
# 5 - 2054 - ホムンクルスレシピ
# 6 - 2055 - 脳ミソ de 向上
# 7 - 2059 - Awakening Wings
# 8 - 2062 - カタルシスの月 upper - 5000 points
# 2061 - ホーンテッド★メイドランチ upper - 5000 points
# 2060 - ちくわパフェだよ☆CKP upper - 5000 points
# 9 - 2074 - HAGURUMA - 5000 points
# 2075 - ノープラン・デイズ - 5000 points
# 502 - Sweet Illusion [ex] - 5000 points
# 10 - 2076 - 左脳スパーク upper - 5000 points
# 11 - 2077 - 東京メモリー - 5000 points
# 12 - 2078 - にゃんのパレードマーチ♪ - 5000 points
# 13 - 2079 - 明滅の果てに - 5000 points
# 14 - 2080 - Shout It Out
# 15 - 2081 - グランデーロの守り
# 16 - 2082 - 恋するMonstro
# 17 - 2083 - Versa upper
# 18 - 2084 - Xジェネの逆襲
# 19 - 2085 - Engraved on my heart ft. 小林マナ
# 20 - 2086 - fallen leaves -IIDX edition-
# 21 - 2087 - Τέλος
# 22 - 2088 - Candy Crime Toe Shoes
# 23 - 2089 - High Speed Junkie!
# 24 - 2090 - Pure Rude
# 25 - 2092 - 地方創生☆チクワクティクス upper - 5000 points
# 2091 - 乙女繚乱 舞い咲き誇れ upper - 5000 points
# 26 - 2093 - pastel@sweets labo(*'v'*)
# 2095 - 恋はどうモロ◎波動OK☆方程式 upper - 5000 points
# 2094 - Mecha Kawa Breaker!!
# 27 - 2096 - あまるがむ
# 28 - 2097 - 勇猛無比
# 29 - 2098 - Unknown Region
# 30 - 2110 - unisonote
# 31 - 2107 - 灰の羽搏
# 32 - 2113 - 情熱タンデムRUNAWAY
# 33 - 2108 - Satan
# 34 - 2111 - 粋 -IKI-
# 35 - 2112 - Treasure Hoard upper
# 36 - 2109 - SOLID STATE SQUAD -RISEN RELIC REMIX-
# 37 - 2114 - 夏色のセーブデータ
# 38 - 2117 - めうめうぺったんたん!! upper - 5000 points
# 2116 - 革命パッショネイト upper - 5000 points
# 39 - 2118 - Gabbalungang
# 40 - 2120 - Caldwell 99 - KAC Lab qualifier(?)
# 41 - 2065 - 葬送のエウロパ
# 2064 - ただ、それだけの理由で
# 42 - 2121 - ISERBROOK
# 43 - 2122 - Amulet of Enbarr
# 44 - 2123 - Sword of Vengeance
# 45 - 2120 - Caldwell 99 (KAC version) - 13000 points
# 46 - 2124 - 満漢全席火花ノ舞
# 47 - 2126 - mathematical good-bye - 13000 points
# 2125 - Hexer - 13000 points
# Clearing the limited time event 47 should unlock:
# 48 - 2127 - F/S - 14000
5: narunaru_phase,
# Super Unilab BOOST! (0-2)
# Boost should be 120, 150, or 200, bemaniwiki has the explanation and it's based on the unlocks left to do
6: super_unilab_boost,
# Unknown event (0-6)
7: 6,
# Unknown event (0-2)
8: 2,
# Kakusei no Elem - Awakening Elem (0-44)
# Songs are unlocked as a percentage of 280 points unless otherwise specified
# 0 - Disabled
# 1 - 2128 - Tan♪Tan♪Tan♪
# 2 - 2129 - Keep the Faith
# 3 - 2130 - Lovin' You
# 4 - 2131 - Redemption Tears
# 5 - 2132 - Dancin' in シャングリラ
# 6 - 2133 - ココロコースター
# 7 - 2136 - ma plume
# 2137 - ma plume upper
# 8 - 2135 - いばら姫
# 9 - 2134 - 螺旋
# 10 - 2147 - めうめうぺったんたん!! (ZAQUVA Remix)
# 2148 - ちくわパフェだよ☆CKP (Yvya Remix)
# 11 - 2152 - 狼弦暴威
# - Player must complete the first 12 before this will show up in the event
# 12 - 2067 - The Escape
# 13 - 2153 - 謎情の雫 ft. Kanae Asaba
# 14 - 2154 - 黒猫と珈琲
# 15 - 2155 - Head Scratcher
# 16 - 2156 - ドーナツホール upper - 230 points
# 2157 - マトリョシカ upper - 230 points
# 17 - 2063 - 遊戯大熊猫
# 18 - 2138 - Stylus
# 19 - 2158 - Crazy Shuffle
# 20 - 2159 - speedstar[02]
# 21 - 2160 - 少年A
# 22 - 2161 - what I wish
# 23 - 2068 - TAKE YOU AWAY
# 24 - 2070 - Dragon Blade -The Arrange-
# 25 - 2162 - Pump up dA CORE
# 26 - 2175 - TURBO BOOSTER
# 27 - 2176 - 夜虹
# 28 - 2066 - 天泣
# 29 - 2173 - オッタマゲッター
# 30 - 2177 - luck upper - 230 points
# 2178 - 脳漿炸裂ガール upper - 230 points
# 31 - 2179 - TYPHØN
# 32 - 2069 - REFLEXES MANIPULATION
# 33 - 2174 - オーバー
# 34 - 2115 - Knockin' On Red Button
# 35 - 2180 - The Metalist
# 36 - 2181 - イマココ!この瞬間
# 37 - 2182 - チョコレートスマイル upper - 230 points
# 38 - 2183 - キリステゴメン upper - 230 points
# 39 - 2099 - Liar×Girl
# 2101 - Hades Doll
# 40 - 2102 - Jazz is Rad
# 2104 - アモ
# 41 - 2100 - encounter
# 2103 - 不可説不可説転
# 42 - 2185 - 閉塞的フレーション
# 2186 - 残像ニ繋ガレタ追憶HIDEAWAY
# 2187 - 弾幕信仰
# 43 - 2105 - UROBOROS OVERDIVE
# 2106 - Megalara Garuda
# 44 - 2184 - Megalara Garuda upper
9: kakusei_phase,
# Enable Awakening Elem (0-1)
10: 1 if (kakusei_phase > 0) else 0,
# CanCan's Super Awakening Boost (0-2)
11: cancan_boost,
# Unknown event (0-2)
12: 2,
# Unknown event (0-2)
13: 2,
},
False,
)
def format_profile(self, userid: UserID, profile: Profile) -> Node:
root = super().format_profile(userid, profile)
account = root.child("account")
account.add_child(Node.s16("sp_riddles_id", profile.get_int("sp_riddles_id")))
# options
option = root.child("option")
option.add_child(Node.bool("lift", profile.get_bool("lift")))
option.add_child(Node.s16("lift_rate", profile.get_int("lift_rate")))
# 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 _ 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))
# Narunaru♪ UniLab jikkenshitsu! event
event_p27 = Node.void("event_p27")
root.add_child(event_p27)
event_p27.add_child(Node.s16("team_id", profile.get_int("team_id")))
event_p27.add_child(Node.bool("first_play", profile.get_bool("first_play", True)))
event_p27.add_child(Node.s16("select_battery_id", profile.get_int("select_battery_id", 1)))
event_p27.add_child(Node.bool("elem_first_play", profile.get_bool("elem_first_play", True)))
event_p27.add_child(Node.bool("today_first_play", profile.get_bool("today_first_play", True)))
# Set up Narunaru♪ UniLab jikkenshitsu! achievements
for achievement in achievements:
if achievement.type == "lab":
team_id = achievement.data.get_int("team_id")
ex_no = achievement.data.get_int("ex_no")
point = achievement.data.get_int("point")
is_cleared = achievement.data.get_bool("is_cleared")
team = Node.void("team")
event_p27.add_child(team)
team.add_child(Node.s16("team_id", team_id))
team.add_child(Node.s16("ex_no", ex_no))
team.add_child(Node.u32("point", point))
team.add_child(Node.bool("is_cleared", is_cleared))
# Set up Kakusei no Elem achievements
game_config = self.get_game_config()
if game_config.get_bool("force_unlock_deco"):
battery = Node.void("battery")
event_p27.add_child(battery)
battery.add_child(Node.s16("battery_id", 1))
battery.add_child(Node.u32("energy", 300))
battery.add_child(Node.bool("is_cleared", True))
else:
for achievement in achievements:
if achievement.type == "battery":
battery_id = achievement.data.get_int("battery_id")
energy = achievement.data.get_int("energy")
is_cleared = achievement.data.get_bool("is_cleared")
battery = Node.void("battery")
event_p27.add_child(battery)
battery.add_child(Node.s16("battery_id", battery_id))
battery.add_child(Node.u32("energy", energy))
battery.add_child(Node.bool("is_cleared", is_cleared))
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
newprofile = super().unformat_profile(userid, request, oldprofile)
game_config = self.get_game_config()
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"))
option = request.child("option")
if option is not None:
newprofile.replace_bool("lift", option.child_value("lift"))
newprofile.replace_int("lift_rate", option.child_value("lift_rate"))
# 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
# Unilab event
event_p27 = request.child("event_p27")
if event_p27 is not None:
newprofile.replace_int("team_id", event_p27.child_value("team_id"))
newprofile.replace_bool("first_play", False)
newprofile.replace_bool("select_battery_id", event_p27.child_value("select_battery_id"))
newprofile.replace_bool("elem_first_play", False)
newprofile.replace_bool("today_first_play", False)
# Extract Narunaru♪ UniLab jikkenshitsu! achievements
lab_data = event_p27.child("team")
if lab_data is not None:
team_id = lab_data.child_value("team_id")
ex_no = lab_data.child_value("ex_no")
point = lab_data.child_value("point")
is_cleared = lab_data.child_value("is_cleared")
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
ex_no,
"lab",
{
"team_id": team_id,
"ex_no": ex_no,
"point": point,
"is_cleared": is_cleared,
},
)
# Extract Kakusei no Elem achievements
battery_data = event_p27.child("battery")
if battery_data is not None:
battery_id = battery_data.child_value("battery_id")
energy = battery_data.child_value("energy")
is_cleared = battery_data.child_value("is_cleared")
if not game_config.get_bool("force_unlock_deco"):
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
battery_id,
"battery",
{
"battery_id": battery_id,
"energy": energy,
"is_cleared": is_cleared,
},
)
return newprofile

View File

@ -6,6 +6,7 @@ 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
from bemani.client.popn.unilab import PopnMusicUnilabClient
__all__ = [
@ -17,4 +18,5 @@ __all__ = [
"PopnMusicUsaNekoClient",
"PopnMusicPeaceClient",
"PopnMusicKaimeiClient",
"PopnMusicUnilabClient",
]

View File

@ -0,0 +1,683 @@
import random
import time
from typing import Any, Dict, Optional
from bemani.client.base import BaseClient
from bemani.protocol import Node
class PopnMusicUnilabClient(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(920, 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

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

View File

@ -200,6 +200,7 @@ class APIClient:
VersionConstants.POPN_MUSIC_USANEKO: "24",
VersionConstants.POPN_MUSIC_PEACE: "25",
VersionConstants.POPN_MUSIC_KAIMEI_RIDDLES: "26",
VersionConstants.POPN_MUSIC_UNILAB: "27",
},
GameConstants.REFLEC_BEAT: {
VersionConstants.REFLEC_BEAT: "1",

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,7 @@ from bemani.client.popn import (
PopnMusicUsaNekoClient,
PopnMusicPeaceClient,
PopnMusicKaimeiClient,
PopnMusicUnilabClient,
)
from bemani.client.ddr import (
DDRX2Client,
@ -110,6 +111,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
pcbid,
config,
)
if game == "pnm-unilab":
return PopnMusicUnilabClient(
proto,
pcbid,
config,
)
if game == "jubeat-saucer":
return JubeatSaucerClient(
proto,
@ -370,6 +377,12 @@ def mainloop(
"old_profile_model": "M39:J:B:A",
"avs": "2.15.8 r6631",
},
"pnm-unilab": {
"name": "Pop'n Music Unilab",
"model": "M39:J:B:A:2024073100",
"old_profile_model": "M39:J:B:A",
"avs": "2.15.8 r6631",
},
"jubeat-saucer": {
"name": "Jubeat Saucer",
"model": "L44:J:A:A:2014012802",
@ -626,6 +639,7 @@ def main() -> None:
"pnm-24": "pnm-usaneko",
"pnm-25": "pnm-peace",
"pnm-26": "pnm-kaimei",
"pnm-27": "pnm-unilab",
"iidx-20": "iidx-tricoro",
"iidx-21": "iidx-spada",
"iidx-22": "iidx-pendual",

View File

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