1
0
mirror of synced 2024-11-27 23:50:47 +01:00
bemaniutils/bemani/backend/reflec/groovin.py
2024-01-02 02:46:24 +00:00

1513 lines
65 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Optional, Dict, Any, List, Tuple
from typing_extensions import Final
from bemani.backend.reflec.base import ReflecBeatBase
from bemani.backend.reflec.colette import ReflecBeatColette
from bemani.common import Profile, ValidatedDict, VersionConstants, ID, Time
from bemani.data import Achievement, Attempt, Score, UserID
from bemani.protocol import Node
class ReflecBeatGroovin(ReflecBeatBase):
name: str = "REFLEC BEAT groovin'!!"
version: int = VersionConstants.REFLEC_BEAT_GROOVIN
# Clear types according to the game
GAME_CLEAR_TYPE_NO_PLAY: Final[int] = 0
GAME_CLEAR_TYPE_EARLY_FAILED: Final[int] = 1
GAME_CLEAR_TYPE_FAILED: Final[int] = 2
GAME_CLEAR_TYPE_CLEARED: Final[int] = 9
GAME_CLEAR_TYPE_HARD_CLEARED: Final[int] = 10
GAME_CLEAR_TYPE_S_HARD_CLEARED: Final[int] = 11
# Combo types according to the game (actually a bitmask, where bit 0 is
# full combo status, and bit 2 is just reflec status). But we don't support
# saving just reflec without full combo, so we downgrade it.
GAME_COMBO_TYPE_NONE: Final[int] = 0
GAME_COMBO_TYPE_ALL_JUST: Final[int] = 2
GAME_COMBO_TYPE_FULL_COMBO: Final[int] = 1
GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST: Final[int] = 3
def previous_version(self) -> Optional[ReflecBeatBase]:
return ReflecBeatColette(self.data, self.config, self.model)
@classmethod
def get_settings(cls) -> Dict[str, Any]:
"""
Return all of our front-end modifiably settings.
"""
return {
"bools": [
{
"name": "Force Song Unlock",
"tip": "Force unlock all songs.",
"category": "game_config",
"setting": "force_unlock_songs",
},
],
"ints": [],
}
def __db_to_game_clear_type(self, db_status: int) -> int:
return {
self.CLEAR_TYPE_NO_PLAY: self.GAME_CLEAR_TYPE_NO_PLAY,
self.CLEAR_TYPE_FAILED: self.GAME_CLEAR_TYPE_FAILED,
self.CLEAR_TYPE_CLEARED: self.GAME_CLEAR_TYPE_CLEARED,
self.CLEAR_TYPE_HARD_CLEARED: self.GAME_CLEAR_TYPE_HARD_CLEARED,
self.CLEAR_TYPE_S_HARD_CLEARED: self.GAME_CLEAR_TYPE_S_HARD_CLEARED,
}[db_status]
def __game_to_db_clear_type(self, status: int) -> int:
return {
self.GAME_CLEAR_TYPE_NO_PLAY: self.CLEAR_TYPE_NO_PLAY,
self.GAME_CLEAR_TYPE_EARLY_FAILED: self.CLEAR_TYPE_FAILED,
self.GAME_CLEAR_TYPE_FAILED: self.CLEAR_TYPE_FAILED,
self.GAME_CLEAR_TYPE_CLEARED: self.CLEAR_TYPE_CLEARED,
self.GAME_CLEAR_TYPE_HARD_CLEARED: self.CLEAR_TYPE_HARD_CLEARED,
self.GAME_CLEAR_TYPE_S_HARD_CLEARED: self.CLEAR_TYPE_S_HARD_CLEARED,
}[status]
def __db_to_game_combo_type(self, db_combo: int) -> int:
return {
self.COMBO_TYPE_NONE: self.GAME_COMBO_TYPE_NONE,
self.COMBO_TYPE_ALMOST_COMBO: self.GAME_COMBO_TYPE_NONE,
self.COMBO_TYPE_FULL_COMBO: self.GAME_COMBO_TYPE_FULL_COMBO,
self.COMBO_TYPE_FULL_COMBO_ALL_JUST: self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST,
}[db_combo]
def __game_to_db_combo_type(self, game_combo: int, miss_count: int) -> int:
if game_combo in [
self.GAME_COMBO_TYPE_NONE,
self.GAME_COMBO_TYPE_ALL_JUST,
]:
if miss_count >= 0 and miss_count <= 2:
return self.COMBO_TYPE_ALMOST_COMBO
else:
return self.COMBO_TYPE_NONE
if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO:
return self.COMBO_TYPE_FULL_COMBO
if game_combo == self.GAME_COMBO_TYPE_FULL_COMBO_ALL_JUST:
return self.COMBO_TYPE_FULL_COMBO_ALL_JUST
raise Exception(f"Invalid game_combo value {game_combo}")
def handle_pcb_rb4error_request(self, request: Node) -> Node:
return Node.void("pcb")
def handle_pcb_rb4uptime_update_request(self, request: Node) -> Node:
return Node.void("pcb")
def handle_pcb_rb4boot_request(self, request: Node) -> Node:
shop_id = ID.parse_machine_id(request.child_value("lid"))
machine = self.get_machine_by_id(shop_id)
if machine is not None:
machine_name = machine.name
close = machine.data.get_bool("close")
hour = machine.data.get_int("hour")
minute = machine.data.get_int("minute")
else:
machine_name = ""
close = False
hour = 0
minute = 0
root = Node.void("pcb")
sinfo = Node.void("sinfo")
root.add_child(sinfo)
sinfo.add_child(Node.string("nm", machine_name))
sinfo.add_child(Node.bool("cl_enbl", close))
sinfo.add_child(Node.u8("cl_h", hour))
sinfo.add_child(Node.u8("cl_m", minute))
sinfo.add_child(Node.bool("shop_flag", True))
return root
def handle_lobby_rb4entry_request(self, request: Node) -> Node:
root = Node.void("lobby")
root.add_child(Node.s32("interval", 120))
root.add_child(Node.s32("interval_p", 120))
# Create a lobby entry for this user
extid = request.child_value("e/uid")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is not None:
profile = self.get_profile(userid)
info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid)
if profile is None or info is None:
return root
self.data.local.lobby.put_lobby(
self.game,
self.version,
userid,
{
"mid": request.child_value("e/mid"),
"ng": request.child_value("e/ng"),
"mopt": request.child_value("e/mopt"),
"lid": request.child_value("e/lid"),
"sn": request.child_value("e/sn"),
"pref": request.child_value("e/pref"),
"stg": request.child_value("e/stg"),
"pside": request.child_value("e/pside"),
"eatime": request.child_value("e/eatime"),
"ga": request.child_value("e/ga"),
"gp": request.child_value("e/gp"),
"la": request.child_value("e/la"),
"ver": request.child_value("e/ver"),
"tension": request.child_value("e/tension"),
},
)
lobby = self.data.local.lobby.get_lobby(
self.game,
self.version,
userid,
)
root.add_child(Node.s32("eid", lobby.get_int("id")))
e = Node.void("e")
root.add_child(e)
e.add_child(Node.s32("eid", lobby.get_int("id")))
e.add_child(Node.u16("mid", lobby.get_int("mid")))
e.add_child(Node.u8("ng", lobby.get_int("ng")))
e.add_child(Node.s32("uid", profile.extid))
e.add_child(Node.s32("uattr", profile.get_int("uattr")))
e.add_child(Node.string("pn", profile.get_str("name")))
e.add_child(Node.s32("plyid", info.get_int("id")))
e.add_child(Node.s16("mg", profile.get_int("mg")))
e.add_child(Node.s32("mopt", lobby.get_int("mopt")))
e.add_child(Node.string("lid", lobby.get_str("lid")))
e.add_child(Node.string("sn", lobby.get_str("sn")))
e.add_child(Node.u8("pref", lobby.get_int("pref")))
e.add_child(Node.s8("stg", lobby.get_int("stg")))
e.add_child(Node.s8("pside", lobby.get_int("pside")))
e.add_child(Node.s16("eatime", lobby.get_int("eatime")))
e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4)))
e.add_child(Node.u16("gp", lobby.get_int("gp")))
e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4)))
e.add_child(Node.u8("ver", lobby.get_int("ver")))
e.add_child(Node.s8("tension", lobby.get_int("tension")))
return root
def handle_lobby_rb4read_request(self, request: Node) -> Node:
root = Node.void("lobby")
root.add_child(Node.s32("interval", 120))
root.add_child(Node.s32("interval_p", 120))
# Look up all lobbies matching the criteria specified
ver = request.child_value("var")
mg = request.child_value("m_grade") # noqa: F841
extid = request.child_value("uid")
limit = request.child_value("max")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is not None:
lobbies = self.data.local.lobby.get_all_lobbies(self.game, self.version)
for user, lobby in lobbies:
if limit <= 0:
break
if user == userid:
# If we have our own lobby, don't return it
continue
if ver != lobby.get_int("ver"):
# Don't return lobby data for different versions
continue
profile = self.get_profile(user)
info = self.data.local.lobby.get_play_session_info(self.game, self.version, userid)
if profile is None or info is None:
# No profile info, don't return this lobby
return root
e = Node.void("e")
root.add_child(e)
e.add_child(Node.s32("eid", lobby.get_int("id")))
e.add_child(Node.u16("mid", lobby.get_int("mid")))
e.add_child(Node.u8("ng", lobby.get_int("ng")))
e.add_child(Node.s32("uid", profile.extid))
e.add_child(Node.s32("uattr", profile.get_int("uattr")))
e.add_child(Node.string("pn", profile.get_str("name")))
e.add_child(Node.s32("plyid", info.get_int("id")))
e.add_child(Node.s16("mg", profile.get_int("mg")))
e.add_child(Node.s32("mopt", lobby.get_int("mopt")))
e.add_child(Node.string("lid", lobby.get_str("lid")))
e.add_child(Node.string("sn", lobby.get_str("sn")))
e.add_child(Node.u8("pref", lobby.get_int("pref")))
e.add_child(Node.s8("stg", lobby.get_int("stg")))
e.add_child(Node.s8("pside", lobby.get_int("pside")))
e.add_child(Node.s16("eatime", lobby.get_int("eatime")))
e.add_child(Node.u8_array("ga", lobby.get_int_array("ga", 4)))
e.add_child(Node.u16("gp", lobby.get_int("gp")))
e.add_child(Node.u8_array("la", lobby.get_int_array("la", 4)))
e.add_child(Node.u8("ver", lobby.get_int("ver")))
e.add_child(Node.s8("tension", lobby.get_int("tension")))
limit = limit - 1
return root
def handle_lobby_rb4delete_request(self, request: Node) -> Node:
eid = request.child_value("eid")
self.data.local.lobby.destroy_lobby(eid)
return Node.void("lobby")
def handle_shop_rb4setting_write_request(self, request: Node) -> Node:
return Node.void("shop")
def handle_shop_rb4info_write_request(self, request: Node) -> Node:
self.update_machine_name(request.child_value("sinfo/nm"))
self.update_machine_data(
{
"close": request.child_value("sinfo/cl_enbl"),
"hour": request.child_value("sinfo/cl_h"),
"minute": request.child_value("sinfo/cl_m"),
"pref": request.child_value("sinfo/prf"),
}
)
return Node.void("shop")
def __add_event_info(self, root: Node) -> None:
event_ctrl = Node.void("event_ctrl")
root.add_child(event_ctrl)
# Contains zero or more nodes like:
# <data>
# <type __type="s32">any</type>
# <index __type="s32">any</phase>
# <value __type="s32">any</phase>
# <value2 __type="s32">any</phase>
# <start_time __type="s32">any</phase>
# <end_time __type="s32">any</phase>
# </data>
item_lock_ctrl = Node.void("item_lock_ctrl")
root.add_child(item_lock_ctrl)
# Contains zero or more nodes like:
# <item>
# <type __type="u8">any</type>
# <id __type="u16">any</id>
# <param __type="u16">0-3</param>
# </item>
def __add_shop_score(self, root: Node) -> None:
shop_score = Node.void("shop_score")
root.add_child(shop_score)
today = Node.void("today")
shop_score.add_child(today)
yesterday = Node.void("yesterday")
shop_score.add_child(yesterday)
all_profiles = self.data.local.user.get_all_profiles(self.game, self.version)
all_attempts = self.data.local.music.get_all_attempts(
self.game,
self.version,
timelimit=(Time.beginning_of_today() - Time.SECONDS_IN_DAY),
)
machine = self.data.local.machine.get_machine(self.config.machine.pcbid)
if machine.arcade is not None:
lids = [machine.id for machine in self.data.local.machine.get_all_machines(machine.arcade)]
else:
lids = [machine.id]
relevant_profiles = [profile for profile in all_profiles if profile[1].get_int("lid", -1) in lids]
for rootnode, timeoffset in [
(today, 0),
(yesterday, Time.SECONDS_IN_DAY),
]:
# Grab all attempts made in the relevant day
relevant_attempts = [
attempt
for attempt in all_attempts
if (
attempt[1].timestamp >= (Time.beginning_of_today() - timeoffset)
and attempt[1].timestamp <= (Time.end_of_today() - timeoffset)
)
]
# Calculate scores based on attempt
scores_by_user: Dict[UserID, Dict[int, Dict[int, Attempt]]] = {}
for userid, attempt in relevant_attempts:
if userid not in scores_by_user:
scores_by_user[userid] = {}
if attempt.id not in scores_by_user[userid]:
scores_by_user[userid][attempt.id] = {}
if attempt.chart not in scores_by_user[userid][attempt.id]:
# No high score for this yet, just use this attempt
scores_by_user[userid][attempt.id][attempt.chart] = attempt
else:
# If this attempt is better than the stored one, replace it
if scores_by_user[userid][attempt.id][attempt.chart].points < attempt.points:
scores_by_user[userid][attempt.id][attempt.chart] = attempt
# Calculate points earned by user in the day
points_by_user: Dict[UserID, int] = {}
for userid in scores_by_user:
points_by_user[userid] = 0
for mid in scores_by_user[userid]:
for chart in scores_by_user[userid][mid]:
points_by_user[userid] = points_by_user[userid] + scores_by_user[userid][mid][chart].points
# Output that day's earned points
for userid, profile in relevant_profiles:
data = Node.void("data")
rootnode.add_child(data)
data.add_child(Node.s16("day_id", int((Time.now() - timeoffset) / Time.SECONDS_IN_DAY)))
data.add_child(Node.s32("user_id", profile.extid))
data.add_child(Node.s16("icon_id", profile.get_dict("config").get_int("icon_id")))
data.add_child(Node.s16("point", min(points_by_user.get(userid, 0), 32767)))
data.add_child(Node.s32("update_time", Time.now()))
data.add_child(Node.string("name", profile.get_str("name")))
rootnode.add_child(Node.s32("timestamp", Time.beginning_of_today() - timeoffset))
def handle_info_rb4common_request(self, request: Node) -> Node:
root = Node.void("info")
self.__add_event_info(root)
self.__add_shop_score(root)
return root
def handle_info_rb4ranking_request(self, request: Node) -> Node:
version = request.child_value("ver")
root = Node.void("info")
root.add_child(Node.s32("ver", version))
ranking = Node.void("ranking")
root.add_child(ranking)
def add_hitchart(name: str, start: int, end: int, hitchart: List[Tuple[int, int]]) -> None:
base = Node.void(name)
ranking.add_child(base)
base.add_child(Node.s32("bt", start))
base.add_child(Node.s32("et", end))
new = Node.void("new")
base.add_child(new)
for mid, plays in hitchart:
d = Node.void("d")
new.add_child(d)
d.add_child(Node.s16("mid", mid))
d.add_child(Node.s32("cnt", plays))
# Weekly hit chart
add_hitchart(
"weekly",
Time.now() - Time.SECONDS_IN_WEEK,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 7),
)
# Monthly hit chart
add_hitchart(
"monthly",
Time.now() - Time.SECONDS_IN_DAY * 30,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 30),
)
# All time hit chart
add_hitchart(
"total",
Time.now() - Time.SECONDS_IN_DAY * 365,
Time.now(),
self.data.local.music.get_hit_chart(self.game, self.version, 1024, 365),
)
return root
def handle_info_rb4shop_score_ranking_request(self, request: Node) -> Node:
start_music_id = request.child_value("min")
end_music_id = request.child_value("max")
root = Node.void("info")
shop_score = Node.void("shop_score")
root.add_child(shop_score)
shop_score.add_child(Node.s32("time", Time.now()))
profiles: Dict[UserID, Profile] = {}
for songid in range(start_music_id, end_music_id + 1):
allscores = self.data.local.music.get_all_scores(
self.game,
self.version,
songid=songid,
)
for ng in [
self.CHART_TYPE_BASIC,
self.CHART_TYPE_MEDIUM,
self.CHART_TYPE_HARD,
self.CHART_TYPE_SPECIAL,
]:
scores = sorted(
[score for score in allscores if score[1].chart == ng],
key=lambda score: score[1].points,
reverse=True,
)
for i in range(len(scores)):
userid, score = scores[i]
if userid not in profiles:
profiles[userid] = self.get_any_profile(userid)
profile = profiles[userid]
data = Node.void("data")
shop_score.add_child(data)
data.add_child(Node.s32("rank", i + 1))
data.add_child(Node.s16("music_id", songid))
data.add_child(Node.s8("note_grade", score.chart))
data.add_child(
Node.s8(
"clear_type",
self.__db_to_game_clear_type(score.data.get_int("clear_type")),
)
)
data.add_child(Node.s32("user_id", profile.extid))
data.add_child(Node.s16("icon_id", profile.get_dict("config").get_int("icon_id")))
data.add_child(Node.s32("score", score.points))
data.add_child(Node.s32("time", score.timestamp))
data.add_child(Node.string("name", profile.get_str("name")))
return root
def handle_info_rb4pzlcmt_read_request(self, request: Node) -> Node:
extid = request.child_value("uid")
locid = ID.parse_machine_id(request.child_value("lid"))
limit = request.child_value("limit")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
comments = [
achievement
for achievement in self.data.local.user.get_all_time_based_achievements(self.game, self.version)
if achievement[1].type == "puzzle_comment"
]
comments.sort(key=lambda x: x[1].timestamp, reverse=True)
favorites = [comment for comment in comments if comment[0] == userid]
locationcomments = [comment for comment in comments if comment[1].data.get_int("locid") == locid]
# Cap all comment blocks to the limit
if limit >= 0:
comments = comments[:limit]
favorites = favorites[:limit]
locationcomments = locationcomments[:limit]
root = Node.void("info")
comment = Node.void("comment")
root.add_child(comment)
comment.add_child(Node.s32("time", Time.now()))
# Mapping of profiles to userIDs
uid_mapping = {uid: prof for (uid, prof) in self.get_any_profiles([c[0] for c in comments])}
# Handle anonymous comments by returning a default profile
uid_mapping[UserID(0)] = Profile(
self.game,
self.version,
"",
0,
{"name": ""},
)
def add_comments(name: str, selected: List[Tuple[UserID, Achievement]]) -> None:
for uid, ach in selected:
cmnt = Node.void(name)
root.add_child(cmnt)
cmnt.add_child(Node.s32("uid", uid_mapping[uid].extid))
cmnt.add_child(Node.string("name", uid_mapping[uid].get_str("name")))
cmnt.add_child(Node.s16("icon", ach.data.get_int("icon")))
cmnt.add_child(Node.s8("bln", ach.data.get_int("bln")))
cmnt.add_child(Node.string("lid", ID.format_machine_id(ach.data.get_int("locid"))))
cmnt.add_child(Node.s8("pref", ach.data.get_int("prefecture")))
cmnt.add_child(Node.s32("time", ach.timestamp))
cmnt.add_child(Node.string("comment", ach.data.get_str("comment")))
cmnt.add_child(Node.bool("is_tweet", ach.data.get_bool("tweet")))
# Add all comments
add_comments("c", comments)
# Add personal comments (favorites)
add_comments("cf", favorites)
# Add location comments
add_comments("cs", locationcomments)
return root
def handle_info_rb4pzlcmt_write_request(self, request: Node) -> Node:
extid = request.child_value("uid")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is None:
# Anonymous comment
userid = UserID(0)
icon = request.child_value("icon")
bln = request.child_value("bln")
locid = ID.parse_machine_id(request.child_value("lid"))
prefecture = request.child_value("pref")
comment = request.child_value("comment")
is_tweet = request.child_value("is_tweet")
# Link comment to user's profile
self.data.local.user.put_time_based_achievement(
self.game,
self.version,
userid,
0, # We never have an ID for this, since comments are add-only
"puzzle_comment",
{
"icon": icon,
"bln": bln,
"locid": locid,
"prefecture": prefecture,
"comment": comment,
"tweet": is_tweet,
},
)
return Node.void("info")
def handle_player_rb4start_request(self, request: Node) -> Node:
root = Node.void("player")
# Create a new play session based on info from the request
refid = request.child_value("rid")
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
self.data.local.lobby.put_play_session_info(
self.game,
self.version,
userid,
{
"ga": request.child_value("ga"),
"gp": request.child_value("gp"),
"la": request.child_value("la"),
"pnid": request.child_value("pnid"),
},
)
info = self.data.local.lobby.get_play_session_info(
self.game,
self.version,
userid,
)
if info is not None:
play_id = info.get_int("id")
else:
play_id = 0
else:
play_id = 0
# Session stuff, and resend global defaults
root.add_child(Node.s32("plyid", play_id))
root.add_child(Node.u64("start_time", Time.now() * 1000))
self.__add_event_info(root)
return root
def handle_player_rb4end_request(self, request: Node) -> Node:
# Destroy play session based on info from the request
refid = request.child_value("rid")
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
# Kill any lingering lobbies by this user
lobby = self.data.local.lobby.get_lobby(
self.game,
self.version,
userid,
)
if lobby is not None:
self.data.local.lobby.destroy_lobby(lobby.get_int("id"))
self.data.local.lobby.destroy_play_session_info(self.game, self.version, userid)
return Node.void("player")
def handle_player_rb4readepisode_request(self, request: Node) -> Node:
extid = request.child_value("user_id")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is not None:
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
else:
achievements = []
root = Node.void("player")
pdata = Node.void("pdata")
root.add_child(pdata)
episode = Node.void("episode")
pdata.add_child(episode)
for achievement in achievements:
if achievement.type != "episode":
continue
info = Node.void("info")
episode.add_child(info)
info.add_child(Node.s32("user_id", extid))
info.add_child(Node.u8("type", achievement.id))
info.add_child(Node.u16("value0", achievement.data.get_int("value0")))
info.add_child(Node.u16("value1", achievement.data.get_int("value1")))
info.add_child(Node.string("text", achievement.data.get_str("text")))
info.add_child(Node.s32("time", achievement.data.get_int("time")))
return root
def handle_player_rb4readscore_request(self, request: Node) -> Node:
refid = request.child_value("rid")
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
scores: List[Score] = []
else:
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
root = Node.void("player")
pdata = Node.void("pdata")
root.add_child(pdata)
record = Node.void("record")
pdata.add_child(record)
record_old = Node.void("record_old")
pdata.add_child(record_old)
for score in scores:
rec = Node.void("rec")
record.add_child(rec)
rec.add_child(Node.s16("mid", score.id))
rec.add_child(Node.s8("ntgrd", score.chart))
rec.add_child(Node.s32("pc", score.plays))
rec.add_child(Node.s8("ct", self.__db_to_game_clear_type(score.data.get_int("clear_type"))))
rec.add_child(Node.s16("ar", score.data.get_int("achievement_rate")))
rec.add_child(Node.s16("scr", score.points))
rec.add_child(Node.s16("ms", score.data.get_int("miss_count")))
rec.add_child(
Node.s16(
"param",
self.__db_to_game_combo_type(score.data.get_int("combo_type")) + score.data.get_int("param"),
)
)
rec.add_child(Node.s32("bscrt", score.timestamp))
rec.add_child(Node.s32("bart", score.data.get_int("best_achievement_rate_time")))
rec.add_child(Node.s32("bctt", score.data.get_int("best_clear_type_time")))
rec.add_child(Node.s32("bmst", score.data.get_int("best_miss_count_time")))
rec.add_child(Node.s32("time", score.data.get_int("last_played_time")))
return root
def handle_player_rb4selectscore_request(self, request: Node) -> Node:
extid = request.child_value("uid")
songid = request.child_value("music_id")
chart = request.child_value("note_grade")
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if userid is None:
score = None
profile = None
else:
score = self.data.remote.music.get_score(self.game, self.version, userid, songid, chart)
profile = self.get_any_profile(userid)
root = Node.void("player")
if score is not None and profile is not None:
player_select_score = Node.void("player_select_score")
root.add_child(player_select_score)
player_select_score.add_child(Node.s32("user_id", extid))
player_select_score.add_child(Node.string("name", profile.get_str("name")))
player_select_score.add_child(Node.s32("m_score", score.points))
player_select_score.add_child(Node.s32("m_scoreTime", score.timestamp))
player_select_score.add_child(Node.s16("m_iconID", profile.get_dict("config").get_int("icon_id")))
return root
def handle_player_rbsvLinkageSave_request(self, request: Node) -> Node:
# I think this is ReflecBeat/SoundVoltex linkage save, and I
# am somewhat convinced that PK/BN is for packets/blocks, but
# whatever.
root = Node.void("player")
root.add_child(Node.s32("before_pk_value", -1))
root.add_child(Node.s32("after_pk_value", -1))
root.add_child(Node.s32("before_bn_value", -1))
root.add_child(Node.s32("after_bn_value", -1))
return root
def handle_player_rb4total_bestallrank_read_request(self, request: Node) -> Node:
# This gives us a 6-integer array mapping to user scores for the following:
# [total score, basic chart score, medium chart score, hard chart score,
# special chart score, new songs score].
# It appears to return several 6-array values similar to the following:
# <score>
# <rank __type="s32" __count="6">1 2 3 4 5 6</rank>
# <score __type="s32" __count="6">101 102 103 104 105 106</score>
# <allrank __type="s32" __count="6">7 8 9 10 11 12</allrank>
# </score>
# The first 'rank' is the displayed value for the six categories. The
# second and third values appear unused in-game. I think this is supposed
# to give a player the idea of what ranking they are on the server for
# various scores.
current_scores = request.child_value("score")
# First, grab all scores on the network for this version, and all songs
# available so we know which songs are new to this version of the game.
all_scores = self.data.remote.music.get_all_scores(self.game, self.version)
all_songs = self.data.local.music.get_all_songs(self.game, self.version)
# Figure out what song IDs are new
new_songs = {song.id for song in all_songs if song.data.get_int("folder", 0) == self.version}
# Now grab all participating users that had scores
all_users = {userid for (userid, score) in all_scores}
# Now, group the scores by user, so we can add up the totals, only including
# scores where the user at least cleared the song.
scores_by_user = {
userid: [
score
for (uid, score) in all_scores
if uid == userid and score.data.get_int("clear_type") >= self.CLEAR_TYPE_CLEARED
]
for userid in all_users
}
# Now, sum up the scores into the six categories that the game expects.
total_scores = sorted(
[sum([score.points for score in scores]) for userid, scores in scores_by_user.items()],
reverse=True,
)
basic_scores = sorted(
[
sum([score.points for score in scores if score.chart == self.CHART_TYPE_BASIC])
for userid, scores in scores_by_user.items()
],
reverse=True,
)
medium_scores = sorted(
[
sum([score.points for score in scores if score.chart == self.CHART_TYPE_MEDIUM])
for userid, scores in scores_by_user.items()
],
reverse=True,
)
hard_scores = sorted(
[
sum([score.points for score in scores if score.chart == self.CHART_TYPE_HARD])
for userid, scores in scores_by_user.items()
],
reverse=True,
)
special_scores = sorted(
[
sum([score.points for score in scores if score.chart == self.CHART_TYPE_SPECIAL])
for userid, scores in scores_by_user.items()
],
reverse=True,
)
new_scores = sorted(
[
sum([score.points for score in scores if score.id in new_songs])
for userid, scores in scores_by_user.items()
],
reverse=True,
)
# Guarantee that a zero score is at the end of every list, so that it makes
# the algorithm for figuring out place have no edge case.
total_scores.append(0)
basic_scores.append(0)
medium_scores.append(0)
hard_scores.append(0)
special_scores.append(0)
new_scores.append(0)
# Now, figure out where we fit based on the scores sent from the game.
user_place = [1, 1, 1, 1, 1, 1]
which_score = [
total_scores,
basic_scores,
medium_scores,
hard_scores,
special_scores,
new_scores,
]
for i in range(len(user_place)):
current_score = current_scores[i]
scores = which_score[i]
for score in scores:
if current_score >= score:
break
user_place[i] = user_place[i] + 1
root = Node.void("player")
scorenode = Node.void("score")
root.add_child(scorenode)
scorenode.add_child(Node.s32_array("rank", user_place))
scorenode.add_child(Node.s32_array("score", [0] * 6))
scorenode.add_child(Node.s32_array("allrank", [len(total_scores)] * 6))
return root
def handle_player_rb4delete_request(self, request: Node) -> Node:
return Node.void("player")
def handle_player_rb4read_request(self, request: Node) -> Node:
refid = request.child_value("rid")
profile = self.get_profile_by_refid(refid)
if profile:
return profile
return Node.void("player")
def handle_player_rb4succeed_request(self, request: Node) -> Node:
refid = request.child_value("rid")
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
previous_version = self.previous_version()
profile = previous_version.get_profile(userid)
achievements = self.data.local.user.get_achievements(
previous_version.game, previous_version.version, userid
)
scores = self.data.remote.music.get_scores(previous_version.game, previous_version.version, userid)
else:
profile = None
root = Node.void("player")
if profile is None:
# Return empty succeed to say this is new
root.add_child(Node.string("name", ""))
root.add_child(Node.s16("lv", -1))
root.add_child(Node.s32("exp", -1))
root.add_child(Node.s32("grd", -1))
root.add_child(Node.s32("ap", -1))
root.add_child(Node.s32("money", -1))
root.add_child(Node.void("released"))
root.add_child(Node.void("mrecord"))
else:
# Return previous profile formatted to say this is data succession
root.add_child(Node.string("name", profile.get_str("name")))
root.add_child(Node.s16("lv", profile.get_int("lvl")))
root.add_child(Node.s32("exp", profile.get_int("exp")))
root.add_child(Node.s32("grd", profile.get_int("mg"))) # This is a guess
root.add_child(Node.s32("ap", profile.get_int("ap")))
root.add_child(Node.s32("money", 0))
released = Node.void("released")
root.add_child(released)
for item in achievements:
if item.type != "item_0":
continue
released.add_child(Node.s16("i", item.id))
mrecord = Node.void("mrecord")
root.add_child(mrecord)
for score in scores:
mrec = Node.void("mrec")
mrecord.add_child(mrec)
mrec.add_child(Node.s16("mid", score.id))
mrec.add_child(Node.s8("ntgrd", score.chart))
mrec.add_child(Node.s32("pc", score.plays))
mrec.add_child(
Node.s8(
"ct",
self.__db_to_game_clear_type(score.data.get_int("clear_type")),
)
)
mrec.add_child(Node.s16("ar", score.data.get_int("achievement_rate")))
mrec.add_child(Node.s16("scr", score.points))
mrec.add_child(Node.s16("ms", score.data.get_int("miss_count")))
mrec.add_child(Node.u16("ver", 0))
mrec.add_child(Node.s32("bst", score.timestamp))
mrec.add_child(Node.s32("bat", score.data.get_int("best_achievement_rate_time")))
mrec.add_child(Node.s32("bct", score.data.get_int("best_clear_type_time")))
mrec.add_child(Node.s32("bmt", score.data.get_int("best_miss_count_time")))
return root
def handle_player_rb4write_request(self, request: Node) -> Node:
refid = request.child_value("pdata/account/rid")
profile = self.put_profile_by_refid(refid, request)
root = Node.void("player")
if profile is None:
root.add_child(Node.s32("uid", 0))
else:
root.add_child(Node.s32("uid", profile.extid))
return root
def format_profile(self, userid: UserID, profile: Profile) -> Node:
statistics = self.get_play_statistics(userid)
game_config = self.get_game_config()
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
links = self.data.local.user.get_links(self.game, self.version, userid)
root = Node.void("player")
pdata = Node.void("pdata")
root.add_child(pdata)
# Account info
account = Node.void("account")
pdata.add_child(account)
account.add_child(Node.s32("usrid", profile.extid))
account.add_child(Node.s32("tpc", statistics.total_plays))
account.add_child(Node.s32("dpc", statistics.today_plays))
account.add_child(Node.s32("crd", 1))
account.add_child(Node.s32("brd", 1))
account.add_child(Node.s32("tdc", statistics.total_days))
account.add_child(Node.s32("intrvld", 0))
account.add_child(Node.s16("ver", 1))
account.add_child(Node.u64("pst", 0))
account.add_child(Node.u64("st", Time.now() * 1000))
account.add_child(Node.u8("debutVer", 2))
# Base profile info
base = Node.void("base")
pdata.add_child(base)
base.add_child(Node.string("name", profile.get_str("name")))
base.add_child(Node.s32("exp", profile.get_int("exp")))
base.add_child(Node.s32("lv", profile.get_int("lvl")))
base.add_child(Node.s32("mg", profile.get_int("mg")))
base.add_child(Node.s32("ap", profile.get_int("ap")))
base.add_child(Node.string("cmnt", ""))
base.add_child(Node.s32("uattr", profile.get_int("uattr")))
base.add_child(Node.s32("money", profile.get_int("money")))
base.add_child(Node.s32("tbs", -1))
base.add_child(Node.s32("tbs_r", -1))
base.add_child(Node.s32("tbgs", -1))
base.add_child(Node.s32("tbgs_r", -1))
base.add_child(Node.s32("tbms", -1))
base.add_child(Node.s32("tbms_r", -1))
base.add_child(Node.s32("qe_win", -1))
base.add_child(Node.s32("qe_legend", -1))
base.add_child(Node.s32("qe2_win", -1))
base.add_child(Node.s32("qe2_legend", -1))
base.add_child(Node.s32("qe3_win", -1))
base.add_child(Node.s32("qe3_legend", -1))
base.add_child(Node.s16_array("mlog", [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]))
base.add_child(Node.s32("class", profile.get_int("class")))
base.add_child(Node.s32("class_ar", profile.get_int("class_ar")))
base.add_child(Node.s32("getrfl", -1))
base.add_child(Node.s32("upper_pt", profile.get_int("upper_pt")))
# Rivals
rival = Node.void("rival")
pdata.add_child(rival)
slotid = 0
for link in links:
if link.type != "rival":
continue
rprofile = self.get_profile(link.other_userid)
if rprofile is None:
continue
lobbyinfo = self.data.local.lobby.get_play_session_info(self.game, self.version, link.other_userid)
if lobbyinfo is None:
lobbyinfo = ValidatedDict()
r = Node.void("r")
rival.add_child(r)
r.add_child(Node.s32("slot_id", slotid))
r.add_child(Node.s32("id", rprofile.extid))
r.add_child(Node.string("name", rprofile.get_str("name")))
r.add_child(Node.s32("icon", profile.get_dict("config").get_int("icon_id")))
r.add_child(Node.s32("m_level", profile.get_int("mg")))
r.add_child(Node.s32("class", profile.get_int("class")))
r.add_child(Node.s32("class_ar", profile.get_int("class_ar")))
r.add_child(Node.bool("friend", True))
r.add_child(Node.bool("target", False))
r.add_child(Node.u32("time", lobbyinfo.get_int("time")))
r.add_child(Node.u8_array("ga", lobbyinfo.get_int_array("ga", 4)))
r.add_child(Node.u16("gp", lobbyinfo.get_int("gp")))
r.add_child(Node.u8_array("ipn", lobbyinfo.get_int_array("la", 4)))
r.add_child(Node.u8_array("pnid", lobbyinfo.get_int_array("pnid", 16)))
slotid = slotid + 1
# Stamps
stamp = Node.void("stamp")
stampdict = profile.get_dict("stamp")
pdata.add_child(stamp)
stamp.add_child(Node.s32_array("stmpcnt", stampdict.get_int_array("stmpcnt", 10)))
stamp.add_child(Node.s64("area", stampdict.get_int("area")))
stamp.add_child(Node.s64("prfvst", stampdict.get_int("prfvst")))
# Configuration
configdict = profile.get_dict("config")
config = Node.void("config")
pdata.add_child(config)
config.add_child(Node.u8("msel_bgm", configdict.get_int("msel_bgm")))
config.add_child(Node.u8("narrowdown_type", configdict.get_int("narrowdown_type")))
config.add_child(Node.s16("icon_id", configdict.get_int("icon_id")))
config.add_child(Node.s16("byword_0", configdict.get_int("byword_0")))
config.add_child(Node.s16("byword_1", configdict.get_int("byword_1")))
config.add_child(Node.bool("is_auto_byword_0", configdict.get_bool("is_auto_byword_0")))
config.add_child(Node.bool("is_auto_byword_1", configdict.get_bool("is_auto_byword_1")))
config.add_child(Node.u8("mrec_type", configdict.get_int("mrec_type")))
config.add_child(Node.u8("tab_sel", configdict.get_int("tab_sel")))
config.add_child(Node.u8("card_disp", configdict.get_int("card_disp")))
config.add_child(Node.u8("score_tab_disp", configdict.get_int("score_tab_disp")))
config.add_child(Node.s16("last_music_id", configdict.get_int("last_music_id", -1)))
config.add_child(Node.u8("last_note_grade", configdict.get_int("last_note_grade")))
config.add_child(Node.u8("sort_type", configdict.get_int("sort_type")))
config.add_child(Node.u8("rival_panel_type", configdict.get_int("rival_panel_type")))
config.add_child(Node.u64("random_entry_work", configdict.get_int("random_entry_work")))
config.add_child(Node.u64("custom_folder_work", configdict.get_int("custom_folder_work")))
config.add_child(Node.u8("folder_type", configdict.get_int("folder_type")))
config.add_child(Node.u8("folder_lamp_type", configdict.get_int("folder_lamp_type")))
config.add_child(Node.bool("is_tweet", configdict.get_bool("is_tweet")))
config.add_child(Node.bool("is_link_twitter", configdict.get_bool("is_link_twitter")))
# Customizations
customdict = profile.get_dict("custom")
custom = Node.void("custom")
pdata.add_child(custom)
custom.add_child(Node.u8("st_shot", customdict.get_int("st_shot")))
custom.add_child(Node.u8("st_frame", customdict.get_int("st_frame")))
custom.add_child(Node.u8("st_expl", customdict.get_int("st_expl")))
custom.add_child(Node.u8("st_bg", customdict.get_int("st_bg")))
custom.add_child(Node.u8("st_shot_vol", customdict.get_int("st_shot_vol")))
custom.add_child(Node.u8("st_bg_bri", customdict.get_int("st_bg_bri")))
custom.add_child(Node.u8("st_obj_size", customdict.get_int("st_obj_size")))
custom.add_child(Node.u8("st_jr_gauge", customdict.get_int("st_jr_gauge")))
custom.add_child(Node.u8("st_clr_gauge", customdict.get_int("st_clr_gauge")))
custom.add_child(Node.u8("st_jdg_disp", customdict.get_int("st_jdg_disp")))
custom.add_child(Node.u8("st_tm_disp", customdict.get_int("st_tm_disp")))
custom.add_child(Node.u8("st_rnd", customdict.get_int("st_rnd")))
custom.add_child(Node.u8("st_hazard", customdict.get_int("st_hazard")))
custom.add_child(Node.u8("st_clr_cond", customdict.get_int("st_clr_cond")))
custom.add_child(Node.s16_array("schat_0", customdict.get_int_array("schat_0", 10)))
custom.add_child(Node.s16_array("schat_1", customdict.get_int_array("schat_1", 10)))
custom.add_child(Node.u8("cheer_voice", customdict.get_int("cheer_voice")))
custom.add_child(Node.u8("same_time_note_disp", customdict.get_int("same_time_note_disp")))
# Unlocks
released = Node.void("released")
pdata.add_child(released)
for item in achievements:
if item.type[:5] != "item_":
continue
itemtype = int(item.type[5:])
if game_config.get_bool("force_unlock_songs") and itemtype == 0:
# Don't echo unlocks when we're force unlocking, we'll do it later
continue
info = Node.void("info")
released.add_child(info)
info.add_child(Node.u8("type", itemtype))
info.add_child(Node.u16("id", item.id))
info.add_child(Node.u16("param", item.data.get_int("param")))
if game_config.get_bool("force_unlock_songs"):
ids: Dict[int, int] = {}
songs = self.data.local.music.get_all_songs(self.game, self.version)
for song in songs:
if song.id not in ids:
ids[song.id] = 0
if song.data.get_int("difficulty") > 0:
ids[song.id] = ids[song.id] | (1 << song.chart)
for songid in ids:
if ids[songid] == 0:
continue
info = Node.void("info")
released.add_child(info)
info.add_child(Node.u8("type", 0))
info.add_child(Node.u16("id", songid))
info.add_child(Node.u16("param", ids[songid]))
# Announcements
announce = Node.void("announce")
pdata.add_child(announce)
for announcement in achievements:
if announcement.type[:13] != "announcement_":
continue
announcementtype = int(announcement.type[13:])
info = Node.void("info")
announce.add_child(info)
info.add_child(Node.u8("type", announcementtype))
info.add_child(Node.u16("id", announcement.id))
info.add_child(Node.u16("param", announcement.data.get_int("param")))
info.add_child(Node.bool("bneedannounce", announcement.data.get_bool("need")))
# Dojo ranking return
dojo = Node.void("dojo")
pdata.add_child(dojo)
for entry in achievements:
if entry.type != "dojo":
continue
rec = Node.void("rec")
dojo.add_child(rec)
rec.add_child(Node.s32("class", entry.id))
rec.add_child(Node.s32("clear_type", entry.data.get_int("clear_type")))
rec.add_child(Node.s32("total_ar", entry.data.get_int("ar")))
rec.add_child(Node.s32("total_score", entry.data.get_int("score")))
rec.add_child(Node.s32("play_count", entry.data.get_int("plays")))
rec.add_child(Node.s32("last_play_time", entry.data.get_int("play_timestamp")))
rec.add_child(Node.s32("record_update_time", entry.data.get_int("record_timestamp")))
rec.add_child(Node.s32("rank", 0))
# Player Parameters
player_param = Node.void("player_param")
pdata.add_child(player_param)
for param in achievements:
if param.type[:13] != "player_param_":
continue
itemtype = int(param.type[13:])
itemnode = Node.void("item")
player_param.add_child(itemnode)
itemnode.add_child(Node.s32("type", itemtype))
itemnode.add_child(Node.s32("bank", param.id))
itemnode.add_child(Node.s32_array("data", param.data.get_int_array("data", 256)))
# Shop score for players
self.__add_shop_score(pdata)
# Quest data
questdict = profile.get_dict("quest")
quest = Node.void("quest")
pdata.add_child(quest)
quest.add_child(Node.s16("eye_color", questdict.get_int("eye_color")))
quest.add_child(Node.s16("body_color", questdict.get_int("body_color")))
quest.add_child(Node.s16("item", questdict.get_int("item")))
quest.add_child(Node.string("comment", ""))
# Derby settings
derby = Node.void("derby")
pdata.add_child(derby)
derby.add_child(Node.bool("is_open", False))
# Codebreaking stuff
codebreaking = Node.void("codebreaking")
pdata.add_child(codebreaking)
codebreaking.add_child(Node.s32("cb_id", -1))
codebreaking.add_child(Node.s32("cb_sub_id", -1))
codebreaking.add_child(Node.s32("music_id", -1))
codebreaking.add_child(Node.string("question", ""))
# Unknown IIDX link crap
iidx_linkage = Node.void("iidx_linkage")
pdata.add_child(iidx_linkage)
iidx_linkage.add_child(Node.s32("linkage_id", -1))
iidx_linkage.add_child(Node.s32("phase", -1))
iidx_linkage.add_child(Node.s64("long_bit_0", -1))
iidx_linkage.add_child(Node.s64("long_bit_1", -1))
iidx_linkage.add_child(Node.s64("long_bit_2", -1))
iidx_linkage.add_child(Node.s64("long_bit_3", -1))
iidx_linkage.add_child(Node.s64("long_bit_4", -1))
iidx_linkage.add_child(Node.s64("long_bit_5", -1))
iidx_linkage.add_child(Node.s32("add_0", -1))
iidx_linkage.add_child(Node.s32("add_1", -1))
iidx_linkage.add_child(Node.s32("add_2", -1))
iidx_linkage.add_child(Node.s32("add_3", -1))
# Unknown event crap
pue = Node.void("pue")
pdata.add_child(pue)
pue.add_child(Node.s32("event_id", -1))
pue.add_child(Node.s32("point", -1))
pue.add_child(Node.s32("value0", -1))
pue.add_child(Node.s32("value1", -1))
pue.add_child(Node.s32("value2", -1))
pue.add_child(Node.s32("value3", -1))
pue.add_child(Node.s32("value4", -1))
pue.add_child(Node.s32("start_time", -1))
pue.add_child(Node.s32("end_time", -1))
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
game_config = self.get_game_config()
newprofile = oldprofile.clone()
# Save base player profile info
newprofile.replace_int("lid", ID.parse_machine_id(request.child_value("pdata/account/lid")))
newprofile.replace_str("name", request.child_value("pdata/base/name"))
newprofile.replace_int("exp", request.child_value("pdata/base/exp"))
newprofile.replace_int("lvl", request.child_value("pdata/base/lvl"))
newprofile.replace_int("mg", request.child_value("pdata/base/mg"))
newprofile.replace_int("ap", request.child_value("pdata/base/ap"))
newprofile.replace_int("money", request.child_value("pdata/base/money"))
newprofile.replace_int("class", request.child_value("pdata/base/class"))
newprofile.replace_int("class_ar", request.child_value("pdata/base/class_ar"))
newprofile.replace_int("upper_pt", request.child_value("pdata/base/upper_pt"))
# Save stamps
stampdict = newprofile.get_dict("stamp")
stamp = request.child("pdata/stamp")
if stamp:
stampdict.replace_int_array("stmpcnt", 10, stamp.child_value("stmpcnt"))
stampdict.replace_int("area", stamp.child_value("area"))
stampdict.replace_int("prfvst", stamp.child_value("prfvst"))
newprofile.replace_dict("stamp", stampdict)
# Save quest stuff
questdict = newprofile.get_dict("quest")
quest = request.child("pdata/quest")
if quest:
questdict.replace_int("eye_color", quest.child_value("eye_color"))
questdict.replace_int("body_color", quest.child_value("body_color"))
questdict.replace_int("item", quest.child_value("item"))
newprofile.replace_dict("quest", questdict)
# Save player dojo
dojo = request.child("pdata/dojo")
if dojo:
dojoid = dojo.child_value("class")
clear_type = dojo.child_value("clear_type")
ar = dojo.child_value("t_ar")
score = dojo.child_value("t_score")
# Figure out timestamp stuff
data = (
self.data.local.user.get_achievement(
self.game,
self.version,
userid,
dojoid,
"dojo",
)
or ValidatedDict()
)
if ar >= data.get_int("ar"):
# We set a new achievement rate, keep the new values
record_time = Time.now()
else:
# We didn't, keep the old values for achievement rate, but
# override score and clear_type only if they were better.
record_time = data.get_int("record_timestamp")
ar = data.get_int("ar")
score = max(score, data.get_int("score"))
clear_type = max(clear_type, data.get_int("clear_type"))
play_time = Time.now()
plays = data.get_int("plays") + 1
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
dojoid,
"dojo",
{
"clear_type": clear_type,
"ar": ar,
"score": score,
"plays": plays,
"play_timestamp": play_time,
"record_timestamp": record_time,
},
)
# Save player config
configdict = newprofile.get_dict("config")
config = request.child("pdata/config")
if config:
configdict.replace_int("msel_bgm", config.child_value("msel_bgm"))
configdict.replace_int("narrowdown_type", config.child_value("narrowdown_type"))
configdict.replace_int("icon_id", config.child_value("icon_id"))
configdict.replace_int("byword_0", config.child_value("byword_0"))
configdict.replace_int("byword_1", config.child_value("byword_1"))
configdict.replace_bool("is_auto_byword_0", config.child_value("is_auto_byword_0"))
configdict.replace_bool("is_auto_byword_1", config.child_value("is_auto_byword_1"))
configdict.replace_int("mrec_type", config.child_value("mrec_type"))
configdict.replace_int("tab_sel", config.child_value("tab_sel"))
configdict.replace_int("card_disp", config.child_value("card_disp"))
configdict.replace_int("score_tab_disp", config.child_value("score_tab_disp"))
configdict.replace_int("last_music_id", config.child_value("last_music_id"))
configdict.replace_int("last_note_grade", config.child_value("last_note_grade"))
configdict.replace_int("sort_type", config.child_value("sort_type"))
configdict.replace_int("rival_panel_type", config.child_value("rival_panel_type"))
configdict.replace_int("random_entry_work", config.child_value("random_entry_work"))
configdict.replace_int("custom_folder_work", config.child_value("custom_folder_work"))
configdict.replace_int("folder_type", config.child_value("folder_type"))
configdict.replace_int("folder_lamp_type", config.child_value("folder_lamp_type"))
configdict.replace_bool("is_tweet", config.child_value("is_tweet"))
configdict.replace_bool("is_link_twitter", config.child_value("is_link_twitter"))
newprofile.replace_dict("config", configdict)
# Save player custom settings
customdict = newprofile.get_dict("custom")
custom = request.child("pdata/custom")
if custom:
customdict.replace_int("st_shot", custom.child_value("st_shot"))
customdict.replace_int("st_frame", custom.child_value("st_frame"))
customdict.replace_int("st_expl", custom.child_value("st_expl"))
customdict.replace_int("st_bg", custom.child_value("st_bg"))
customdict.replace_int("st_shot_vol", custom.child_value("st_shot_vol"))
customdict.replace_int("st_bg_bri", custom.child_value("st_bg_bri"))
customdict.replace_int("st_obj_size", custom.child_value("st_obj_size"))
customdict.replace_int("st_jr_gauge", custom.child_value("st_jr_gauge"))
customdict.replace_int("st_clr_gauge", custom.child_value("st_clr_gauge"))
customdict.replace_int("st_jdg_disp", custom.child_value("st_jdg_disp"))
customdict.replace_int("st_tm_disp", custom.child_value("st_tm_disp"))
customdict.replace_int("st_rnd", custom.child_value("st_rnd"))
customdict.replace_int("st_hazard", custom.child_value("st_hazard"))
customdict.replace_int("st_clr_cond", custom.child_value("st_clr_cond"))
customdict.replace_int_array("schat_0", 10, custom.child_value("schat_0"))
customdict.replace_int_array("schat_1", 10, custom.child_value("schat_1"))
customdict.replace_int("cheer_voice", custom.child_value("cheer_voice"))
customdict.replace_int("same_time_note_disp", custom.child_value("same_time_note_disp"))
newprofile.replace_dict("custom", customdict)
# Save player parameter info
params = request.child("pdata/player_param")
if params:
for child in params.children:
if child.name != "item":
continue
item_type = child.child_value("type")
bank = child.child_value("bank")
paramdata = child.child_value("data") or []
while len(paramdata) < 256:
paramdata.append(0)
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
bank,
f"player_param_{item_type}",
{
"data": paramdata,
},
)
# Save player episode info
episode = request.child("pdata/episode")
if episode:
for child in episode.children:
if child.name != "info":
continue
# I assume this is copypasta, but I want to be sure
extid = child.child_value("user_id")
if extid != newprofile.extid:
raise Exception(f"Unexpected user ID, got {extid} expecting {newprofile.extid}")
episode_type = child.child_value("type")
episode_value0 = child.child_value("value0")
episode_value1 = child.child_value("value1")
episode_text = child.child_value("text")
episode_time = child.child_value("time")
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
episode_type,
"episode",
{
"value0": episode_value0,
"value1": episode_value1,
"text": episode_text,
"time": episode_time,
},
)
# Save released info
released = request.child("pdata/released")
if released:
for child in released.children:
if child.name != "info":
continue
item_id = child.child_value("id")
item_type = child.child_value("type")
param = child.child_value("param")
if game_config.get_bool("force_unlock_songs") and item_type == 0:
# Don't save unlocks when we're force unlocking
continue
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
item_id,
f"item_{item_type}",
{
"param": param,
},
)
# Save announce info
announce = request.child("pdata/announce")
if announce:
for child in announce.children:
if child.name != "info":
continue
announce_id = child.child_value("id")
announce_type = child.child_value("type")
param = child.child_value("param")
need = child.child_value("bneedannounce")
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
announce_id,
f"announcement_{announce_type}",
{
"param": param,
"need": need,
},
)
# Grab any new rivals added during this play session
rivalnode = request.child("pdata/rival")
if rivalnode:
for child in rivalnode.children:
if child.name != "r":
continue
extid = child.child_value("id")
other_userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if other_userid is None:
continue
self.data.local.user.put_link(
self.game,
self.version,
userid,
"rival",
other_userid,
{},
)
# Grab any new records set during this play session
songplays = request.child("pdata/stglog")
if songplays:
for child in songplays.children:
if child.name != "log":
continue
songid = child.child_value("mid")
chart = child.child_value("ng")
clear_type = child.child_value("ct")
if songid == 0 and chart == 0 and clear_type == -1:
# Dummy song save during profile create
continue
points = child.child_value("sc")
achievement_rate = child.child_value("ar")
param = child.child_value("param")
miss_count = child.child_value("jt_ms")
# Param is some random bits along with the combo type
combo_type = param & 0x3
param = param ^ combo_type
clear_type = self.__game_to_db_clear_type(clear_type)
combo_type = self.__game_to_db_combo_type(combo_type, miss_count)
self.update_score(
userid,
songid,
chart,
points,
achievement_rate,
clear_type,
combo_type,
miss_count,
param=param,
)
# Keep track of play statistics
self.update_play_statistics(userid)
return newprofile