711 lines
30 KiB
Python
711 lines
30 KiB
Python
# vim: set fileencoding=utf-8
|
|
from typing import Dict, Any
|
|
from typing_extensions import Final
|
|
|
|
from bemani.backend.popn.base import PopnMusicBase
|
|
from bemani.backend.popn.stubs import PopnMusicSengokuRetsuden
|
|
|
|
from bemani.backend.base import Status
|
|
from bemani.common import Profile, VersionConstants
|
|
from bemani.data import Score, UserID
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class PopnMusicTuneStreet(PopnMusicBase):
|
|
|
|
name: str = "Pop'n Music TUNE STREET"
|
|
version: int = VersionConstants.POPN_MUSIC_TUNE_STREET
|
|
|
|
# Play modes, as reported by profile save from the game
|
|
GAME_PLAY_MODE_CHALLENGE: Final[int] = 3
|
|
GAME_PLAY_MODE_CHO_CHALLENGE: Final[int] = 4
|
|
GAME_PLAY_MODE_TOWN_CHO_CHALLENGE: Final[int] = 15
|
|
|
|
# Play flags, as saved into/loaded from the DB
|
|
GAME_PLAY_FLAG_FAILED: Final[int] = 0
|
|
GAME_PLAY_FLAG_CLEARED: Final[int] = 1
|
|
GAME_PLAY_FLAG_FULL_COMBO: Final[int] = 2
|
|
GAME_PLAY_FLAG_PERFECT_COMBO: Final[int] = 3
|
|
|
|
# Chart type, as reported by profile save from the game
|
|
GAME_CHART_TYPE_NORMAL: Final[int] = 0
|
|
GAME_CHART_TYPE_HYPER: Final[int] = 1
|
|
GAME_CHART_TYPE_5_BUTTON: Final[int] = 2
|
|
GAME_CHART_TYPE_EX: Final[int] = 3
|
|
GAME_CHART_TYPE_BATTLE_NORMAL: Final[int] = 4
|
|
GAME_CHART_TYPE_BATTLE_HYPER: Final[int] = 5
|
|
GAME_CHART_TYPE_ENJOY_5_BUTTON: Final[int] = 6
|
|
GAME_CHART_TYPE_ENJOY_9_BUTTON: Final[int] = 7
|
|
|
|
# Extra chart types supported by Pop'n 19
|
|
CHART_TYPE_OLD_NORMAL: Final[int] = 4
|
|
CHART_TYPE_OLD_HYPER: Final[int] = 5
|
|
CHART_TYPE_OLD_EX: Final[int] = 6
|
|
CHART_TYPE_ENJOY_5_BUTTON: Final[int] = 7
|
|
CHART_TYPE_ENJOY_9_BUTTON: Final[int] = 8
|
|
CHART_TYPE_5_BUTTON: Final[int] = 9
|
|
|
|
# Chart type, as packed into a hiscore binary
|
|
GAME_CHART_TYPE_5_BUTTON_POSITION: Final[int] = 0
|
|
GAME_CHART_TYPE_NORMAL_POSITION: Final[int] = 1
|
|
GAME_CHART_TYPE_HYPER_POSITION: Final[int] = 2
|
|
GAME_CHART_TYPE_EX_POSITION: Final[int] = 3
|
|
GAME_CHART_TYPE_CHO_NORMAL_POSITION: Final[int] = 4
|
|
GAME_CHART_TYPE_CHO_HYPER_POSITION: Final[int] = 5
|
|
GAME_CHART_TYPE_CHO_EX_POSITION: Final[int] = 6
|
|
|
|
# Highest song ID we can represent
|
|
GAME_MAX_MUSIC_ID: Final[int] = 1045
|
|
|
|
def previous_version(self) -> PopnMusicBase:
|
|
return PopnMusicSengokuRetsuden(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": "Game Phase",
|
|
"tip": "Game unlock phase for all players.",
|
|
"category": "game_config",
|
|
"setting": "game_phase",
|
|
"values": {
|
|
0: "NO PHASE",
|
|
1: "SECRET DATA RELEASE",
|
|
2: "MAX: ALL DATA RELEASE",
|
|
},
|
|
},
|
|
{
|
|
"name": "Town Mode Phase",
|
|
"tip": "Town mode phase for all players.",
|
|
"category": "game_config",
|
|
"setting": "town_phase",
|
|
"values": {
|
|
0: "town mode disabled",
|
|
1: "town phase 1",
|
|
2: "town phase 2",
|
|
3: "Pop'n Naan Festival",
|
|
# 4 seems to be a continuation of town phase 2. Intentionally leaving it out.
|
|
5: "town phase 3",
|
|
6: "town phase 4",
|
|
7: "Miracle 4 + 1",
|
|
# 8 seems to be a continuation of town phase 4. Intentionally leaving it out.
|
|
9: "town phase MAX",
|
|
10: "Find your daughter!",
|
|
# 11 is a continuation of phase MAX after find your daughter, with Tanabata
|
|
# bamboo grass added as well.
|
|
11: "town phase MAX+1",
|
|
12: "Peruri-san visits",
|
|
# 13 is a continuation of phase MAX+1 after peruri-san visits, with Watermelon
|
|
# pattern tank added as well.
|
|
13: "town phase MAX+2",
|
|
14: "Find Deuil!",
|
|
# 15 is a continuation of phase MAX+2 after find deuil, with Tsukimi dumplings
|
|
# added as well.
|
|
15: "town phase MAX+3",
|
|
16: "Landmark stamp rally",
|
|
# 17 is a continuation of MAX+3 after landmark stamp rally ends, but offering
|
|
# no additional stuff.
|
|
},
|
|
},
|
|
],
|
|
"bools": [
|
|
{
|
|
"name": "Force Song Unlock",
|
|
"tip": "Force unlock all songs.",
|
|
"category": "game_config",
|
|
"setting": "force_unlock_songs",
|
|
},
|
|
{
|
|
"name": "Force Customization Unlock",
|
|
"tip": "Force unlock all theme and menu customizations.",
|
|
"category": "game_config",
|
|
"setting": "force_unlock_customizations",
|
|
},
|
|
],
|
|
}
|
|
|
|
def __format_flags_for_score(self, score: Score) -> int:
|
|
# Format song flags (cleared/not, combo flags)
|
|
playedflag = {
|
|
self.CHART_TYPE_5_BUTTON: 0x2000,
|
|
self.CHART_TYPE_OLD_NORMAL: 0x0800,
|
|
self.CHART_TYPE_OLD_HYPER: 0x1000,
|
|
self.CHART_TYPE_OLD_EX: 0x4000,
|
|
self.CHART_TYPE_NORMAL: 0x0800,
|
|
self.CHART_TYPE_HYPER: 0x1000,
|
|
self.CHART_TYPE_EX: 0x4000,
|
|
# We don't have a played flag for these, only cleared/no play
|
|
self.CHART_TYPE_ENJOY_5_BUTTON: 0,
|
|
self.CHART_TYPE_ENJOY_9_BUTTON: 0,
|
|
}[score.chart]
|
|
# Shift value for cleared/failed/combo indicators
|
|
shift = {
|
|
self.CHART_TYPE_5_BUTTON: 4,
|
|
self.CHART_TYPE_OLD_NORMAL: 0,
|
|
self.CHART_TYPE_OLD_HYPER: 2,
|
|
self.CHART_TYPE_OLD_EX: 6,
|
|
self.CHART_TYPE_NORMAL: 0,
|
|
self.CHART_TYPE_HYPER: 2,
|
|
self.CHART_TYPE_EX: 6,
|
|
self.CHART_TYPE_ENJOY_5_BUTTON: 9,
|
|
self.CHART_TYPE_ENJOY_9_BUTTON: 8,
|
|
}[score.chart]
|
|
flags = {
|
|
self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_FLAG_FAILED,
|
|
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_FLAG_FAILED,
|
|
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_FLAG_FAILED,
|
|
self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_FLAG_CLEARED,
|
|
self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_FLAG_CLEARED,
|
|
self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_FLAG_CLEARED,
|
|
self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_FLAG_CLEARED,
|
|
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_FLAG_FULL_COMBO,
|
|
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_FLAG_FULL_COMBO,
|
|
self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_FLAG_FULL_COMBO,
|
|
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_FLAG_PERFECT_COMBO,
|
|
}[score.data.get_int("medal")]
|
|
return (flags << shift) | playedflag
|
|
|
|
def format_profile(self, userid: UserID, profile: Profile) -> Node:
|
|
root = Node.void("playerdata")
|
|
|
|
# Format profile
|
|
binary_profile = [0] * 2198
|
|
|
|
# Copy name. We intentionally leave location 12 alone as it is
|
|
# the null termination for the name if it happens to be 12
|
|
# characters (6 shift-jis kana).
|
|
name_binary = profile.get_str("name", "なし").encode("shift-jis")[0:12]
|
|
for name_pos, byte in enumerate(name_binary):
|
|
binary_profile[name_pos] = byte
|
|
|
|
# Copy game mode. Modes sent to the game are as follows.
|
|
# 0 - Enjoy mode.
|
|
# 1 - Challenge mode.
|
|
# 2 - Battle mode.
|
|
# 3 - Net ranking mode (enabled by setting netvs_phase in game.get).
|
|
# 4 - Cho challenge mode.
|
|
# 5 - Town mode (enabled by event_phase in game.get).
|
|
binary_profile[13] = {
|
|
0: 0,
|
|
1: 0,
|
|
2: 1,
|
|
3: 1,
|
|
4: 4,
|
|
5: 2,
|
|
13: 5,
|
|
14: 5,
|
|
15: 5,
|
|
}[profile.get_int("play_mode")]
|
|
|
|
# Copy miscelaneous values
|
|
binary_profile[15] = profile.get_int("last_play_flag") & 0xFF
|
|
binary_profile[16] = profile.get_int("medal_and_friend") & 0xFF
|
|
binary_profile[37] = profile.get_int("read_news") & 0xFF
|
|
binary_profile[38] = profile.get_int("skin_tex_note") & 0xFF
|
|
binary_profile[39] = profile.get_int("skin_tex_cmn") & 0xFF
|
|
binary_profile[40] = profile.get_int("skin_sd_bgm") & 0xFF
|
|
binary_profile[41] = profile.get_int("skin_sd_se") & 0xFF
|
|
binary_profile[44] = profile.get_int("option") & 0xFF
|
|
binary_profile[45] = (profile.get_int("option") >> 8) & 0xFF
|
|
binary_profile[46] = (profile.get_int("option") >> 16) & 0xFF
|
|
binary_profile[47] = (profile.get_int("option") >> 24) & 0xFF
|
|
binary_profile[48] = profile.get_int("jubeat_collabo") & 0xFF
|
|
binary_profile[49] = (profile.get_int("jubeat_collabo") >> 8) & 0xFF
|
|
|
|
# 52-56 and 56-60 make up two 32 bit colors found in color_3p_flag.
|
|
binary_profile[60] = profile.get_int("chara", -1) & 0xFF
|
|
binary_profile[61] = (profile.get_int("chara", -1) >> 8) & 0xFF
|
|
binary_profile[62] = profile.get_int("music") & 0xFF
|
|
binary_profile[63] = (profile.get_int("music") >> 8) & 0xFF
|
|
binary_profile[64] = profile.get_int("sheet") & 0xFF
|
|
binary_profile[65] = profile.get_int("category") & 0xFF
|
|
binary_profile[66] = profile.get_int("norma_point") & 0xFF
|
|
binary_profile[67] = (profile.get_int("norma_point") >> 8) & 0xFF
|
|
|
|
# Format Scores
|
|
hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 7) * 17) + 7) / 8)
|
|
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
|
|
for score in scores:
|
|
if score.id > self.GAME_MAX_MUSIC_ID:
|
|
continue
|
|
|
|
# Skip any scores for chart types we don't support
|
|
if score.chart in [
|
|
self.CHART_TYPE_EASY,
|
|
]:
|
|
continue
|
|
if score.data.get_int("medal") == self.PLAY_MEDAL_NO_PLAY:
|
|
continue
|
|
|
|
flags = self.__format_flags_for_score(score)
|
|
|
|
flags_index = score.id * 2
|
|
binary_profile[108 + flags_index] = binary_profile[108 + flags_index] | (
|
|
flags & 0xFF
|
|
)
|
|
binary_profile[109 + flags_index] = binary_profile[109 + flags_index] | (
|
|
(flags >> 8) & 0xFF
|
|
)
|
|
|
|
if score.chart in [
|
|
self.CHART_TYPE_ENJOY_5_BUTTON,
|
|
self.CHART_TYPE_ENJOY_9_BUTTON,
|
|
]:
|
|
# We don't return enjoy scores, just the flags that we played them
|
|
continue
|
|
|
|
# Format actual score, according to DB chart position
|
|
hiscore_index = (score.id * 7) + {
|
|
self.CHART_TYPE_5_BUTTON: self.GAME_CHART_TYPE_5_BUTTON_POSITION,
|
|
self.CHART_TYPE_OLD_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
|
|
self.CHART_TYPE_OLD_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
|
|
self.CHART_TYPE_OLD_EX: self.GAME_CHART_TYPE_EX_POSITION,
|
|
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_CHO_NORMAL_POSITION,
|
|
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_CHO_HYPER_POSITION,
|
|
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_CHO_EX_POSITION,
|
|
}[score.chart]
|
|
hiscore_byte_pos = int((hiscore_index * 17) / 8)
|
|
hiscore_bit_pos = int((hiscore_index * 17) % 8)
|
|
hiscore_value = score.points << hiscore_bit_pos
|
|
hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (
|
|
hiscore_value & 0xFF
|
|
)
|
|
hiscore_array[hiscore_byte_pos + 1] = hiscore_array[
|
|
hiscore_byte_pos + 1
|
|
] | ((hiscore_value >> 8) & 0xFF)
|
|
hiscore_array[hiscore_byte_pos + 2] = hiscore_array[
|
|
hiscore_byte_pos + 2
|
|
] | ((hiscore_value >> 16) & 0xFF)
|
|
|
|
# Format most played
|
|
most_played = [
|
|
x[0]
|
|
for x in self.data.local.music.get_most_played(
|
|
self.game, self.version, userid, 20
|
|
)
|
|
]
|
|
while len(most_played) < 20:
|
|
most_played.append(-1)
|
|
profile_pos = 68
|
|
for musicid in most_played:
|
|
binary_profile[profile_pos] = musicid & 0xFF
|
|
binary_profile[profile_pos + 1] = (musicid >> 8) & 0xFF
|
|
profile_pos = profile_pos + 2
|
|
|
|
# Town purchases, including BGM/announcer changes and such.
|
|
# The town customization area will show up if the player owns
|
|
# one or more customization in any of the following four
|
|
# purchase locations. These are all purchased in town mode.
|
|
# - 4-7 are song unlock flags.
|
|
# - 8 appears to be purchased pop-kuns.
|
|
# - 9 appears to be purchased themes.
|
|
# - 10 appears to be purchased BGMs.
|
|
# - 11 appears to be purchased sound effects.
|
|
binary_town = [0] * 141
|
|
town = profile.get_dict("town")
|
|
|
|
# Last play flag, so the selection for 5/9/9+cool sticks.
|
|
binary_town[140] = town.get_int("play_type")
|
|
|
|
# Fill in basic town points, tracked here and returned in basic profile for some reason.
|
|
binary_town[0] = town.get_int("points") & 0xFF
|
|
binary_town[1] = (town.get_int("points") >> 8) & 0xFF
|
|
binary_town[2] = (town.get_int("points") >> 16) & 0xFF
|
|
binary_town[3] = (town.get_int("points") >> 24) & 0xFF
|
|
|
|
# Fill in purchase flags (this is for stuff like BGMs, SEs, Pop-kun customizations, etc).
|
|
bought_flg = town.get_int_array("bought_flg", 3)
|
|
game_config = self.get_game_config()
|
|
force_unlock_songs = game_config.get_bool("force_unlock_songs")
|
|
force_unlock_customizations = game_config.get_bool(
|
|
"force_unlock_customizations"
|
|
)
|
|
|
|
if force_unlock_songs:
|
|
bought_flg[0] = 0xFFFFFFFF
|
|
if force_unlock_customizations:
|
|
bought_flg[1] = 0xFFFFFFFF
|
|
|
|
for flg, off in enumerate([4, 8, 12]):
|
|
binary_town[off + 0] = bought_flg[flg] & 0xFF
|
|
binary_town[off + 1] = (bought_flg[flg] >> 8) & 0xFF
|
|
binary_town[off + 2] = (bought_flg[flg] >> 16) & 0xFF
|
|
binary_town[off + 3] = (bought_flg[flg] >> 24) & 0xFF
|
|
|
|
# Fill in build flags (presumably for what parcels of land have been bought and built on).
|
|
build_flg = town.get_int_array("build_flg", 8)
|
|
for flg, off in enumerate([16, 20, 24, 28, 32, 36, 40, 44]):
|
|
binary_town[off + 0] = build_flg[flg] & 0xFF
|
|
binary_town[off + 1] = (build_flg[flg] >> 8) & 0xFF
|
|
binary_town[off + 2] = (build_flg[flg] >> 16) & 0xFF
|
|
binary_town[off + 3] = (build_flg[flg] >> 24) & 0xFF
|
|
|
|
# Fill in character flags (presumably for character location, orientation, stats, etc).
|
|
chara_flg = town.get_int_array("chara_flg", 19)
|
|
for flg, off in enumerate(
|
|
[
|
|
48,
|
|
52,
|
|
56,
|
|
60,
|
|
64,
|
|
68,
|
|
72,
|
|
76,
|
|
80,
|
|
84,
|
|
88,
|
|
92,
|
|
96,
|
|
100,
|
|
104,
|
|
108,
|
|
112,
|
|
116,
|
|
120,
|
|
]
|
|
):
|
|
binary_town[off + 0] = chara_flg[flg] & 0xFF
|
|
binary_town[off + 1] = (chara_flg[flg] >> 8) & 0xFF
|
|
binary_town[off + 2] = (chara_flg[flg] >> 16) & 0xFF
|
|
binary_town[off + 3] = (chara_flg[flg] >> 24) & 0xFF
|
|
|
|
# Fill in miscellaneous event flags.
|
|
event_flg = town.get_int_array("event_flg", 4)
|
|
for flg, off in enumerate([124, 128, 132, 136]):
|
|
binary_town[off + 0] = event_flg[flg] & 0xFF
|
|
binary_town[off + 1] = (event_flg[flg] >> 8) & 0xFF
|
|
binary_town[off + 2] = (event_flg[flg] >> 16) & 0xFF
|
|
binary_town[off + 3] = (event_flg[flg] >> 24) & 0xFF
|
|
|
|
# Construct final profile
|
|
root.add_child(Node.binary("b", bytes(binary_profile)))
|
|
root.add_child(Node.binary("hiscore", bytes(hiscore_array)))
|
|
root.add_child(Node.binary("town", bytes(binary_town)))
|
|
|
|
return root
|
|
|
|
def unformat_profile(
|
|
self, userid: UserID, request: Node, oldprofile: Profile
|
|
) -> Profile:
|
|
newprofile = oldprofile.clone()
|
|
|
|
# Extract the playmode, important for scores later
|
|
playmode = int(request.attribute("play_mode"))
|
|
newprofile.replace_int("play_mode", playmode)
|
|
|
|
# Extract profile options
|
|
newprofile.replace_int("chara", int(request.attribute("chara_num")))
|
|
if "option" in request.attributes:
|
|
newprofile.replace_int("option", int(request.attribute("option")))
|
|
if "last_play_flag" in request.attributes:
|
|
newprofile.replace_int(
|
|
"last_play_flag", int(request.attribute("last_play_flag"))
|
|
)
|
|
if "medal_and_friend" in request.attributes:
|
|
newprofile.replace_int(
|
|
"medal_and_friend", int(request.attribute("medal_and_friend"))
|
|
)
|
|
if "music_num" in request.attributes:
|
|
newprofile.replace_int("music", int(request.attribute("music_num")))
|
|
if "sheet_num" in request.attributes:
|
|
newprofile.replace_int("sheet", int(request.attribute("sheet_num")))
|
|
if "category_num" in request.attributes:
|
|
newprofile.replace_int("category", int(request.attribute("category_num")))
|
|
if "read_news_no_max" in request.attributes:
|
|
newprofile.replace_int(
|
|
"read_news", int(request.attribute("read_news_no_max"))
|
|
)
|
|
if "jubeat_collabo" in request.attributes:
|
|
newprofile.replace_int(
|
|
"jubeat_collabo", int(request.attribute("jubeat_collabo"))
|
|
)
|
|
if "norma_point" in request.attributes:
|
|
newprofile.replace_int("norma_point", int(request.attribute("norma_point")))
|
|
if "skin_tex_note" in request.attributes:
|
|
newprofile.replace_int(
|
|
"skin_tex_note", int(request.attribute("skin_tex_note"))
|
|
)
|
|
if "skin_tex_cmn" in request.attributes:
|
|
newprofile.replace_int(
|
|
"skin_tex_cmn", int(request.attribute("skin_tex_cmn"))
|
|
)
|
|
if "skin_sd_bgm" in request.attributes:
|
|
newprofile.replace_int("skin_sd_bgm", int(request.attribute("skin_sd_bgm")))
|
|
if "skin_sd_se" in request.attributes:
|
|
newprofile.replace_int("skin_sd_se", int(request.attribute("skin_sd_se")))
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid)
|
|
|
|
# Extract scores
|
|
for node in request.children:
|
|
if node.name == "music":
|
|
songid = int(node.attribute("music_num"))
|
|
chart = int(node.attribute("sheet_num"))
|
|
points = int(node.attribute("score"))
|
|
data = int(node.attribute("data"))
|
|
|
|
# We never save battle scores
|
|
if chart in [
|
|
self.GAME_CHART_TYPE_BATTLE_NORMAL,
|
|
self.GAME_CHART_TYPE_BATTLE_HYPER,
|
|
]:
|
|
continue
|
|
|
|
# Arrange order to be compatible with future mixes
|
|
if playmode in {
|
|
self.GAME_PLAY_MODE_CHO_CHALLENGE,
|
|
self.GAME_PLAY_MODE_TOWN_CHO_CHALLENGE,
|
|
}:
|
|
if chart in [
|
|
self.GAME_CHART_TYPE_5_BUTTON,
|
|
self.GAME_CHART_TYPE_ENJOY_5_BUTTON,
|
|
self.GAME_CHART_TYPE_ENJOY_9_BUTTON,
|
|
]:
|
|
# We don't save 5 button for cho scores, or enjoy modes
|
|
continue
|
|
chart = {
|
|
self.GAME_CHART_TYPE_NORMAL: self.CHART_TYPE_NORMAL,
|
|
self.GAME_CHART_TYPE_HYPER: self.CHART_TYPE_HYPER,
|
|
self.GAME_CHART_TYPE_EX: self.CHART_TYPE_EX,
|
|
}[chart]
|
|
else:
|
|
chart = {
|
|
self.GAME_CHART_TYPE_NORMAL: self.CHART_TYPE_OLD_NORMAL,
|
|
self.GAME_CHART_TYPE_HYPER: self.CHART_TYPE_OLD_HYPER,
|
|
self.GAME_CHART_TYPE_5_BUTTON: self.CHART_TYPE_5_BUTTON,
|
|
self.GAME_CHART_TYPE_EX: self.CHART_TYPE_OLD_EX,
|
|
self.GAME_CHART_TYPE_ENJOY_5_BUTTON: self.CHART_TYPE_ENJOY_5_BUTTON,
|
|
self.GAME_CHART_TYPE_ENJOY_9_BUTTON: self.CHART_TYPE_ENJOY_9_BUTTON,
|
|
}[chart]
|
|
|
|
# Extract play flags
|
|
shift = {
|
|
self.CHART_TYPE_5_BUTTON: 4,
|
|
self.CHART_TYPE_OLD_NORMAL: 0,
|
|
self.CHART_TYPE_OLD_HYPER: 2,
|
|
self.CHART_TYPE_OLD_EX: 6,
|
|
self.CHART_TYPE_NORMAL: 0,
|
|
self.CHART_TYPE_HYPER: 2,
|
|
self.CHART_TYPE_EX: 6,
|
|
self.CHART_TYPE_ENJOY_5_BUTTON: 9,
|
|
self.CHART_TYPE_ENJOY_9_BUTTON: 8,
|
|
}[chart]
|
|
|
|
if chart in [
|
|
self.CHART_TYPE_ENJOY_5_BUTTON,
|
|
self.CHART_TYPE_ENJOY_9_BUTTON,
|
|
]:
|
|
# We only store cleared or not played for enjoy mode
|
|
mask = 0x1
|
|
else:
|
|
# We store all data for regular charts
|
|
mask = 0x3
|
|
|
|
# Grab flags, map to medals in DB. Choose lowest one for each so
|
|
# a newer pop'n can still improve scores and medals.
|
|
flags = (data >> shift) & mask
|
|
medal = {
|
|
self.GAME_PLAY_FLAG_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED,
|
|
self.GAME_PLAY_FLAG_CLEARED: self.PLAY_MEDAL_CIRCLE_CLEARED,
|
|
self.GAME_PLAY_FLAG_FULL_COMBO: self.PLAY_MEDAL_CIRCLE_FULL_COMBO,
|
|
self.GAME_PLAY_FLAG_PERFECT_COMBO: self.PLAY_MEDAL_PERFECT,
|
|
}[flags]
|
|
self.update_score(userid, songid, chart, points, medal)
|
|
|
|
# Update town mode data.
|
|
town = newprofile.get_dict("town")
|
|
|
|
# Basic stuff that's in the base node for no reason?
|
|
if "tp" in request.attributes:
|
|
town.replace_int("points", int(request.attribute("tp")))
|
|
|
|
# Stuff that is in the town node
|
|
townnode = request.child("town")
|
|
if townnode is not None:
|
|
if "play_type" in townnode.attributes:
|
|
town.replace_int("play_type", int(townnode.attribute("play_type")))
|
|
if "base" in townnode.attributes:
|
|
town.replace_int_array(
|
|
"base", 4, [int(x) for x in townnode.attribute("base").split(",")]
|
|
)
|
|
if "bought_flg" in townnode.attributes:
|
|
bought_array = [
|
|
int(x) for x in townnode.attribute("bought_flg").split(",")
|
|
]
|
|
if len(bought_array) == 3:
|
|
game_config = self.get_game_config()
|
|
force_unlock_songs = game_config.get_bool("force_unlock_songs")
|
|
force_unlock_customizations = game_config.get_bool(
|
|
"force_unlock_customizations"
|
|
)
|
|
old_bought_array = town.get_int_array("bought_flg", 3)
|
|
|
|
if force_unlock_songs:
|
|
# Don't save force unlocked flags, it'll clobber the profile.
|
|
bought_array[0] = old_bought_array[0]
|
|
if force_unlock_customizations:
|
|
# Don't save force unlocked flags, it'll clobber the profile.
|
|
bought_array[1] = old_bought_array[1]
|
|
|
|
town.replace_int_array("bought_flg", 3, bought_array)
|
|
if "build_flg" in townnode.attributes:
|
|
town.replace_int_array(
|
|
"build_flg",
|
|
8,
|
|
[int(x) for x in townnode.attribute("build_flg").split(",")],
|
|
)
|
|
if "chara_flg" in townnode.attributes:
|
|
town.replace_int_array(
|
|
"chara_flg",
|
|
19,
|
|
[int(x) for x in townnode.attribute("chara_flg").split(",")],
|
|
)
|
|
if "event_flg" in townnode.attributes:
|
|
town.replace_int_array(
|
|
"event_flg",
|
|
4,
|
|
[int(x) for x in townnode.attribute("event_flg").split(",")],
|
|
)
|
|
for bid in range(8):
|
|
if f"building_{bid}" in townnode.attributes:
|
|
town.replace_int_array(
|
|
f"building_{bid}",
|
|
8,
|
|
[
|
|
int(x)
|
|
for x in townnode.attribute(f"building_{bid}").split(",")
|
|
],
|
|
)
|
|
|
|
newprofile.replace_dict("town", town)
|
|
|
|
return newprofile
|
|
|
|
def handle_game_get_request(self, request: Node) -> Node:
|
|
game_config = self.get_game_config()
|
|
game_phase = game_config.get_int("game_phase")
|
|
town_phase = game_config.get_int("town_phase")
|
|
|
|
root = Node.void("game")
|
|
root.set_attribute(
|
|
"game_phase", str(game_phase)
|
|
) # Phase unlocks, for song availability.
|
|
root.set_attribute("boss_battle_point", "1")
|
|
root.set_attribute("boss_diff", "100,100,100,100,100,100,100,100,100,100")
|
|
root.set_attribute("card_phase", "3")
|
|
root.set_attribute(
|
|
"event_phase", str(town_phase)
|
|
) # Town mode, for the main event.
|
|
root.set_attribute("gfdm_phase", "2")
|
|
root.set_attribute("ir_phase", "14")
|
|
root.set_attribute("jubeat_phase", "2")
|
|
root.set_attribute("local_matching_enable", "1")
|
|
root.set_attribute("matching_sec", "120")
|
|
root.set_attribute(
|
|
"netvs_phase", "0"
|
|
) # Net taisen mode phase, maximum 18 (no lobby support).
|
|
return root
|
|
|
|
def handle_game_active_request(self, request: Node) -> Node:
|
|
# Update the name of this cab for admin purposes
|
|
self.update_machine_name(request.attribute("shop_name"))
|
|
return Node.void("game")
|
|
|
|
def handle_playerdata_expire_request(self, request: Node) -> Node:
|
|
return Node.void("playerdata")
|
|
|
|
def handle_playerdata_logout_request(self, request: Node) -> Node:
|
|
return Node.void("playerdata")
|
|
|
|
def handle_playerdata_get_request(self, request: Node) -> Node:
|
|
modelstring = request.attribute("model")
|
|
refid = request.attribute("ref_id")
|
|
root = self.get_profile_by_refid(
|
|
refid,
|
|
self.NEW_PROFILE_ONLY if modelstring is None else self.OLD_PROFILE_ONLY,
|
|
)
|
|
if root is None:
|
|
root = Node.void("playerdata")
|
|
root.set_attribute("status", str(Status.NO_PROFILE))
|
|
return root
|
|
|
|
def handle_playerdata_town_request(self, request: Node) -> Node:
|
|
refid = request.attribute("ref_id")
|
|
root = Node.void("playerdata")
|
|
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is None:
|
|
return root
|
|
|
|
profile = self.get_profile(userid)
|
|
if profile is None:
|
|
return root
|
|
|
|
town = profile.get_dict("town")
|
|
|
|
residence = Node.void("residence")
|
|
root.add_child(residence)
|
|
residence.set_attribute("id", str(town.get_int("residence")))
|
|
|
|
# It appears there can be up to 9 map nodes, not sure why. I'm only returning the
|
|
# first one. Perhaps if there's multiple towns, the residence ID lets you choose
|
|
# between them? Maybe it has to do with friends towns?
|
|
mapdata = [0] * 180
|
|
|
|
# Map over progress for base and buildings. Positions 173-176 are for base flags.
|
|
base = town.get_int_array("base", 4)
|
|
for i in range(4):
|
|
mapdata[173 + i] = base[i]
|
|
|
|
# Positions 42-105 are for building flags.
|
|
for bid, start in enumerate([42, 50, 58, 66, 74, 82, 90, 98]):
|
|
building = town.get_int_array(f"building_{bid}", 8)
|
|
for i in range(8):
|
|
mapdata[start + i] = building[i]
|
|
|
|
mapnode = Node.binary("map", bytes(mapdata))
|
|
root.add_child(mapnode)
|
|
mapnode.set_attribute("residence", "0")
|
|
|
|
return root
|
|
|
|
def handle_playerdata_new_request(self, request: Node) -> Node:
|
|
refid = request.attribute("ref_id")
|
|
name = request.attribute("name")
|
|
root = self.new_profile_by_refid(refid, name)
|
|
if root is None:
|
|
root = Node.void("playerdata")
|
|
root.set_attribute("status", str(Status.NO_PROFILE))
|
|
return root
|
|
|
|
def handle_playerdata_set_request(self, request: Node) -> Node:
|
|
refid = request.attribute("ref_id")
|
|
|
|
root = Node.void("playerdata")
|
|
if refid is None:
|
|
return root
|
|
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
if userid is None:
|
|
return root
|
|
|
|
oldprofile = self.get_profile(userid) or Profile(
|
|
self.game, self.version, refid, 0
|
|
)
|
|
newprofile = self.unformat_profile(userid, request, oldprofile)
|
|
|
|
if newprofile is not None:
|
|
self.put_profile(userid, newprofile)
|
|
|
|
return root
|
|
|
|
def handle_lobby_requests(self, request: Node) -> Node:
|
|
# Stub out the entire lobby service
|
|
return Node.void("lobby")
|