835 lines
36 KiB
Python
835 lines
36 KiB
Python
# vim: set fileencoding=utf-8
|
|
import copy
|
|
import random
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from bemani.backend.base import Status
|
|
from bemani.backend.jubeat.base import JubeatBase
|
|
from bemani.backend.jubeat.common import (
|
|
JubeatDemodataGetHitchartHandler,
|
|
JubeatDemodataGetNewsHandler,
|
|
JubeatGamendRegisterHandler,
|
|
JubeatGametopGetMeetingHandler,
|
|
JubeatLobbyCheckHandler,
|
|
JubeatLoggerReportHandler,
|
|
)
|
|
from bemani.backend.jubeat.course import JubeatCourse
|
|
from bemani.backend.jubeat.saucer import JubeatSaucer
|
|
from bemani.common import Profile, ValidatedDict, VersionConstants, Time
|
|
from bemani.data import Data, Score, UserID
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class JubeatSaucerFulfill(
|
|
JubeatDemodataGetHitchartHandler,
|
|
JubeatDemodataGetNewsHandler,
|
|
JubeatGamendRegisterHandler,
|
|
JubeatGametopGetMeetingHandler,
|
|
JubeatLobbyCheckHandler,
|
|
JubeatLoggerReportHandler,
|
|
JubeatCourse,
|
|
JubeatBase,
|
|
):
|
|
|
|
name = 'Jubeat Saucer Fulfill'
|
|
version = VersionConstants.JUBEAT_SAUCER_FULFILL
|
|
|
|
GAME_COURSE_REQUIREMENT_SCORE = 1
|
|
GAME_COURSE_REQUIREMENT_FULL_COMBO = 2
|
|
GAME_COURSE_REQUIREMENT_PERFECT_PERCENT = 3
|
|
|
|
GAME_COURSE_RATING_FAILED = 1
|
|
GAME_COURSE_RATING_BRONZE = 2
|
|
GAME_COURSE_RATING_SILVER = 3
|
|
GAME_COURSE_RATING_GOLD = 4
|
|
|
|
def previous_version(self) -> Optional[JubeatBase]:
|
|
return JubeatSaucer(self.data, self.config, self.model)
|
|
|
|
@classmethod
|
|
def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
|
|
"""
|
|
Insert daily FC challenges into the DB.
|
|
"""
|
|
events = []
|
|
if data.local.network.should_schedule(cls.game, cls.version, 'fc_challenge', 'daily'):
|
|
# Generate a new list of two FC challenge songs.
|
|
start_time, end_time = data.local.network.get_schedule_duration('daily')
|
|
all_songs = set(song.id for song in data.local.music.get_all_songs(cls.game, cls.version))
|
|
daily_songs = random.sample(all_songs, 2)
|
|
data.local.game.put_time_sensitive_settings(
|
|
cls.game,
|
|
cls.version,
|
|
'fc_challenge',
|
|
{
|
|
'start_time': start_time,
|
|
'end_time': end_time,
|
|
'today': daily_songs[0],
|
|
'whim': daily_songs[1],
|
|
},
|
|
)
|
|
events.append((
|
|
'jubeat_fc_challenge_charts',
|
|
{
|
|
'version': cls.version,
|
|
'today': daily_songs[0],
|
|
'whim': daily_songs[1],
|
|
},
|
|
))
|
|
|
|
# Mark that we did some actual work here.
|
|
data.local.network.mark_scheduled(cls.game, cls.version, 'fc_challenge', 'daily')
|
|
return events
|
|
|
|
def handle_shopinfo_regist_request(self, request: Node) -> Node:
|
|
# Update the name of this cab for admin purposes
|
|
self.update_machine_name(request.child_value('shop/name'))
|
|
|
|
shopinfo = Node.void('shopinfo')
|
|
|
|
data = Node.void('data')
|
|
shopinfo.add_child(data)
|
|
data.add_child(Node.u32('cabid', 1))
|
|
data.add_child(Node.string('locationid', 'nowhere'))
|
|
data.add_child(Node.u8('is_send', 1))
|
|
data.add_child(Node.s32_array(
|
|
'white_music_list',
|
|
[
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
-1, -1, -1, -1,
|
|
],
|
|
))
|
|
data.add_child(Node.u8('tax_phase', 1))
|
|
|
|
lab = Node.void('lab')
|
|
data.add_child(lab)
|
|
lab.add_child(Node.bool('is_open', False))
|
|
|
|
vocaloid_event = Node.void('vocaloid_event')
|
|
data.add_child(vocaloid_event)
|
|
vocaloid_event.add_child(Node.u8('state', 0))
|
|
vocaloid_event.add_child(Node.s32('music_id', 0))
|
|
|
|
vocaloid_event2 = Node.void('vocaloid_event2')
|
|
data.add_child(vocaloid_event2)
|
|
vocaloid_event2.add_child(Node.u8('state', 0))
|
|
vocaloid_event2.add_child(Node.s32('music_id', 0))
|
|
|
|
# No obnoxious 30 second wait to play.
|
|
matching_off = Node.void('matching_off')
|
|
data.add_child(matching_off)
|
|
matching_off.add_child(Node.bool('is_open', True))
|
|
|
|
tenka = Node.void('tenka')
|
|
data.add_child(tenka)
|
|
tenka.add_child(Node.bool('is_participant', False))
|
|
|
|
return shopinfo
|
|
|
|
def handle_gametop_get_course_request(self, request: Node) -> Node:
|
|
data = request.child('data')
|
|
player = data.child('player')
|
|
extid = player.child_value('jid')
|
|
|
|
gametop = Node.void('gametop')
|
|
data = Node.void('data')
|
|
gametop.add_child(data)
|
|
|
|
# Course list available
|
|
course_list = Node.void('course_list')
|
|
data.add_child(course_list)
|
|
|
|
validcourses: List[int] = []
|
|
for course in self.get_all_courses():
|
|
coursenode = Node.void('course')
|
|
course_list.add_child(coursenode)
|
|
|
|
# Basic course info
|
|
if course['id'] in validcourses:
|
|
raise Exception('Cannot have same course ID specified twice!')
|
|
validcourses.append(course['id'])
|
|
coursenode.add_child(Node.s32('id', course['id']))
|
|
coursenode.add_child(Node.string('name', course['name']))
|
|
coursenode.add_child(Node.u8('level', course['level']))
|
|
|
|
# Translate internal to game
|
|
def translate_req(internal_req: int) -> int:
|
|
return {
|
|
self.COURSE_REQUIREMENT_SCORE: self.GAME_COURSE_REQUIREMENT_SCORE,
|
|
self.COURSE_REQUIREMENT_FULL_COMBO: self.GAME_COURSE_REQUIREMENT_FULL_COMBO,
|
|
self.COURSE_REQUIREMENT_PERFECT_PERCENT: self.GAME_COURSE_REQUIREMENT_PERFECT_PERCENT,
|
|
}.get(internal_req, 0)
|
|
|
|
# Course bronze/silver/gold rules
|
|
ids = [0] * 3
|
|
bronze_values = [0] * 3
|
|
silver_values = [0] * 3
|
|
gold_values = [0] * 3
|
|
slot = 0
|
|
for req in course['requirements']:
|
|
req_values = course['requirements'][req]
|
|
|
|
ids[slot] = translate_req(req)
|
|
bronze_values[slot] = req_values[0]
|
|
silver_values[slot] = req_values[1]
|
|
gold_values[slot] = req_values[2]
|
|
slot = slot + 1
|
|
|
|
norma = Node.void('norma')
|
|
coursenode.add_child(norma)
|
|
norma.add_child(Node.s32_array('norma_id', ids))
|
|
norma.add_child(Node.s32_array('bronze_value', bronze_values))
|
|
norma.add_child(Node.s32_array('silver_value', silver_values))
|
|
norma.add_child(Node.s32_array('gold_value', gold_values))
|
|
|
|
# Music list for course
|
|
music_index = 0
|
|
music_list = Node.void('music_list')
|
|
coursenode.add_child(music_list)
|
|
|
|
for entry in course['music']:
|
|
music = Node.void('music')
|
|
music.set_attribute('index', str(music_index))
|
|
music_list.add_child(music)
|
|
music.add_child(Node.s32('music_id', entry[0]))
|
|
music.add_child(Node.u8('seq', entry[1]))
|
|
music_index = music_index + 1
|
|
|
|
# Look up profile so we can load the last course played
|
|
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
|
|
profile = self.get_profile(userid)
|
|
if profile is None:
|
|
profile = Profile(self.game, self.version, "", extid)
|
|
|
|
# Player scores for courses
|
|
player_list = Node.void('player_list')
|
|
data.add_child(player_list)
|
|
player = Node.void('player')
|
|
player_list.add_child(player)
|
|
player.add_child(Node.s32('jid', extid))
|
|
|
|
result_list = Node.void('result_list')
|
|
player.add_child(result_list)
|
|
playercourses = self.get_courses(userid)
|
|
for courseid in playercourses:
|
|
if courseid not in validcourses:
|
|
continue
|
|
|
|
rating = {
|
|
self.COURSE_RATING_FAILED: self.GAME_COURSE_RATING_FAILED,
|
|
self.COURSE_RATING_BRONZE: self.GAME_COURSE_RATING_BRONZE,
|
|
self.COURSE_RATING_SILVER: self.GAME_COURSE_RATING_SILVER,
|
|
self.COURSE_RATING_GOLD: self.GAME_COURSE_RATING_GOLD,
|
|
}[playercourses[courseid]['rating']]
|
|
scores = playercourses[courseid]['scores']
|
|
|
|
result = Node.void('result')
|
|
result_list.add_child(result)
|
|
result.add_child(Node.s32('id', courseid))
|
|
result.add_child(Node.u8('rating', rating))
|
|
result.add_child(Node.s32_array('score', scores))
|
|
|
|
# Last course ID
|
|
last_course_id = Node.s32('last_course_id', profile.get_dict('last').get_int('last_course_id', -1))
|
|
data.add_child(last_course_id)
|
|
|
|
return gametop
|
|
|
|
def handle_gametop_regist_request(self, request: Node) -> Node:
|
|
data = request.child('data')
|
|
player = data.child('player')
|
|
passnode = player.child('pass')
|
|
refid = passnode.child_value('refid')
|
|
name = player.child_value('name')
|
|
root = self.new_profile_by_refid(refid, name)
|
|
return root
|
|
|
|
def handle_gametop_get_pdata_request(self, request: Node) -> Node:
|
|
data = request.child('data')
|
|
player = data.child('player')
|
|
passnode = player.child('pass')
|
|
refid = passnode.child_value('refid')
|
|
root = self.get_profile_by_refid(refid)
|
|
if root is None:
|
|
root = Node.void('gametop')
|
|
root.set_attribute('status', str(Status.NO_PROFILE))
|
|
return root
|
|
|
|
def handle_gametop_get_mdata_request(self, request: Node) -> Node:
|
|
data = request.child('data')
|
|
player = data.child('player')
|
|
extid = player.child_value('jid')
|
|
mdata_ver = player.child_value('mdata_ver') # Game requests mdata 3 times per profile for some reason
|
|
if mdata_ver != 1:
|
|
root = Node.void('gametop')
|
|
datanode = Node.void('data')
|
|
root.add_child(datanode)
|
|
player = Node.void('player')
|
|
datanode.add_child(player)
|
|
player.add_child(Node.s32('jid', extid))
|
|
playdata = Node.void('playdata')
|
|
player.add_child(playdata)
|
|
playdata.set_attribute('count', '0')
|
|
return root
|
|
root = self.get_scores_by_extid(extid)
|
|
if root is None:
|
|
root = Node.void('gametop')
|
|
root.set_attribute('status', str(Status.NO_PROFILE))
|
|
return root
|
|
|
|
def handle_gametop_get_rival_mdata_request(self, request: Node) -> Node:
|
|
data = request.child('data')
|
|
player = data.child('player')
|
|
extid = player.child_value('rival')
|
|
root = self.get_scores_by_extid(extid)
|
|
if root is None:
|
|
root = Node.void('gametop')
|
|
root.set_attribute('status', str(Status.NO_PROFILE))
|
|
return root
|
|
|
|
def format_profile(self, userid: UserID, profile: Profile) -> Node:
|
|
root = Node.void('gametop')
|
|
data = Node.void('data')
|
|
root.add_child(data)
|
|
player = Node.void('player')
|
|
data.add_child(player)
|
|
|
|
# Player info and statistics
|
|
info = Node.void('info')
|
|
player.add_child(info)
|
|
info.add_child(Node.s16('jubility', profile.get_int('jubility')))
|
|
info.add_child(Node.s16('jubility_yday', profile.get_int('jubility_yday')))
|
|
info.add_child(Node.s32('tune_cnt', profile.get_int('tune_cnt')))
|
|
info.add_child(Node.s32('save_cnt', profile.get_int('save_cnt')))
|
|
info.add_child(Node.s32('saved_cnt', profile.get_int('saved_cnt')))
|
|
info.add_child(Node.s32('fc_cnt', profile.get_int('fc_cnt')))
|
|
info.add_child(Node.s32('ex_cnt', profile.get_int('ex_cnt')))
|
|
info.add_child(Node.s32('pf_cnt', profile.get_int('pf_cnt')))
|
|
info.add_child(Node.s32('clear_cnt', profile.get_int('clear_cnt')))
|
|
info.add_child(Node.s32('match_cnt', profile.get_int('match_cnt')))
|
|
info.add_child(Node.s32('beat_cnt', profile.get_int('beat_cnt')))
|
|
info.add_child(Node.s32('mynews_cnt', profile.get_int('mynews_cnt')))
|
|
info.add_child(Node.s32('extra_point', profile.get_int('extra_point')))
|
|
info.add_child(Node.bool('is_extra_played', profile.get_bool('is_extra_played')))
|
|
if 'total_best_score' in profile:
|
|
info.add_child(Node.s32('total_best_score', profile.get_int('total_best_score')))
|
|
|
|
# Looks to be set to true when there's an old profile, stops tutorial from
|
|
# happening on first load.
|
|
info.add_child(Node.bool('inherit', profile.get_bool('has_old_version')))
|
|
|
|
# Not saved, but loaded
|
|
info.add_child(Node.s32('mtg_entry_cnt', 123))
|
|
info.add_child(Node.s32('mtg_hold_cnt', 456))
|
|
info.add_child(Node.u8('mtg_result', 10))
|
|
|
|
# First play stuff we don't support
|
|
free_first_play = Node.void('free_first_play')
|
|
player.add_child(free_first_play)
|
|
free_first_play.add_child(Node.bool('is_available', False))
|
|
free_first_play.add_child(Node.s32('point', 0))
|
|
free_first_play.add_child(Node.s32('point_used', 0))
|
|
|
|
# Secret unlocks
|
|
item = Node.void('item')
|
|
player.add_child(item)
|
|
item.add_child(Node.s32_array(
|
|
'secret_list',
|
|
profile.get_int_array(
|
|
'secret_list',
|
|
32,
|
|
[-1] * 32,
|
|
),
|
|
))
|
|
item.add_child(Node.s32_array(
|
|
'title_list',
|
|
profile.get_int_array(
|
|
'title_list',
|
|
96,
|
|
[-1] * 96,
|
|
),
|
|
))
|
|
item.add_child(Node.s16('theme_list', profile.get_int('theme_list', -1)))
|
|
item.add_child(Node.s32_array('marker_list', profile.get_int_array('marker_list', 2, [-1] * 2)))
|
|
item.add_child(Node.s32_array('parts_list', profile.get_int_array('parts_list', 96, [-1] * 96)))
|
|
|
|
new = Node.void('new')
|
|
item.add_child(new)
|
|
new.add_child(Node.s32_array(
|
|
'secret_list',
|
|
profile.get_int_array(
|
|
'secret_list_new',
|
|
32,
|
|
[-1] * 32,
|
|
),
|
|
))
|
|
new.add_child(Node.s32_array(
|
|
'title_list',
|
|
profile.get_int_array(
|
|
'title_list_new',
|
|
96,
|
|
[-1] * 96,
|
|
),
|
|
))
|
|
new.add_child(Node.s16('theme_list', profile.get_int('theme_list_new', -1)))
|
|
new.add_child(Node.s32_array('marker_list', profile.get_int_array('marker_list_new', 2, [-1] * 2)))
|
|
|
|
# Last played data, for showing cursor and such
|
|
lastdict = profile.get_dict('last')
|
|
last = Node.void('last')
|
|
player.add_child(last)
|
|
last.add_child(Node.s32('music_id', lastdict.get_int('music_id')))
|
|
last.add_child(Node.s8('marker', lastdict.get_int('marker')))
|
|
last.add_child(Node.s16('title', lastdict.get_int('title')))
|
|
last.add_child(Node.s8('theme', lastdict.get_int('theme')))
|
|
last.add_child(Node.s8('sort', lastdict.get_int('sort')))
|
|
last.add_child(Node.s8('rank_sort', lastdict.get_int('rank_sort')))
|
|
last.add_child(Node.s8('combo_disp', lastdict.get_int('combo_disp')))
|
|
last.add_child(Node.s8('seq_id', lastdict.get_int('seq_id')))
|
|
last.add_child(Node.s16('parts', lastdict.get_int('parts')))
|
|
last.add_child(Node.s8('category', lastdict.get_int('category')))
|
|
last.add_child(Node.s64('play_time', lastdict.get_int('play_time')))
|
|
last.add_child(Node.string('shopname', lastdict.get_str('shopname')))
|
|
last.add_child(Node.string('areaname', lastdict.get_str('areaname')))
|
|
last.add_child(Node.s8('expert_option', lastdict.get_int('expert_option')))
|
|
last.add_child(Node.s8('matching', lastdict.get_int('matching')))
|
|
last.add_child(Node.s8('hazard', lastdict.get_int('hazard')))
|
|
last.add_child(Node.s8('hard', lastdict.get_int('hard')))
|
|
|
|
# Miscelaneous crap
|
|
player.add_child(Node.s32('session_id', 1))
|
|
player.add_child(Node.u64('event_flag', 0))
|
|
|
|
# Macchiato event
|
|
macchiatodict = profile.get_dict('macchiato')
|
|
macchiato = Node.void('macchiato')
|
|
player.add_child(macchiato)
|
|
macchiato.add_child(Node.s32('pack_id', macchiatodict.get_int('pack_id')))
|
|
macchiato.add_child(Node.u16('bean_num', macchiatodict.get_int('bean_num')))
|
|
macchiato.add_child(Node.s32('daily_milk_num', macchiatodict.get_int('daily_milk_num')))
|
|
macchiato.add_child(Node.bool('is_received_daily_milk', macchiatodict.get_bool('is_received_daily_milk')))
|
|
macchiato.add_child(Node.s32('today_tune_cnt', macchiatodict.get_int('today_tune_cnt')))
|
|
macchiato.add_child(Node.s32_array(
|
|
'daily_milk_bonus',
|
|
macchiatodict.get_int_array('daily_milk_bonus', 9, [-1, -1, -1, -1, -1, -1, -1, -1, -1]),
|
|
))
|
|
macchiato.add_child(Node.s32('daily_play_burst', macchiatodict.get_int('daily_play_burst')))
|
|
macchiato.add_child(Node.bool('sub_menu_is_completed', macchiatodict.get_bool('sub_menu_is_completed')))
|
|
macchiato.add_child(Node.s32('compensation_milk', macchiatodict.get_int('compensation_milk')))
|
|
macchiato.add_child(Node.s32('match_cnt', macchiatodict.get_int('match_cnt')))
|
|
|
|
# Probably never will support this
|
|
macchiato_music_list = Node.void('macchiato_music_list')
|
|
macchiato.add_child(macchiato_music_list)
|
|
macchiato_music_list.set_attribute('count', '0')
|
|
|
|
# Same with this comment
|
|
macchiato.add_child(Node.s32('sub_pack_id', 0))
|
|
sub_macchiato_music_list = Node.void('sub_macchiato_music_list')
|
|
macchiato.add_child(sub_macchiato_music_list)
|
|
sub_macchiato_music_list.set_attribute('count', '0')
|
|
|
|
# And this
|
|
season_music_list = Node.void('season_music_list')
|
|
macchiato.add_child(season_music_list)
|
|
season_music_list.set_attribute('count', '0')
|
|
|
|
# Weird, this is sent as a void with a bunch of subnodes, but returned as an int array.
|
|
achievement_list = Node.void('achievement_list')
|
|
macchiato.add_child(achievement_list)
|
|
achievement_list.set_attribute('count', '0')
|
|
|
|
# Also probably never supporting this either
|
|
cow_list = Node.void('cow_list')
|
|
macchiato.add_child(cow_list)
|
|
cow_list.set_attribute('count', '0')
|
|
|
|
# No news, ever.
|
|
news = Node.void('news')
|
|
player.add_child(news)
|
|
news.add_child(Node.s16('checked', 0))
|
|
|
|
# Add rivals to profile.
|
|
rivallist = Node.void('rivallist')
|
|
player.add_child(rivallist)
|
|
|
|
links = self.data.local.user.get_links(self.game, self.version, userid)
|
|
rivalcount = 0
|
|
for link in links:
|
|
if link.type != 'rival':
|
|
continue
|
|
|
|
rprofile = self.get_profile(link.other_userid)
|
|
if rprofile is None:
|
|
continue
|
|
|
|
rival = Node.void('rival')
|
|
rivallist.add_child(rival)
|
|
rival.add_child(Node.s32('jid', rprofile.extid))
|
|
rival.add_child(Node.string('name', rprofile.get_str('name')))
|
|
|
|
# Lazy way of keeping track of rivals, since we can only have 4
|
|
# or the game with throw up. At least, I think Fulfill can have
|
|
# 4 instead of the 3 found in newer versions, given the size of
|
|
# the array that it loads the values in. However, to keep things
|
|
# simple, I only supported three here.
|
|
rivalcount += 1
|
|
if rivalcount >= 3:
|
|
break
|
|
|
|
rivallist.set_attribute('count', str(rivalcount))
|
|
|
|
# Full combo daily challenge.
|
|
entry = self.data.local.game.get_time_sensitive_settings(self.game, self.version, 'fc_challenge')
|
|
if entry is None:
|
|
entry = ValidatedDict()
|
|
|
|
# Figure out if we've played these songs
|
|
start_time, end_time = self.data.local.network.get_schedule_duration('daily')
|
|
today_attempts = self.data.local.music.get_all_attempts(self.game, self.version, userid, entry.get_int('today', -1), timelimit=start_time)
|
|
whim_attempts = self.data.local.music.get_all_attempts(self.game, self.version, userid, entry.get_int('whim', -1), timelimit=start_time)
|
|
|
|
challenge = Node.void('challenge')
|
|
player.add_child(challenge)
|
|
today = Node.void('today')
|
|
challenge.add_child(today)
|
|
today.add_child(Node.s32('music_id', entry.get_int('today', -1)))
|
|
today.add_child(Node.u8('state', 0x40 if len(today_attempts) > 0 else 0x0))
|
|
whim = Node.void('whim')
|
|
challenge.add_child(whim)
|
|
whim.add_child(Node.s32('music_id', entry.get_int('whim', -1)))
|
|
whim.add_child(Node.u8('state', 0x40 if len(whim_attempts) > 0 else 0x0))
|
|
|
|
# Sane defaults for unknown nodes
|
|
only_now_music = Node.void('only_now_music')
|
|
player.add_child(only_now_music)
|
|
only_now_music.set_attribute('count', '0')
|
|
lab_edit_seq = Node.void('lab_edit_seq')
|
|
player.add_child(lab_edit_seq)
|
|
lab_edit_seq.set_attribute('count', '0')
|
|
kac_music = Node.void('kac_music')
|
|
player.add_child(kac_music)
|
|
kac_music.set_attribute('count', '0')
|
|
history = Node.void('history')
|
|
player.add_child(history)
|
|
history.set_attribute('count', '0')
|
|
share_music = Node.void('share_music')
|
|
player.add_child(share_music)
|
|
share_music.set_attribute('count', '0')
|
|
bonus_music = Node.void('bonus_music')
|
|
player.add_child(bonus_music)
|
|
bonus_music.set_attribute('count', '0')
|
|
|
|
bingo = Node.void('bingo')
|
|
player.add_child(bingo)
|
|
reward = Node.void('reward')
|
|
bingo.add_child(reward)
|
|
reward.add_child(Node.s32('total', 0))
|
|
reward.add_child(Node.s32('point', 0))
|
|
group = Node.void('group')
|
|
player.add_child(group)
|
|
group.add_child(Node.s32('group_id', 0))
|
|
|
|
# Basic profile info
|
|
player.add_child(Node.string('name', profile.get_str('name', 'なし')))
|
|
player.add_child(Node.s32('jid', profile.extid))
|
|
player.add_child(Node.string('refid', profile.refid))
|
|
|
|
# Miscelaneous history stuff
|
|
data.add_child(Node.u8('termver', 16))
|
|
data.add_child(Node.u32('season_etime', 0))
|
|
data.add_child(Node.s32_array(
|
|
'white_music_list',
|
|
[
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
],
|
|
))
|
|
data.add_child(Node.s32_array(
|
|
'open_music_list',
|
|
[
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
],
|
|
))
|
|
|
|
# Unsupported collaboration events with other games
|
|
collabo_info = Node.void('collabo_info')
|
|
data.add_child(collabo_info)
|
|
|
|
# Unsupported policy break stuff
|
|
policy_break = Node.void('policy_break')
|
|
collabo_info.add_child(policy_break)
|
|
policy_break.set_attribute('type', '1')
|
|
policy_break.add_child(Node.u8('state', 1))
|
|
policy_break.add_child(Node.bool('is_report_end', False))
|
|
|
|
# Unsupported vocaloid stuff
|
|
vocaloid_event = Node.void('vocaloid_event')
|
|
collabo_info.add_child(vocaloid_event)
|
|
vocaloid_event.set_attribute('type', '1')
|
|
vocaloid_event.add_child(Node.u8('state', 0))
|
|
vocaloid_event.add_child(Node.s32('music_id', 0))
|
|
|
|
# Unsupported vocaloid stuff
|
|
vocaloid_event2 = Node.void('vocaloid_event2')
|
|
collabo_info.add_child(vocaloid_event2)
|
|
vocaloid_event2.set_attribute('type', '1')
|
|
vocaloid_event2.add_child(Node.u8('state', 0))
|
|
vocaloid_event2.add_child(Node.s32('music_id', 0))
|
|
|
|
# Maybe it is possible to turn off internet matching here?
|
|
lab = Node.void('lab')
|
|
data.add_child(lab)
|
|
lab.add_child(Node.bool('is_open', False))
|
|
matching_off = Node.void('matching_off')
|
|
data.add_child(matching_off)
|
|
matching_off.add_child(Node.bool('is_open', True))
|
|
|
|
return root
|
|
|
|
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
|
|
newprofile = copy.deepcopy(oldprofile)
|
|
data = request.child('data')
|
|
|
|
# Grab player information
|
|
player = data.child('player')
|
|
|
|
# Grab last information. Lots of this will be filled in while grabbing scores
|
|
last = newprofile.get_dict('last')
|
|
last.replace_int('play_time', player.child_value('time_gameend'))
|
|
last.replace_str('shopname', player.child_value('shopname'))
|
|
last.replace_str('areaname', player.child_value('areaname'))
|
|
|
|
# Grab player info for echoing back
|
|
info = player.child('info')
|
|
if info is not None:
|
|
newprofile.replace_int('jubility', info.child_value('jubility'))
|
|
newprofile.replace_int('jubility_yday', info.child_value('jubility_yday'))
|
|
newprofile.replace_int('tune_cnt', info.child_value('tune_cnt'))
|
|
newprofile.replace_int('save_cnt', info.child_value('save_cnt'))
|
|
newprofile.replace_int('saved_cnt', info.child_value('saved_cnt'))
|
|
newprofile.replace_int('fc_cnt', info.child_value('fc_cnt'))
|
|
newprofile.replace_int('ex_cnt', info.child_value('exc_cnt')) # Not a mistake, Jubeat is weird
|
|
newprofile.replace_int('pf_cnt', info.child_value('pf_cnt'))
|
|
newprofile.replace_int('clear_cnt', info.child_value('clear_cnt'))
|
|
newprofile.replace_int('match_cnt', info.child_value('match_cnt'))
|
|
newprofile.replace_int('beat_cnt', info.child_value('beat_cnt'))
|
|
newprofile.replace_int('total_best_score', info.child_value('total_best_score'))
|
|
newprofile.replace_int('mynews_cnt', info.child_value('mynews_cnt'))
|
|
newprofile.replace_int('extra_point', info.child_value('extra_point'))
|
|
newprofile.replace_bool('is_extra_played', info.child_value('is_extra_played'))
|
|
|
|
last.replace_int('expert_option', info.child_value('expert_option'))
|
|
last.replace_int('matching', info.child_value('matching'))
|
|
last.replace_int('hazard', info.child_value('hazard'))
|
|
last.replace_int('hard', info.child_value('hard'))
|
|
|
|
# Grab unlock progress
|
|
item = player.child('item')
|
|
if item is not None:
|
|
newprofile.replace_int_array('secret_list', 32, item.child_value('secret_list'))
|
|
newprofile.replace_int_array('title_list', 96, item.child_value('title_list'))
|
|
newprofile.replace_int('theme_list', item.child_value('theme_list'))
|
|
newprofile.replace_int_array('marker_list', 2, item.child_value('marker_list'))
|
|
newprofile.replace_int_array('parts_list', 96, item.child_value('parts_list'))
|
|
newprofile.replace_int_array('secret_list_new', 32, item.child_value('secret_new'))
|
|
newprofile.replace_int_array('title_list_new', 96, item.child_value('title_new'))
|
|
newprofile.replace_int('theme_list_new', item.child_value('theme_new'))
|
|
newprofile.replace_int_array('marker_list_new', 2, item.child_value('marker_new'))
|
|
|
|
# Grab macchiato event
|
|
macchiatodict = newprofile.get_dict('macchiato')
|
|
macchiato = player.child('macchiato')
|
|
if macchiato is not None:
|
|
macchiatodict.replace_int('pack_id', macchiato.child_value('pack_id'))
|
|
macchiatodict.replace_int('bean_num', macchiato.child_value('bean_num'))
|
|
macchiatodict.replace_int('daily_milk_num', macchiato.child_value('daily_milk_num'))
|
|
macchiatodict.replace_bool('is_received_daily_milk', macchiato.child_value('is_received_daily_milk'))
|
|
macchiatodict.replace_bool('sub_menu_is_completed', macchiato.child_value('sub_menu_is_completed'))
|
|
macchiatodict.replace_int('today_tune_cnt', macchiato.child_value('today_tune_cnt'))
|
|
macchiatodict.replace_int_array('daily_milk_bonus', 9, macchiato.child_value('daily_milk_bonus'))
|
|
macchiatodict.replace_int('compensation_milk', macchiato.child_value('compensation_milk'))
|
|
macchiatodict.replace_int('match_cnt', macchiato.child_value('match_cnt'))
|
|
macchiatodict.replace_int('used_bean', macchiato.child_value('used_bean'))
|
|
macchiatodict.replace_int('used_milk', macchiato.child_value('used_milk'))
|
|
macchiatodict.replace_int('daily_play_burst', macchiato.child_value('daily_play_burst'))
|
|
newprofile.replace_dict('macchiato', macchiatodict)
|
|
|
|
# Get timestamps for played songs
|
|
timestamps: Dict[int, int] = {}
|
|
history = player.child('history')
|
|
if history is not None:
|
|
for tune in history.children:
|
|
if tune.name != 'tune':
|
|
continue
|
|
entry = int(tune.attribute('log_id'))
|
|
ts = int(tune.child_value('timestamp') / 1000)
|
|
timestamps[entry] = ts
|
|
|
|
# Grab scores and save those
|
|
result = data.child('result')
|
|
if result is not None:
|
|
for tune in result.children:
|
|
if tune.name != 'tune':
|
|
continue
|
|
result = tune.child('player')
|
|
|
|
last.replace_int('marker', tune.child_value('marker'))
|
|
last.replace_int('title', tune.child_value('title'))
|
|
last.replace_int('parts', tune.child_value('parts'))
|
|
last.replace_int('theme', tune.child_value('theme'))
|
|
last.replace_int('sort', tune.child_value('sort'))
|
|
last.replace_int('category', tune.child_value('category'))
|
|
last.replace_int('rank_sort', tune.child_value('rank_sort'))
|
|
last.replace_int('combo_disp', tune.child_value('combo_disp'))
|
|
|
|
songid = tune.child_value('music')
|
|
entry = int(tune.attribute('id'))
|
|
timestamp = timestamps.get(entry, Time.now())
|
|
chart = int(result.child('score').attribute('seq'))
|
|
points = result.child_value('score')
|
|
flags = int(result.child('score').attribute('clear'))
|
|
combo = int(result.child('score').attribute('combo'))
|
|
ghost = result.child_value('mbar')
|
|
|
|
# Miscelaneous last data for echoing to profile get
|
|
last.replace_int('music_id', songid)
|
|
last.replace_int('seq_id', chart)
|
|
|
|
mapping = {
|
|
self.GAME_FLAG_BIT_CLEARED: self.PLAY_MEDAL_CLEARED,
|
|
self.GAME_FLAG_BIT_FULL_COMBO: self.PLAY_MEDAL_FULL_COMBO,
|
|
self.GAME_FLAG_BIT_EXCELLENT: self.PLAY_MEDAL_EXCELLENT,
|
|
self.GAME_FLAG_BIT_NEARLY_FULL_COMBO: self.PLAY_MEDAL_NEARLY_FULL_COMBO,
|
|
self.GAME_FLAG_BIT_NEARLY_EXCELLENT: self.PLAY_MEDAL_NEARLY_EXCELLENT,
|
|
}
|
|
|
|
# Figure out the highest medal based on bits passed in
|
|
medal = self.PLAY_MEDAL_FAILED
|
|
for bit in mapping:
|
|
if flags & bit > 0:
|
|
medal = max(medal, mapping[bit])
|
|
|
|
self.update_score(userid, timestamp, songid, chart, points, medal, combo, ghost)
|
|
|
|
# Grab the course results as well
|
|
course = data.child('course')
|
|
if course is not None:
|
|
courseid = course.child_value('course_id')
|
|
rating = {
|
|
self.GAME_COURSE_RATING_FAILED: self.COURSE_RATING_FAILED,
|
|
self.GAME_COURSE_RATING_BRONZE: self.COURSE_RATING_BRONZE,
|
|
self.GAME_COURSE_RATING_SILVER: self.COURSE_RATING_SILVER,
|
|
self.GAME_COURSE_RATING_GOLD: self.COURSE_RATING_GOLD,
|
|
}[course.child_value('rating')]
|
|
scores = [0] * 5
|
|
for music in course.children:
|
|
if music.name != 'music':
|
|
continue
|
|
index = int(music.attribute('index'))
|
|
scores[index] = music.child_value('score')
|
|
|
|
# Save course itself
|
|
self.save_course(userid, courseid, rating, scores)
|
|
|
|
# Save the last course ID
|
|
last.replace_int('last_course_id', courseid)
|
|
|
|
# Save back last information gleaned from results
|
|
newprofile.replace_dict('last', last)
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid)
|
|
|
|
return newprofile
|
|
|
|
def format_scores(self, userid: UserID, profile: Profile, scores: List[Score]) -> Node:
|
|
|
|
root = Node.void('gametop')
|
|
datanode = Node.void('data')
|
|
root.add_child(datanode)
|
|
player = Node.void('player')
|
|
datanode.add_child(player)
|
|
player.add_child(Node.s32('jid', profile.extid))
|
|
playdata = Node.void('playdata')
|
|
player.add_child(playdata)
|
|
playdata.set_attribute('count', str(len(scores)))
|
|
|
|
music = ValidatedDict()
|
|
for score in scores:
|
|
data = music.get_dict(str(score.id))
|
|
play_cnt = data.get_int_array('play_cnt', 3)
|
|
clear_cnt = data.get_int_array('clear_cnt', 3)
|
|
clear_flags = data.get_int_array('clear_flags', 3)
|
|
fc_cnt = data.get_int_array('fc_cnt', 3)
|
|
ex_cnt = data.get_int_array('ex_cnt', 3)
|
|
points = data.get_int_array('points', 3)
|
|
|
|
# Replace data for this chart type
|
|
play_cnt[score.chart] = score.plays
|
|
clear_cnt[score.chart] = score.data.get_int('clear_count')
|
|
fc_cnt[score.chart] = score.data.get_int('full_combo_count')
|
|
ex_cnt[score.chart] = score.data.get_int('excellent_count')
|
|
points[score.chart] = score.points
|
|
|
|
# Format the clear flags
|
|
clear_flags[score.chart] = self.GAME_FLAG_BIT_PLAYED
|
|
if score.data.get_int('clear_count') > 0:
|
|
clear_flags[score.chart] |= self.GAME_FLAG_BIT_CLEARED
|
|
if score.data.get_int('full_combo_count') > 0:
|
|
clear_flags[score.chart] |= self.GAME_FLAG_BIT_FULL_COMBO
|
|
if score.data.get_int('excellent_count') > 0:
|
|
clear_flags[score.chart] |= self.GAME_FLAG_BIT_EXCELLENT
|
|
|
|
# Save chart data back
|
|
data.replace_int_array('play_cnt', 3, play_cnt)
|
|
data.replace_int_array('clear_cnt', 3, clear_cnt)
|
|
data.replace_int_array('clear_flags', 3, clear_flags)
|
|
data.replace_int_array('fc_cnt', 3, fc_cnt)
|
|
data.replace_int_array('ex_cnt', 3, ex_cnt)
|
|
data.replace_int_array('points', 3, points)
|
|
|
|
# Update the ghost (untyped)
|
|
ghost = data.get('ghost', [None, None, None])
|
|
ghost[score.chart] = score.data.get('ghost')
|
|
data['ghost'] = ghost
|
|
|
|
# Save it back
|
|
music.replace_dict(str(score.id), data)
|
|
|
|
for scoreid in music:
|
|
scoredata = music[scoreid]
|
|
musicdata = Node.void('musicdata')
|
|
playdata.add_child(musicdata)
|
|
|
|
musicdata.set_attribute('music_id', scoreid)
|
|
musicdata.add_child(Node.s32_array('play_cnt', scoredata.get_int_array('play_cnt', 3)))
|
|
musicdata.add_child(Node.s32_array('clear_cnt', scoredata.get_int_array('clear_cnt', 3)))
|
|
musicdata.add_child(Node.s32_array('fc_cnt', scoredata.get_int_array('fc_cnt', 3)))
|
|
musicdata.add_child(Node.s32_array('ex_cnt', scoredata.get_int_array('ex_cnt', 3)))
|
|
musicdata.add_child(Node.s32_array('score', scoredata.get_int_array('points', 3)))
|
|
musicdata.add_child(Node.s8_array('clear', scoredata.get_int_array('clear_flags', 3)))
|
|
|
|
ghosts = scoredata.get('ghost', [None, None, None])
|
|
for i in range(len(ghosts)):
|
|
ghost = ghosts[i]
|
|
if ghost is None:
|
|
continue
|
|
|
|
bar = Node.u8_array('bar', ghost)
|
|
musicdata.add_child(bar)
|
|
bar.set_attribute('seq', str(i))
|
|
|
|
return root
|