Update unilab.py (#101)

Adjust max deco id to final value and actually handle the decopa fake "force unlock" that somehow got lost in the shuffle of the older commits

* Event ID documentation

Properly handle the permanent unlocks for event id 1, relocate lively quest to the right value, document attract mode ad flags.

This leaves one event unaccounted for. I'll circle back around to this later to see if I can figure out the final event id at some point in the future.

* Update bootstrap

Add popn 27 to bootstrap list

* Update README.md

Add peace, kaimei riddles, and unilab to api readme

* Update endpoints.py

Aligned comments for unicode values with the correct lines

* Update unilab.py

Add some missing client test stuff

* Update read.py

Last datecode missing for unilab (not counting 2024091700 which is identical to 2024073100).  This should be the last unicode change.

* Ignore deco parts when force deco is enabled for Unilab.

* Fix formatting/lint issues.

---------

Co-authored-by: Jennifer Taylor <dragonminded@dragonminded.com>
This commit is contained in:
tyam
2026-06-03 17:22:39 -05:00
committed by GitHub
parent 56c80a4f1c
commit 4897f8e1f0
7 changed files with 174 additions and 40 deletions

View File

@@ -187,8 +187,8 @@ Valid game series and their versions are as follows. Clients and servers should
* ``22`` - Pop'n Music Lapistoria
* ``23`` - Pop'n Music Eclale
* ``24`` - Pop'n Music うさぎと猫と少年の夢
* ``25`` - Pop'n Music Peace
* ``26`` - Pop'n Music Kaimei Riddles
* ``25`` - Pop'n Music peace
* ``26`` - Pop'n Music 解明リドルズ
* ``27`` - Pop'n Music Unilab
* ``reflecbeat``
* ``1`` - REFLEC BEAT

View File

@@ -1075,6 +1075,9 @@ class PopnMusicModernBase(PopnMusicBase, ABC):
if game_config.get_bool("force_unlock_songs") and itemtype == 0:
# We already sent song unlocks in the force unlock section above.
continue
if game_config.get_bool("force_unlock_deco") and itemtype == 7:
# We already sent deco part unlocks in the force unlock section.
continue
item = Node.void("item")
root.add_child(item)
@@ -1331,6 +1334,9 @@ class PopnMusicModernBase(PopnMusicBase, ABC):
if game_config.get_bool("force_unlock_songs") and itemtype == 0:
# If we enabled force song unlocks, don't save songs to the profile.
continue
if game_config.get_bool("force_unlock_deco") and itemtype == 7:
# If we enabled force deco parts unlock, don't save deco parts.
continue
self.data.local.user.put_achievement(
self.game,

View File

@@ -20,7 +20,7 @@ class PopnMusicUnilab(PopnMusicModernBase):
GAME_MAX_MUSIC_ID: int = 2188
# Biggest deco part ID in the game
GAME_MAX_DECO_ID: int = 81
GAME_MAX_DECO_ID: int = 109
def previous_version(self) -> PopnMusicBase:
return PopnMusicKaimei(self.data, self.config, self.model)
@@ -50,9 +50,9 @@ class PopnMusicUnilab(PopnMusicModernBase):
},
},
{
# 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.",
# Shutchou! pop'n quest Lively II ~nostalgia~ event
"name": "Shutchou! pop'n quest Lively II 〜nostalgia〜 phase",
"tip": "Shutchou! pop'n quest Lively II 〜nostalgia〜 phase for all players.",
"category": "game_config",
"setting": "popn_quest_lively_2",
"values": {
@@ -291,23 +291,24 @@ class PopnMusicUnilab(PopnMusicModernBase):
# 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,
# Permanent song unlocks (0-6)
# This setting PERMANENTLY unlocks 86 songs by granting them in the database.
# Reverting this to a lower number will not remove these songs from the profile.
# 1 - Unlock 45 songs from 解明!MN探偵社:
# 195, 267, 323, 521, 575, 1894, 1896, 1908, 1914, 1915, 1916, 1924, 1925, 1926, 1927, 1928,
# 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 1943, 1944, 1945,
# 1946, 1947, 1955, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1965, 1968, 1969, 1970
# 2 - Unlock all 6 songs from BEMANI 2021真夏の歌合戦5番勝負:
# 1948, 1949, 1950, 1951, 1952, 1953
# 3 - Unlock all 11 songs from 冬のポップン!スペシャルコンボ!
# 1917, 1918, 1919, 1920, 1921, 1922, 1923, 1971, 1972, 1973, 1974
# 4 - Unlock 12 songs from 解明!MN探偵社:
# 1976, 1977, 1978, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006
# 5 - Unlock the final 10 songs from 解明!MN探偵社:
# 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2015, 2016, 2018
# 6 - Unlock 2 songs from BPL応援 楽曲解禁スタンプラリー:
# 1954, 1956
1: 6,
# KAC 2023 (0-4) - Please see the site below for what songs are in set A and set B
# https://bemaniwiki.com/?%B8%F8%BC%B0%C2%E7%B2%F1/KONAMI+Arcade+Championship%282023%29/%CD%BD%C1%AA%A5%E9%A5%A6%A5%F3%A5%C9#popn
# 0 - Disabled
@@ -321,7 +322,16 @@ class PopnMusicUnilab(PopnMusicModernBase):
# 1 - Net taisen
# 2 - Net taisen + Local mode
3: 1 if enable_net_taisen else 0,
# Unknown event (0-7)
# Paseli Festival Attract mode ads (0-7)
# As updates to datecodes happen the older ones are are completely locked out
# 0 - Paseli Festival 2022 - 5000 yen gift for 400 winners! - 2022-09-11 through 2022-09-25
# 1 - Disabled
# 2 - Paseli Festival 2022-2023 - 10,000 yen gift for 1,210 winners! - 2022-12-15 through 2023-01-15
# 3 - Appears identical to 2
# 4 - Paseli Festival 2023 - 10,000 yen gift for 5,000 winners! - 2023-06-01 through 2023-06-30
# 5 - Disabled
# 6 - Paseli Festival 2023 - 10,000 yen gift for 5,000 winners! - 2023-11-22 through 2023-12-24
# 7 - Disabled
4: 7,
# Narunaru♪ UniLab jikkenshitsu! (0-48)
# 6500 clear points are needed unless otherwise specified
@@ -391,9 +401,27 @@ class PopnMusicUnilab(PopnMusicModernBase):
# 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)
# Shutchou! pop'n quest Lively II ~nostalgia~ (0-6)
# When active, the following songs are available for unlock
# 1 - 1990 - Petit Queen
# 1991 - 波と凪の挟間で
# 1989 - Ketter
# 2 - 1984 - コルドバの女
# 1985 - say...but in vain
# 1992 - Northern Cross
# 3 - 1982 - Surf on the Light
# 1988 - Danza Pantera
# 1983 - バッドエンド・シンドローム
# 4 - 1993 - Un Happy Heart
# 1987 - 水晶塔のオルカ
# 1986 - virkatoの主題によるperson09風超絶技巧変奏曲
# 5 - 2017 - virkatoの主題によるperson09風超絶技巧変奏曲 upper
# 6 - Event Ended
7: popn_quest_lively_2,
# Attract mode ad campaign (0-2)
# 0 - Off
# 1 - pop'n PIICA / clear file campaign
# 2 - Ended
8: 2,
# Kakusei no Elem - Awakening Elem (0-44)
# Songs are unlocked as a percentage of 280 points unless otherwise specified
@@ -458,7 +486,10 @@ class PopnMusicUnilab(PopnMusicModernBase):
10: 1 if (kakusei_phase > 0) else 0,
# CanCan's Super Awakening Boost (0-2)
11: cancan_boost,
# Unknown event (0-2)
# BEMANI Pro League 3 ad in attract mode (0-2)
# 0 - Off
# 1 - Streaming on Youtube every Wednesday
# 2 - Ended
12: 2,
# Unknown event (0-2)
13: 2,
@@ -570,6 +601,17 @@ class PopnMusicUnilab(PopnMusicModernBase):
battery.add_child(Node.u32("energy", energy))
battery.add_child(Node.bool("is_cleared", is_cleared))
# Fake unlock all deco items if enabled
if game_config.get_bool("force_unlock_deco"):
for deco_id in range(1, self.GAME_MAX_DECO_ID + 1):
item = Node.void("item")
item.add_child(Node.u8("type", 7))
item.add_child(Node.u16("id", deco_id))
item.add_child(Node.u16("param", 0))
item.add_child(Node.bool("is_new", False))
item.add_child(Node.u64("get_time", 0))
root.add_child(item)
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:

View File

@@ -135,6 +135,20 @@ class PopnMusicUnilabClient(BaseClient):
self.assert_path(resp, "response/player24/customize")
self.assert_path(resp, "response/player24/stamp/stamp_id")
self.assert_path(resp, "response/player24/stamp/cnt")
# Kaimei Riddles
self.assert_path(resp, "response/player24/event2021/point")
self.assert_path(resp, "response/player24/event2021/step")
self.assert_path(resp, "response/player24/event2021/quest_point")
self.assert_path(resp, "response/player24/event2021/step_nos")
self.assert_path(resp, "response/player24/event2021/quest_point_nos")
self.assert_path(resp, "response/player24/account/sp_riddles_id")
self.assert_path(resp, "response/player24/riddles_data/sh_riddles/sh_riddles_id")
# Unilab
self.assert_path(resp, "response/player24/event_p27/team_id")
self.assert_path(resp, "response/player24/event_p27/first_play")
self.assert_path(resp, "response/player24/event_p27/select_battery_id")
self.assert_path(resp, "response/player24/event_p27/elem_first_play")
self.assert_path(resp, "response/player24/event_p27/today_first_play")
def verify_player24_read(self, ref_id: str, msg_type: str) -> Dict[str, Dict[int, Dict[str, int]]]:
call = self.call_node()

View File

@@ -413,10 +413,10 @@ class TXP2File(TrackedCoverage, VerboseOutput):
window[window_i] = comp[comp_i]
window_i += 1
comp_i += 1
window_i &= 0xfff
window_i &= 0xFFF
else:
slide_off = (((comp[comp_i + 1] & 0xf0) << 4) | comp[comp_i]) & 0xfff
slide_len = (comp[comp_i + 1] & 0x0f) + 3
slide_off = (((comp[comp_i + 1] & 0xF0) << 4) | comp[comp_i]) & 0xFFF
slide_len = (comp[comp_i + 1] & 0x0F) + 3
comp_i += 2
if (len(decomp) + slide_len) > decomp_len:
@@ -427,8 +427,8 @@ class TXP2File(TrackedCoverage, VerboseOutput):
window[window_i] = window[slide_off]
window_i += 1
slide_off += 1
window_i &= 0xfff
slide_off &= 0xfff
window_i &= 0xFFF
slide_off &= 0xFFF
slide_len -= 1
control >>= 1
i += 1

View File

@@ -335,13 +335,13 @@ def updatename() -> Dict[str, Any]:
if (
re.match(
"^["
+ "\uff20-\uff3a"
+ "\uff41-\uff5a" # widetext A-Z and @
+ "\uff10-\uff19" # widetext a-z
+ "\uff0c\uff0e\uff3f" # widetext 0-9
+ "\u3041-\u308d\u308f\u3092\u3093" # widetext ,._
+ "\u30a1-\u30ed\u30ef\u30f2\u30f3\u30fc" # hiragana
+ "]*$", # katakana
+ "\uff20-\uff3a" # widetext A-Z and @
+ "\uff41-\uff5a" # widetext a-z
+ "\uff10-\uff19" # widetext 0-9
+ "\uff0c\uff0e\uff3f" # widetext ,._
+ "\u3041-\u308d\u308f\u3092\u3093" # hiragana
+ "\u30a1-\u30ed\u30ef\u30f2\u30f3\u30fc" # katakana
+ "]*$",
name,
)
is None

View File

@@ -2718,6 +2718,78 @@ class ImportPopn(ImportBase):
available_charts=available_charts,
)
)
# Based on M39:J:A:A:2023112800
configurations.append(
PopnScrapeConfiguration(
version="M39:J:A:A:2023112800",
# Normal offset for music DB, size
offset=0x2C14C8,
step=172,
length=2139,
# Offset and step of file DB
file_offset=0x2AF6C0,
file_step=32,
# Standard lookups
genre_offset=0,
title_offset=1,
artist_offset=2,
comment_offset=3,
english_title_offset=4,
english_artist_offset=5,
extended_genre_offset=None,
charts_offset=8,
folder_offset=9,
# Offsets for normal chart difficulties
easy_offset=12,
normal_offset=13,
hyper_offset=14,
ex_offset=15,
# Offsets for battle chart difficulties
battle_normal_offset=16,
battle_hyper_offset=17,
# Offsets into which offset to seek to for file lookups
easy_file_offset=18,
normal_file_offset=19,
hyper_file_offset=20,
ex_file_offset=21,
battle_normal_file_offset=22,
battle_hyper_file_offset=23,
packedfmt=(
"<"
"I" # Genre
"I" # Title
"I" # Artist
"I" # Comment
"I" # English Title
"I" # English Artist
"H" # ??
"H" # ??
"I" # Available charts mask
"I" # Folder
"I" # Event unlocks?
"I" # Event unlocks?
"B" # Easy difficulty
"B" # Normal difficulty
"B" # Hyper difficulty
"B" # EX difficulty
"B" # Battle normal difficulty
"B" # Battle hyper difficulty
"xx" # Unknown pointer
"H" # Easy chart pointer
"H" # Normal chart pointer
"H" # Hyper chart pointer
"H" # EX chart pointer
"H" # Battle normal pointer
"H" # Battle hyper pointer
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
),
# Offsets into file DB for finding file and folder.
file_folder_offset=0,
file_name_offset=1,
filefmt="<" "I" "I" "I" "I" "I" "I" "I" "I", # Folder # Filename
available_charts=available_charts,
)
)
# Based on M39:J:A:A:2023121800
configurations.append(
PopnScrapeConfiguration(