mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-03-21 17:24:33 -05:00
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:
parent
61a2b19c71
commit
5fc9286ee1
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
642
bemani/backend/popn/unilab.py
Normal file
642
bemani/backend/popn/unilab.py
Normal 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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
683
bemani/client/popn/unilab.py
Normal file
683
bemani/client/popn/unilab.py
Normal 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 = "TEST"
|
||||
|
||||
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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
1101
bemani/utils/read.py
1101
bemani/utils/read.py
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ declare -a arr=(
|
|||
"pnm-24"
|
||||
"pnm-25"
|
||||
"pnm-26"
|
||||
"pnm-27"
|
||||
"iidx-20"
|
||||
"iidx-21"
|
||||
"iidx-22"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user