1
0
mirror of synced 2024-12-04 10:37:56 +01:00
bemaniutils/bemani/backend/popn/fantasia.py

461 lines
22 KiB
Python
Raw Normal View History

# vim: set fileencoding=utf-8
import copy
from typing import Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.tunestreet import PopnMusicTuneStreet
from bemani.backend.base import Status
from bemani.common import ValidatedDict, VersionConstants, Time, ID
from bemani.data import Score, UserID
from bemani.protocol import Node
class PopnMusicFantasia(PopnMusicBase):
name = "Pop'n Music fantasia"
version = VersionConstants.POPN_MUSIC_FANTASIA
# Chart type, as returned from the game
GAME_CHART_TYPE_EASY = 2
GAME_CHART_TYPE_NORMAL = 0
GAME_CHART_TYPE_HYPER = 1
GAME_CHART_TYPE_EX = 3
# Chart type, as packed into a hiscore binary
GAME_CHART_TYPE_EASY_POSITION = 0
GAME_CHART_TYPE_NORMAL_POSITION = 1
GAME_CHART_TYPE_HYPER_POSITION = 2
GAME_CHART_TYPE_EX_POSITION = 3
# Medal type, as returned from the game
GAME_PLAY_MEDAL_CIRCLE_FAILED = 1
GAME_PLAY_MEDAL_DIAMOND_FAILED = 2
GAME_PLAY_MEDAL_STAR_FAILED = 3
GAME_PLAY_MEDAL_CIRCLE_CLEARED = 5
GAME_PLAY_MEDAL_DIAMOND_CLEARED = 6
GAME_PLAY_MEDAL_STAR_CLEARED = 7
GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO = 9
GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO = 10
GAME_PLAY_MEDAL_STAR_FULL_COMBO = 11
GAME_PLAY_MEDAL_PERFECT = 15
# Maximum music ID for this game
GAME_MAX_MUSIC_ID = 1150
def previous_version(self) -> Optional[PopnMusicBase]:
return PopnMusicTuneStreet(self.data, self.config, self.model)
def __format_medal_for_score(self, score: Score) -> int:
medal = {
self.PLAY_MEDAL_CIRCLE_FAILED: self.GAME_PLAY_MEDAL_CIRCLE_FAILED,
self.PLAY_MEDAL_DIAMOND_FAILED: self.GAME_PLAY_MEDAL_DIAMOND_FAILED,
self.PLAY_MEDAL_STAR_FAILED: self.GAME_PLAY_MEDAL_STAR_FAILED,
self.PLAY_MEDAL_EASY_CLEAR: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED, # Map approximately
self.PLAY_MEDAL_CIRCLE_CLEARED: self.GAME_PLAY_MEDAL_CIRCLE_CLEARED,
self.PLAY_MEDAL_DIAMOND_CLEARED: self.GAME_PLAY_MEDAL_DIAMOND_CLEARED,
self.PLAY_MEDAL_STAR_CLEARED: self.GAME_PLAY_MEDAL_STAR_CLEARED,
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO,
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO,
self.PLAY_MEDAL_STAR_FULL_COMBO: self.GAME_PLAY_MEDAL_STAR_FULL_COMBO,
self.PLAY_MEDAL_PERFECT: self.GAME_PLAY_MEDAL_PERFECT,
}[score.data.get_int('medal')]
position = {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
return medal << (position * 4)
def format_profile(self, userid: UserID, profile: ValidatedDict) -> Node:
root = Node.void('playerdata')
# Set up the base profile
base = Node.void('base')
root.add_child(base)
base.add_child(Node.string('name', profile.get_str('name', 'なし')))
base.add_child(Node.string('g_pm_id', ID.format_extid(profile.get_int('extid'))))
base.add_child(Node.u8('mode', profile.get_int('mode', 0)))
base.add_child(Node.s8('button', profile.get_int('button', 0)))
base.add_child(Node.s8('last_play_flag', profile.get_int('last_play_flag', -1)))
base.add_child(Node.u8('medal_and_friend', profile.get_int('medal_and_friend', 0)))
base.add_child(Node.s8('category', profile.get_int('category', -1)))
base.add_child(Node.s8('sub_category', profile.get_int('sub_category', -1)))
base.add_child(Node.s16('chara', profile.get_int('chara', -1)))
base.add_child(Node.s8('chara_category', profile.get_int('chara_category', -1)))
base.add_child(Node.u8('collabo', profile.get_int('collabo', 255)))
base.add_child(Node.u8('sheet', profile.get_int('sheet', 0)))
base.add_child(Node.s8('tutorial', profile.get_int('tutorial', 0)))
base.add_child(Node.s32('music_open_pt', profile.get_int('music_open_pt', 0)))
base.add_child(Node.s8('is_conv', -1))
base.add_child(Node.s32('option', profile.get_int('option', 0)))
base.add_child(Node.s16('music', profile.get_int('music', -1)))
base.add_child(Node.u16('ep', profile.get_int('ep', 0)))
base.add_child(Node.s32_array('sp_color_flg', profile.get_int_array('sp_color_flg', 2)))
base.add_child(Node.s32('read_news', profile.get_int('read_news', 0)))
base.add_child(Node.s16('consecutive_days_coupon', profile.get_int('consecutive_days_coupon', 0)))
base.add_child(Node.s8('staff', 0))
# Player card section
player_card_dict = profile.get_dict('player_card')
player_card = Node.void('player_card')
root.add_child(player_card)
player_card.add_child(Node.u8_array('title', player_card_dict.get_int_array('title', 2, [0, 1])))
player_card.add_child(Node.u8('frame', player_card_dict.get_int('frame')))
player_card.add_child(Node.u8('base', player_card_dict.get_int('base')))
player_card.add_child(Node.u8_array('seal', player_card_dict.get_int_array('seal', 2)))
player_card.add_child(Node.s32_array('get_title', player_card_dict.get_int_array('get_title', 4)))
player_card.add_child(Node.s32('get_frame', player_card_dict.get_int('get_frame')))
player_card.add_child(Node.s32('get_base', player_card_dict.get_int('get_base')))
player_card.add_child(Node.s32_array('get_seal', player_card_dict.get_int_array('get_seal', 2)))
# Player card EX section
player_card_ex = Node.void('player_card_ex')
root.add_child(player_card_ex)
player_card_ex.add_child(Node.s32('get_title_ex', player_card_dict.get_int('get_title_ex')))
player_card_ex.add_child(Node.s32('get_frame_ex', player_card_dict.get_int('get_frame_ex')))
player_card_ex.add_child(Node.s32('get_base_ex', player_card_dict.get_int('get_base_ex')))
player_card_ex.add_child(Node.s32('get_seal_ex', player_card_dict.get_int('get_seal_ex')))
# Statistics section and scores section
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
base.add_child(Node.u8('active_fr_num', 0)) # TODO: Hook up rivals code?
base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0)))
base.add_child(Node.s16('today_play_cnt', today_count))
base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 3)]
most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 20)]
while len(last_played) < 3:
last_played.append(-1)
while len(most_played) < 20:
most_played.append(-1)
hiscore_array = [0] * int((((self.GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)
clear_medal = [0] * self.GAME_MAX_MUSIC_ID
clear_medal_sub = [0] * self.GAME_MAX_MUSIC_ID
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 not in [
self.CHART_TYPE_EASY,
self.CHART_TYPE_NORMAL,
self.CHART_TYPE_HYPER,
self.CHART_TYPE_EX,
]:
continue
points = score.points
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
hiscore_index = (score.id * 4) + {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY_POSITION,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL_POSITION,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER_POSITION,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX_POSITION,
}[score.chart]
hiscore_byte_pos = int((hiscore_index * 17) / 8)
hiscore_bit_pos = int((hiscore_index * 17) % 8)
hiscore_value = 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)
hiscore = bytes(hiscore_array)
player_card.add_child(Node.s16_array('best_music', most_played[0:3]))
base.add_child(Node.s16_array('my_best', most_played))
base.add_child(Node.s16_array('latest_music', last_played))
base.add_child(Node.u16_array('clear_medal', clear_medal))
base.add_child(Node.u8_array('clear_medal_sub', clear_medal_sub))
# Goes outside of base for some reason
root.add_child(Node.binary('hiscore', hiscore))
# Net VS section
netvs = Node.void('netvs')
root.add_child(netvs)
netvs.add_child(Node.s32_array('get_ojama', [0, 0]))
netvs.add_child(Node.s32('rank_point', 0))
netvs.add_child(Node.s32('play_point', 0))
netvs.add_child(Node.s16_array('record', [0, 0, 0, 0, 0, 0]))
netvs.add_child(Node.u8('rank', 0))
netvs.add_child(Node.s8_array('ojama_condition', [0] * 74))
netvs.add_child(Node.s8_array('set_ojama', [0, 0, 0]))
netvs.add_child(Node.s8_array('set_recommend', [0, 0, 0]))
netvs.add_child(Node.s8_array('jewelry', [0] * 15))
for dialog in [0, 1, 2, 3, 4, 5]:
# TODO: Configure this, maybe?
netvs.add_child(Node.string('dialog', 'dialog#{}'.format(dialog)))
sp_data = Node.void('sp_data')
root.add_child(sp_data)
sp_data.add_child(Node.s32('sp', profile.get_int('sp', 0)))
reflec_data = Node.void('reflec_data')
root.add_child(reflec_data)
reflec_data.add_child(Node.s8_array('reflec', profile.get_int_array('reflec', 2)))
# Navigate section
navigate_dict = profile.get_dict('navigate')
navigate = Node.void('navigate')
root.add_child(navigate)
navigate.add_child(Node.s8('genre', navigate_dict.get_int('genre')))
navigate.add_child(Node.s8('image', navigate_dict.get_int('image')))
navigate.add_child(Node.s8('level', navigate_dict.get_int('level')))
navigate.add_child(Node.s8('ojama', navigate_dict.get_int('ojama')))
navigate.add_child(Node.s16('limit_num', navigate_dict.get_int('limit_num')))
navigate.add_child(Node.s8('button', navigate_dict.get_int('button')))
navigate.add_child(Node.s8('life', navigate_dict.get_int('life')))
navigate.add_child(Node.s16('progress', navigate_dict.get_int('progress')))
return root
def format_conversion(self, userid: UserID, profile: ValidatedDict) -> Node:
root = Node.void('playerdata')
root.add_child(Node.string('name', profile.get_str('name', 'なし')))
root.add_child(Node.s16('chara', profile.get_int('chara', -1)))
root.add_child(Node.s32('option', profile.get_int('option', 0)))
root.add_child(Node.u8('version', 0))
root.add_child(Node.u8('kind', 0))
root.add_child(Node.u8('season', 0))
clear_medal = [0] * self.GAME_MAX_MUSIC_ID
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 not in [
self.CHART_TYPE_EASY,
self.CHART_TYPE_NORMAL,
self.CHART_TYPE_HYPER,
self.CHART_TYPE_EX,
]:
continue
clear_medal[score.id] = clear_medal[score.id] | self.__format_medal_for_score(score)
root.add_child(Node.u16_array('clear_medal', clear_medal))
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict:
# For some reason, Pop'n 20 sends us two profile saves, one with 'not done yet'
# so we only want to process the done yet node. The 'not gameover' save has
# jubeat collabo stuff set in it, but we don't use that so it doesn't matter.
if request.child_value('is_not_gameover') == 1:
return oldprofile
newprofile = copy.deepcopy(oldprofile)
newprofile.replace_int('option', request.child_value('option'))
newprofile.replace_int('chara', request.child_value('chara'))
newprofile.replace_int('mode', request.child_value('mode'))
newprofile.replace_int('button', request.child_value('button'))
newprofile.replace_int('music', request.child_value('music'))
newprofile.replace_int('sheet', request.child_value('sheet'))
newprofile.replace_int('last_play_flag', request.child_value('last_play_flag'))
newprofile.replace_int('category', request.child_value('category'))
newprofile.replace_int('sub_category', request.child_value('sub_category'))
newprofile.replace_int('chara_category', request.child_value('chara_category'))
newprofile.replace_int('medal_and_friend', request.child_value('medal_and_friend'))
newprofile.replace_int('ep', request.child_value('ep'))
newprofile.replace_int_array('sp_color_flg', 2, request.child_value('sp_color_flg'))
newprofile.replace_int('read_news', request.child_value('read_news'))
newprofile.replace_int('consecutive_days_coupon', request.child_value('consecutive_days_coupon'))
newprofile.replace_int('tutorial', request.child_value('tutorial'))
newprofile.replace_int('music_open_pt', request.child_value('music_open_pt'))
newprofile.replace_int('collabo', request.child_value('collabo'))
sp_node = request.child('sp_data')
if sp_node is not None:
newprofile.replace_int('sp', sp_node.child_value('sp'))
reflec_node = request.child('reflec_data')
if reflec_node is not None:
newprofile.replace_int_array('reflec', 2, reflec_node.child_value('reflec'))
# Keep track of play statistics
self.update_play_statistics(userid)
# Extract player card stuff
player_card_dict = newprofile.get_dict('player_card')
player_card_dict.replace_int_array('title', 2, request.child_value('title'))
player_card_dict.replace_int('frame', request.child_value('frame'))
player_card_dict.replace_int('base', request.child_value('base'))
player_card_dict.replace_int_array('seal', 2, request.child_value('seal'))
player_card_dict.replace_int_array('get_title', 4, request.child_value('get_title'))
player_card_dict.replace_int('get_frame', request.child_value('get_frame'))
player_card_dict.replace_int('get_base', request.child_value('get_base'))
player_card_dict.replace_int_array('get_seal', 2, request.child_value('get_seal'))
player_card_ex = request.child('player_card_ex')
if player_card_ex is not None:
player_card_dict.replace_int('get_title_ex', player_card_ex.child_value('get_title_ex'))
player_card_dict.replace_int('get_frame_ex', player_card_ex.child_value('get_frame_ex'))
player_card_dict.replace_int('get_base_ex', player_card_ex.child_value('get_base_ex'))
player_card_dict.replace_int('get_seal_ex', player_card_ex.child_value('get_seal_ex'))
newprofile.replace_dict('player_card', player_card_dict)
# Extract navigate stuff
navigate_dict = newprofile.get_dict('navigate')
navigate = request.child('navigate')
if navigate is not None:
navigate_dict.replace_int('genre', navigate.child_value('genre'))
navigate_dict.replace_int('image', navigate.child_value('image'))
navigate_dict.replace_int('level', navigate.child_value('level'))
navigate_dict.replace_int('ojama', navigate.child_value('ojama'))
navigate_dict.replace_int('limit_num', navigate.child_value('limit_num'))
navigate_dict.replace_int('button', navigate.child_value('button'))
navigate_dict.replace_int('life', navigate.child_value('life'))
navigate_dict.replace_int('progress', navigate.child_value('progress'))
newprofile.replace_dict('navigate', navigate_dict)
# Extract scores
for node in request.children:
if node.name == 'stage':
songid = node.child_value('no')
chart = {
self.GAME_CHART_TYPE_EASY: self.CHART_TYPE_EASY,
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,
}[node.child_value('sheet')]
medal = (node.child_value('n_data') >> (chart * 4)) & 0x000F
medal = {
self.GAME_PLAY_MEDAL_CIRCLE_FAILED: self.PLAY_MEDAL_CIRCLE_FAILED,
self.GAME_PLAY_MEDAL_DIAMOND_FAILED: self.PLAY_MEDAL_DIAMOND_FAILED,
self.GAME_PLAY_MEDAL_STAR_FAILED: self.PLAY_MEDAL_STAR_FAILED,
self.GAME_PLAY_MEDAL_CIRCLE_CLEARED: self.PLAY_MEDAL_CIRCLE_CLEARED,
self.GAME_PLAY_MEDAL_DIAMOND_CLEARED: self.PLAY_MEDAL_DIAMOND_CLEARED,
self.GAME_PLAY_MEDAL_STAR_CLEARED: self.PLAY_MEDAL_STAR_CLEARED,
self.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO: self.PLAY_MEDAL_CIRCLE_FULL_COMBO,
self.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO: self.PLAY_MEDAL_DIAMOND_FULL_COMBO,
self.GAME_PLAY_MEDAL_STAR_FULL_COMBO: self.PLAY_MEDAL_STAR_FULL_COMBO,
self.GAME_PLAY_MEDAL_PERFECT: self.PLAY_MEDAL_PERFECT,
}[medal]
points = node.child_value('score')
self.update_score(userid, songid, chart, points, medal)
return newprofile
def handle_playerdata_request(self, request: Node) -> Optional[Node]:
method = request.attribute('method')
if method == 'expire':
return Node.void('playerdata')
elif method == 'logout':
return Node.void('playerdata')
elif method == 'get':
modelstring = request.attribute('model')
refid = request.child_value('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
elif method == 'conversion':
refid = request.child_value('ref_id')
name = request.child_value('name')
chara = request.child_value('chara')
root = self.new_profile_by_refid(refid, name, chara)
if root is None:
root = Node.void('playerdata')
root.set_attribute('status', str(Status.NO_PROFILE))
return root
elif method == 'new':
refid = request.child_value('ref_id')
name = request.child_value('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
elif method == 'set':
refid = request.attribute('ref_id')
root = Node.void('playerdata')
root.add_child(Node.s8('pref', -1))
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 ValidatedDict()
newprofile = self.unformat_profile(userid, request, oldprofile)
if newprofile is not None:
self.put_profile(userid, newprofile)
root.add_child(Node.string('name', newprofile['name']))
return root
# Invalid method
return None
def handle_game_request(self, request: Node) -> Optional[Node]:
method = request.attribute('method')
if method == 'get':
# TODO: Hook these up to config so we can change this
root = Node.void('game')
root.add_child(Node.s32('game_phase', 2))
root.add_child(Node.s32('ir_phase', 0))
root.add_child(Node.s32('event_phase', 5))
root.add_child(Node.s32('netvs_phase', 0))
root.add_child(Node.s32('card_phase', 6))
root.add_child(Node.s32('illust_phase', 2))
root.add_child(Node.s32('psp_phase', 5))
root.add_child(Node.s32('other_phase', 1))
root.add_child(Node.s32('jubeat_phase', 1))
root.add_child(Node.s32('public_phase', 3))
root.add_child(Node.s32('kac_phase', 2))
root.add_child(Node.s32('local_matching', 1))
root.add_child(Node.s32('n_matching_sec', 60))
root.add_child(Node.s32('l_matching_sec', 60))
root.add_child(Node.s32('is_check_cpu', 0))
root.add_child(Node.s32('week_no', 0))
root.add_child(Node.s32_array('ng_illust', [0] * 10))
root.add_child(Node.s16_array('sel_ranking', [-1] * 10))
root.add_child(Node.s16_array('up_ranking', [-1] * 10))
return root
if method == 'active':
# Update the name of this cab for admin purposes
self.update_machine_name(request.child_value('shop_name'))
return Node.void('game')
if method == 'taxphase':
return Node.void('game')
# Invalid method
return None