1513 lines
65 KiB
Python
1513 lines
65 KiB
Python
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": "PLAYER"},
|
||
)
|
||
|
||
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
|