836 lines
37 KiB
Python
836 lines
37 KiB
Python
# 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
|