1
0
mirror of synced 2024-11-15 02:17:36 +01:00
bemaniutils/bemani/backend/popn/eclale.py

836 lines
37 KiB
Python
Raw Normal View History

# vim: set fileencoding=utf-8
import binascii
import copy
from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.lapistoria import PopnMusicLapistoria
from bemani.common import Time, ValidatedDict, VersionConstants
from bemani.data import UserID, Link
from bemani.protocol import Node
class PopnMusicEclale(PopnMusicBase):
name = "Pop'n Music éclale"
version = VersionConstants.POPN_MUSIC_ECLALE
# Chart type, as returned from the game
GAME_CHART_TYPE_EASY = 0
GAME_CHART_TYPE_NORMAL = 1
GAME_CHART_TYPE_HYPER = 2
GAME_CHART_TYPE_EX = 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_EASY_CLEAR = 4
GAME_PLAY_MEDAL_CIRCLE_CLEARED = 5
GAME_PLAY_MEDAL_DIAMOND_CLEARED = 6
GAME_PLAY_MEDAL_STAR_CLEARED = 7
GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO = 8
GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO = 9
GAME_PLAY_MEDAL_STAR_FULL_COMBO = 10
GAME_PLAY_MEDAL_PERFECT = 11
# Biggest ID in the music DB
GAME_MAX_MUSIC_ID = 1550
def previous_version(self) -> Optional[PopnMusicBase]:
return PopnMusicLapistoria(self.data, self.config, self.model)
def __construct_common_info(self, root: Node) -> None:
# Event phases
phases = {
# Unknown event (0-16)
0: 16,
# Unknown event (0-3)
1: 3,
# Unknown event (0-1)
2: 1,
# Unknown event (0-2)
3: 2,
# Unknown event (0-1)
4: 1,
# Unknown event (0-2)
5: 2,
# Unknown event (0-1)
6: 1,
# Unknown event (0-4)
7: 4,
# Unknown event (0-3)
8: 3,
# Unknown event (0-4)
9: 4,
# Unknown event (0-4)
10: 4,
# Unknown event (0-1)
11: 1,
# Possibly global event matching related? (0-1)
12: 1,
# Unknown event (0-4)
13: 4,
}
for phaseid in phases:
phase = Node.void('phase')
root.add_child(phase)
phase.add_child(Node.s16('event_id', phaseid))
phase.add_child(Node.s16('phase', phases[phaseid]))
for areaid in range(1, 50):
area = Node.void('area')
root.add_child(area)
area.add_child(Node.s16('area_id', areaid))
area.add_child(Node.u64('end_date', 0))
area.add_child(Node.s16('medal_id', areaid))
area.add_child(Node.bool('is_limit', False))
# Calculate most popular characters
profiles = self.data.remote.user.get_all_profiles(self.game, self.version)
charas: Dict[int, int] = {}
for (_userid, profile) in profiles:
chara = profile.get_int('chara', -1)
if chara <= 0:
continue
if chara not in charas:
charas[chara] = 1
else:
charas[chara] = charas[chara] + 1
# Order a typle by most popular character to least popular character
charamap = sorted(
[(c, charas[c]) for c in charas],
key=lambda c: c[1],
reverse=True,
)
# Output the top 20 of them
rank = 1
for (charaid, _usecount) in charamap[:20]:
popular = Node.void('popular')
root.add_child(popular)
popular.add_child(Node.s16('rank', rank))
popular.add_child(Node.s16('chara_num', charaid))
rank = rank + 1
# Output the hit chart
for (songid, _plays) in self.data.local.music.get_hit_chart(self.game, self.version, 500):
popular_music = Node.void('popular_music')
root.add_child(popular_music)
popular_music.add_child(Node.s16('music_num', songid))
# Output goods prices
for goodsid in range(1, 421):
if goodsid >= 1 and goodsid <= 80:
price = 60
elif goodsid >= 81 and goodsid <= 120:
price = 250
elif goodsid >= 121 and goodsid <= 142:
price = 500
elif goodsid >= 143 and goodsid <= 300:
price = 100
elif goodsid >= 301 and goodsid <= 420:
price = 150
else:
raise Exception('Invalid goods ID!')
goods = Node.void('goods')
root.add_child(goods)
goods.add_child(Node.s16('goods_id', goodsid))
goods.add_child(Node.s32('price', price))
goods.add_child(Node.s16('goods_type', 0))
def handle_pcb23_boot_request(self, request: Node) -> Node:
return Node.void('pcb23')
def handle_pcb23_error_request(self, request: Node) -> Node:
return Node.void('pcb23')
def handle_pcb23_dlstatus_request(self, request: Node) -> Node:
return Node.void('pcb23')
def handle_pcb23_write_request(self, request: Node) -> Node:
# Update the name of this cab for admin purposes
self.update_machine_name(request.child_value('pcb_setting/name'))
return Node.void('pcb23')
def handle_info23_common_request(self, request: Node) -> Node:
info = Node.void('info23')
self.__construct_common_info(info)
return info
def handle_lobby22_request(self, request: Node) -> Optional[Node]:
# Stub out the entire lobby22 service (yes, its lobby22 in Pop'n 23)
return Node.void('lobby22')
def handle_player23_start_request(self, request: Node) -> Node:
root = Node.void('player23')
root.add_child(Node.s32('play_id', 0))
self.__construct_common_info(root)
return root
def handle_player23_logout_request(self, request: Node) -> Node:
return Node.void('player23')
def handle_player23_read_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
root = self.get_profile_by_refid(refid, self.OLD_PROFILE_FALLTHROUGH)
if root is None:
root = Node.void('player23')
root.add_child(Node.s8('result', 2))
return root
def handle_player23_write_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
if refid is not None:
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
else:
userid = None
if userid is not None:
oldprofile = self.get_profile(userid) or ValidatedDict()
newprofile = self.unformat_profile(userid, request, oldprofile)
if newprofile is not None:
self.put_profile(userid, newprofile)
return Node.void('player23')
def handle_player23_new_request(self, request: Node) -> Node:
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('player23')
root.add_child(Node.s8('result', 2))
return root
def handle_player23_conversion_request(self, request: Node) -> Node:
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('player23')
root.add_child(Node.s8('result', 2))
return root
def handle_player23_buy_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
if refid is not None:
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
else:
userid = None
if userid is not None:
itemid = request.child_value('id')
itemtype = request.child_value('type')
itemparam = request.child_value('param')
price = request.child_value('price')
lumina = request.child_value('lumina')
if lumina >= price:
# Update player lumina balance
profile = self.get_profile(userid) or ValidatedDict()
profile.replace_int('lumina', lumina - price)
self.put_profile(userid, profile)
# Grant the object
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
itemid,
f'item_{itemtype}',
{
'param': itemparam,
'is_new': True,
},
)
return Node.void('player23')
def handle_player23_read_score_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
root = Node.void('player23')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
else:
scores = []
for score in scores:
# 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
medal = score.data.get_int('medal')
music = Node.void('music')
root.add_child(music)
music.add_child(Node.s16('music_num', score.id))
music.add_child(Node.u8('sheet_num', {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
}[score.chart]))
music.add_child(Node.s32('score', points))
music.add_child(Node.u8('clear_type', {
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_EASY_CLEAR,
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,
}[medal]))
music.add_child(Node.s16('cnt', score.plays))
return root
def handle_player23_friend_request(self, request: Node) -> Node:
refid = request.attribute('ref_id')
no = int(request.attribute('no', '-1'))
root = Node.void('player23')
if no < 0:
root.add_child(Node.s8('result', 2))
return root
# Look up our own user ID based on the RefID provided.
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
root.add_child(Node.s8('result', 2))
return root
# Grab the links that we care about.
links = self.data.local.user.get_links(self.game, self.version, userid)
profiles: Dict[UserID, ValidatedDict] = {}
rivals: List[Link] = []
for link in links:
if link.type != 'rival':
continue
other_profile = self.get_profile(link.other_userid)
if other_profile is None:
continue
profiles[link.other_userid] = other_profile
rivals.append(link)
# Somehow requested an invalid profile.
if no >= len(rivals):
root.add_child(Node.s8('result', 2))
return root
rivalid = links[no].other_userid
rivalprofile = profiles[rivalid]
scores = self.data.remote.music.get_scores(self.game, self.version, rivalid)
# First, output general profile info.
friend = Node.void('friend')
root.add_child(friend)
friend.add_child(Node.s16('no', no))
friend.add_child(Node.string('g_pm_id', self.format_extid(rivalprofile.get_int('extid')))) # Eclale formats on its own
friend.add_child(Node.string('name', rivalprofile.get_str('name', 'なし')))
friend.add_child(Node.s16('chara', rivalprofile.get_int('chara', -1)))
# This might be for having non-active or non-confirmed friends, but setting to 0 makes the
# ranking numbers disappear and the player icon show a questionmark.
friend.add_child(Node.s8('is_open', 1))
for score in scores:
# 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
medal = score.data.get_int('medal')
music = Node.void('music')
friend.add_child(music)
music.set_attribute('music_num', str(score.id))
music.set_attribute('sheet_num', str({
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
}[score.chart]))
music.set_attribute('score', str(points))
music.set_attribute('clearmedal', str({
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_EASY_CLEAR,
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,
}[medal]))
return root
def handle_player23_write_music_request(self, request: Node) -> Node:
refid = request.child_value('ref_id')
root = Node.void('player23')
if refid is None:
return root
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
return root
songid = request.child_value('music_num')
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,
}[request.child_value('sheet_num')]
medal = request.child_value('clearmedal')
points = request.child_value('score')
combo = request.child_value('combo')
stats = {
'cool': request.child_value('cool'),
'great': request.child_value('great'),
'good': request.child_value('good'),
'bad': request.child_value('bad')
}
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_EASY_CLEAR: self.PLAY_MEDAL_EASY_CLEAR,
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]
self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats)
return root
def format_conversion(self, userid: UserID, profile: ValidatedDict) -> Node:
root = Node.void('player23')
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.s8('result', 1))
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
for score in scores:
# 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
medal = score.data.get_int('medal')
music = Node.void('music')
root.add_child(music)
music.add_child(Node.s16('music_num', score.id))
music.add_child(Node.u8('sheet_num', {
self.CHART_TYPE_EASY: self.GAME_CHART_TYPE_EASY,
self.CHART_TYPE_NORMAL: self.GAME_CHART_TYPE_NORMAL,
self.CHART_TYPE_HYPER: self.GAME_CHART_TYPE_HYPER,
self.CHART_TYPE_EX: self.GAME_CHART_TYPE_EX,
}[score.chart]))
music.add_child(Node.s32('score', points))
music.add_child(Node.u8('clear_type', {
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_EASY_CLEAR,
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,
}[medal]))
music.add_child(Node.s16('cnt', score.plays))
return root
def format_extid(self, extid: int) -> str:
data = str(extid)
crc = abs(binascii.crc32(data.encode('ascii'))) % 10000
return f'{data}{crc:04d}'
def format_profile(self, userid: UserID, profile: ValidatedDict) -> Node:
root = Node.void('player23')
# Mark this as a current profile
root.add_child(Node.s8('result', 0))
# Account stuff
account = Node.void('account')
root.add_child(account)
account.add_child(Node.string('g_pm_id', self.format_extid(profile.get_int('extid')))) # Eclale formats on its own
account.add_child(Node.string('name', profile.get_str('name', 'なし')))
account.add_child(Node.s8('tutorial', profile.get_int('tutorial')))
account.add_child(Node.s16('area_id', profile.get_int('area_id')))
account.add_child(Node.s16('lumina', profile.get_int('lumina', 300)))
account.add_child(Node.s16('read_news', profile.get_int('read_news')))
account.add_child(Node.bool('welcom_pack', profile.get_bool('welcom_pack')))
account.add_child(Node.s16_array('medal_set', profile.get_int_array('medal_set', 4)))
account.add_child(Node.s16_array('nice', profile.get_int_array('nice', 30, [-1] * 30)))
account.add_child(Node.s16_array('favorite_chara', profile.get_int_array('favorite_chara', 20, [-1] * 20)))
account.add_child(Node.s16_array('special_area', profile.get_int_array('special_area', 8)))
account.add_child(Node.s16_array('chocolate_charalist', profile.get_int_array('chocolate_charalist', 5, [-1] * 5)))
account.add_child(Node.s16_array('teacher_setting', profile.get_int_array('teacher_setting', 10)))
# Stuff we never change
account.add_child(Node.s8('staff', 0))
account.add_child(Node.s16('item_type', 0))
account.add_child(Node.s16('item_id', 0))
account.add_child(Node.s8('is_conv', 0))
account.add_child(Node.bool('meteor_flg', True))
account.add_child(Node.s16_array('license_data', [-1] * 20))
# Add statistics section
last_played = [x[0] for x in self.data.local.music.get_last_played(self.game, self.version, userid, 5)]
most_played = [x[0] for x in self.data.local.music.get_most_played(self.game, self.version, userid, 10)]
while len(last_played) < 5:
last_played.append(-1)
while len(most_played) < 10:
most_played.append(-1)
account.add_child(Node.s16_array('my_best', most_played))
account.add_child(Node.s16_array('latest_music', last_played))
# Number of rivals that are active for this version.
links = self.data.local.user.get_links(self.game, self.version, userid)
rivalcount = 0
for link in links:
if link.type != 'rival':
continue
if not self.has_profile(link.other_userid):
continue
# This profile is valid.
rivalcount += 1
account.add_child(Node.u8('active_fr_num', rivalcount))
# player statistics
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
account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0)))
account.add_child(Node.s16('today_play_cnt', today_count))
account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
account.add_child(Node.s16('interval_day', 0))
# Set up info node
info = Node.void('info')
root.add_child(info)
info.add_child(Node.u16('ep', profile.get_int('ep')))
# Set up last information
config = Node.void('config')
root.add_child(config)
config.add_child(Node.u8('mode', profile.get_int('mode')))
config.add_child(Node.s16('chara', profile.get_int('chara', -1)))
config.add_child(Node.s16('music', profile.get_int('music', -1)))
config.add_child(Node.u8('sheet', profile.get_int('sheet')))
config.add_child(Node.s8('category', profile.get_int('category', -1)))
config.add_child(Node.s8('sub_category', profile.get_int('sub_category', -1)))
config.add_child(Node.s8('chara_category', profile.get_int('chara_category', -1)))
config.add_child(Node.s16('course_id', profile.get_int('course_id', -1)))
config.add_child(Node.s8('course_folder', profile.get_int('course_folder', -1)))
config.add_child(Node.s8('ms_banner_disp', profile.get_int('ms_banner_disp')))
config.add_child(Node.s8('ms_down_info', profile.get_int('ms_down_info')))
config.add_child(Node.s8('ms_side_info', profile.get_int('ms_side_info')))
config.add_child(Node.s8('ms_raise_type', profile.get_int('ms_raise_type')))
config.add_child(Node.s8('ms_rnd_type', profile.get_int('ms_rnd_type')))
# Player options
option = Node.void('option')
option_dict = profile.get_dict('option')
root.add_child(option)
option.add_child(Node.s16('hispeed', option_dict.get_int('hispeed')))
option.add_child(Node.u8('popkun', option_dict.get_int('popkun')))
option.add_child(Node.bool('hidden', option_dict.get_bool('hidden')))
option.add_child(Node.s16('hidden_rate', option_dict.get_int('hidden_rate')))
option.add_child(Node.bool('sudden', option_dict.get_bool('sudden')))
option.add_child(Node.s16('sudden_rate', option_dict.get_int('sudden_rate')))
option.add_child(Node.s8('randmir', option_dict.get_int('randmir')))
option.add_child(Node.s8('gauge_type', option_dict.get_int('gauge_type')))
option.add_child(Node.u8('ojama_0', option_dict.get_int('ojama_0')))
option.add_child(Node.u8('ojama_1', option_dict.get_int('ojama_1')))
option.add_child(Node.bool('forever_0', option_dict.get_bool('forever_0')))
option.add_child(Node.bool('forever_1', option_dict.get_bool('forever_1')))
option.add_child(Node.bool('full_setting', option_dict.get_bool('full_setting')))
option.add_child(Node.u8('judge', option_dict.get_int('judge')))
# Unknown custom category stuff?
custom_cate = Node.void('custom_cate')
root.add_child(custom_cate)
custom_cate.add_child(Node.s8('valid', 0))
custom_cate.add_child(Node.s8('lv_min', -1))
custom_cate.add_child(Node.s8('lv_max', -1))
custom_cate.add_child(Node.s8('medal_min', -1))
custom_cate.add_child(Node.s8('medal_max', -1))
custom_cate.add_child(Node.s8('friend_no', -1))
custom_cate.add_child(Node.s8('score_flg', -1))
# Set up achievements
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
for achievement in achievements:
if achievement.type[:5] == 'item_':
itemtype = int(achievement.type[5:])
param = achievement.data.get_int('param')
is_new = achievement.data.get_bool('is_new')
item = Node.void('item')
root.add_child(item)
# Type is the type of unlock/item. Type 0 is song unlock in Eclale.
# In this case, the id is the song ID according to the game. Unclear
# what the param is supposed to be, but i've seen 8 and 0. Might be
# what chart is available?
item.add_child(Node.u8('type', itemtype))
item.add_child(Node.u16('id', achievement.id))
item.add_child(Node.u16('param', param))
item.add_child(Node.bool('is_new', is_new))
elif achievement.type == 'chara':
friendship = achievement.data.get_int('friendship')
chara = Node.void('chara_param')
root.add_child(chara)
chara.add_child(Node.u16('chara_id', achievement.id))
chara.add_child(Node.u16('friendship', friendship))
elif achievement.type == 'medal':
level = achievement.data.get_int('level')
exp = achievement.data.get_int('exp')
set_count = achievement.data.get_int('set_count')
get_count = achievement.data.get_int('get_count')
medal = Node.void('medal')
root.add_child(medal)
medal.add_child(Node.s16('medal_id', achievement.id))
medal.add_child(Node.s16('level', level))
medal.add_child(Node.s32('exp', exp))
medal.add_child(Node.s32('set_count', set_count))
medal.add_child(Node.s32('get_count', get_count))
# Unknown customizations
customize = Node.void('customize')
root.add_child(customize)
customize.add_child(Node.u16('effect_left', profile.get_int('effect_left')))
customize.add_child(Node.u16('effect_center', profile.get_int('effect_center')))
customize.add_child(Node.u16('effect_right', profile.get_int('effect_right')))
customize.add_child(Node.u16('hukidashi', profile.get_int('hukidashi')))
customize.add_child(Node.u16('comment_1', profile.get_int('comment_1')))
customize.add_child(Node.u16('comment_2', profile.get_int('comment_2')))
# NetVS section
netvs = Node.void('netvs')
root.add_child(netvs)
netvs.add_child(Node.s16_array('record', [0] * 6))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.string('dialog', ''))
netvs.add_child(Node.s8_array('ojama_condition', [0] * 74))
netvs.add_child(Node.s8_array('set_ojama', [0] * 3))
netvs.add_child(Node.s8_array('set_recommend', [0] * 3))
netvs.add_child(Node.u32('netvs_play_cnt', 0))
# Event stuff
event = Node.void('event')
root.add_child(event)
event.add_child(Node.s16('enemy_medal', profile.get_int('event_enemy_medal')))
event.add_child(Node.s16('hp', profile.get_int('event_hp')))
# Stamp stuff
stamp = Node.void('stamp')
root.add_child(stamp)
stamp.add_child(Node.s16('stamp_id', profile.get_int('stamp_id')))
stamp.add_child(Node.s16('cnt', profile.get_int('stamp_cnt')))
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: ValidatedDict) -> ValidatedDict:
newprofile = copy.deepcopy(oldprofile)
# Set that we've seen this profile
newprofile.replace_bool('welcom_pack', True)
account = request.child('account')
if account is not None:
newprofile.replace_int('tutorial', account.child_value('tutorial'))
newprofile.replace_int('read_news', account.child_value('read_news'))
newprofile.replace_int('area_id', account.child_value('area_id'))
newprofile.replace_int('lumina', account.child_value('lumina'))
newprofile.replace_int_array('medal_set', 4, account.child_value('medal_set'))
newprofile.replace_int_array('nice', 30, account.child_value('nice'))
newprofile.replace_int_array('favorite_chara', 20, account.child_value('favorite_chara'))
newprofile.replace_int_array('special_area', 8, account.child_value('special_area'))
newprofile.replace_int_array('chocolate_charalist', 5, account.child_value('chocolate_charalist'))
newprofile.replace_int_array('teacher_setting', 10, account.child_value('teacher_setting'))
info = request.child('info')
if info is not None:
newprofile.replace_int('ep', info.child_value('ep'))
config = request.child('config')
if config is not None:
newprofile.replace_int('mode', config.child_value('mode'))
newprofile.replace_int('chara', config.child_value('chara'))
newprofile.replace_int('music', config.child_value('music'))
newprofile.replace_int('sheet', config.child_value('sheet'))
newprofile.replace_int('category', config.child_value('category'))
newprofile.replace_int('sub_category', config.child_value('sub_category'))
newprofile.replace_int('chara_category', config.child_value('chara_category'))
newprofile.replace_int('course_id', config.child_value('course_id'))
newprofile.replace_int('course_folder', config.child_value('course_folder'))
newprofile.replace_int('ms_banner_disp', config.child_value('ms_banner_disp'))
newprofile.replace_int('ms_down_info', config.child_value('ms_down_info'))
newprofile.replace_int('ms_side_info', config.child_value('ms_side_info'))
newprofile.replace_int('ms_raise_type', config.child_value('ms_raise_type'))
newprofile.replace_int('ms_rnd_type', config.child_value('ms_rnd_type'))
option_dict = newprofile.get_dict('option')
option = request.child('option')
if option is not None:
option_dict.replace_int('hispeed', option.child_value('hispeed'))
option_dict.replace_int('popkun', option.child_value('popkun'))
option_dict.replace_bool('hidden', option.child_value('hidden'))
option_dict.replace_int('hidden_rate', option.child_value('hidden_rate'))
option_dict.replace_bool('sudden', option.child_value('sudden'))
option_dict.replace_int('sudden_rate', option.child_value('sudden_rate'))
option_dict.replace_int('randmir', option.child_value('randmir'))
option_dict.replace_int('gauge_type', option.child_value('gauge_type'))
option_dict.replace_int('ojama_0', option.child_value('ojama_0'))
option_dict.replace_int('ojama_1', option.child_value('ojama_1'))
option_dict.replace_bool('forever_0', option.child_value('forever_0'))
option_dict.replace_bool('forever_1', option.child_value('forever_1'))
option_dict.replace_bool('full_setting', option.child_value('full_setting'))
option_dict.replace_int('judge', option.child_value('judge'))
newprofile.replace_dict('option', option_dict)
customize = request.child('customize')
if customize is not None:
newprofile.replace_int('effect_left', customize.child_value('effect_left'))
newprofile.replace_int('effect_center', customize.child_value('effect_center'))
newprofile.replace_int('effect_right', customize.child_value('effect_right'))
newprofile.replace_int('hukidashi', customize.child_value('hukidashi'))
newprofile.replace_int('comment_1', customize.child_value('comment_1'))
newprofile.replace_int('comment_2', customize.child_value('comment_2'))
event = request.child('event')
if event is not None:
newprofile.replace_int('event_enemy_medal', event.child_value('enemy_medal'))
newprofile.replace_int('event_hp', event.child_value('hp'))
stamp = request.child('stamp')
if stamp is not None:
newprofile.replace_int('stamp_id', stamp.child_value('stamp_id'))
newprofile.replace_int('stamp_cnt', stamp.child_value('cnt'))
# Extract achievements
for node in request.children:
if node.name == 'item':
itemid = node.child_value('id')
itemtype = node.child_value('type')
param = node.child_value('param')
is_new = node.child_value('is_new')
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
itemid,
f'item_{itemtype}',
{
'param': param,
'is_new': is_new,
},
)
elif node.name == 'chara_param':
charaid = node.child_value('chara_id')
friendship = node.child_value('friendship')
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
charaid,
'chara',
{
'friendship': friendship,
},
)
elif node.name == 'medal':
medalid = node.child_value('medal_id')
level = node.child_value('level')
exp = node.child_value('exp')
set_count = node.child_value('set_count')
get_count = node.child_value('get_count')
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
medalid,
'medal',
{
'level': level,
'exp': exp,
'set_count': set_count,
'get_count': get_count,
},
)
# Keep track of play statistics
self.update_play_statistics(userid)
return newprofile