1
0
mirror of synced 2024-12-25 20:14:50 +01:00
bemaniutils/bemani/backend/jubeat/saucerfulfill.py

1063 lines
40 KiB
Python
Raw Normal View History

# vim: set fileencoding=utf-8
import random
from typing import Any, Dict, List, Optional, Tuple
from typing_extensions import Final
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: str = "Jubeat Saucer Fulfill"
version: int = VersionConstants.JUBEAT_SAUCER_FULFILL
GAME_COURSE_REQUIREMENT_SCORE: Final[int] = 1
GAME_COURSE_REQUIREMENT_FULL_COMBO: Final[int] = 2
GAME_COURSE_REQUIREMENT_PERFECT_PERCENT: Final[int] = 3
GAME_COURSE_RATING_FAILED: Final[int] = 1
GAME_COURSE_RATING_BRONZE: Final[int] = 2
GAME_COURSE_RATING_SILVER: Final[int] = 3
GAME_COURSE_RATING_GOLD: Final[int] = 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)
)
if len(all_songs) >= 2:
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
2020-05-09 00:41:51 +02:00
def handle_gametop_get_rival_mdata_request(self, request: Node) -> Node:
data = request.child("data")
player = data.child("player")
extid = player.child_value("rival")
2020-05-09 00:41:51 +02:00
root = self.get_scores_by_extid(extid)
if root is None:
root = Node.void("gametop")
root.set_attribute("status", str(Status.NO_PROFILE))
2020-05-09 00:41:51 +02:00
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") and not profile.get_bool("saved"),
)
)
# 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))
2020-05-09 00:41:51 +02:00
# Add rivals to profile.
rivallist = Node.void("rivallist")
player.add_child(rivallist)
2020-05-09 00:41:51 +02:00
links = self.data.local.user.get_links(self.game, self.version, userid)
rivalcount = 0
for link in links:
if link.type != "rival":
2020-05-09 00:41:51 +02:00
continue
rprofile = self.get_profile(link.other_userid)
if rprofile is None:
continue
rival = Node.void("rival")
2020-05-09 00:41:51 +02:00
rivallist.add_child(rival)
rival.add_child(Node.s32("jid", rprofile.extid))
rival.add_child(Node.string("name", rprofile.get_str("name")))
2020-05-09 00:41:51 +02:00
# 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.music_version,
userid,
entry.get_int("today", -1),
timelimit=start_time,
)
whim_attempts = self.data.local.music.get_all_attempts(
self.game,
self.music_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 = oldprofile.clone()
newprofile.replace_bool("saved", True)
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:
# Ignore festo-and-above chart types.
if score.chart not in {
self.CHART_TYPE_BASIC,
self.CHART_TYPE_ADVANCED,
self.CHART_TYPE_EXTREME,
}:
continue
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