1
0
mirror of synced 2024-11-30 16:54:30 +01:00
bemaniutils/bemani/backend/ddr/ddrx2.py
2024-01-02 02:46:24 +00:00

781 lines
32 KiB
Python

# vim: set fileencoding=utf-8
from typing import Dict, List, Optional, Tuple
from typing_extensions import Final
from bemani.backend.ddr.base import DDRBase
from bemani.backend.ddr.stubs import DDRX
from bemani.backend.ddr.common import (
DDRGameFriendHandler,
DDRGameLockHandler,
DDRGameLoadCourseHandler,
DDRGameLoadHandler,
DDRGameLogHandler,
DDRGameMessageHandler,
DDRGameNewHandler,
DDRGameOldHandler,
DDRGameRankingHandler,
DDRGameSaveCourseHandler,
DDRGameSaveHandler,
DDRGameScoreHandler,
DDRGameShopHandler,
DDRGameTraceHandler,
)
from bemani.common import Time, VersionConstants, Profile, intish
from bemani.data import Score, UserID
from bemani.protocol import Node
class DDRX2(
DDRGameFriendHandler,
DDRGameLockHandler,
DDRGameLoadCourseHandler,
DDRGameLoadHandler,
DDRGameLogHandler,
DDRGameMessageHandler,
DDRGameOldHandler,
DDRGameNewHandler,
DDRGameRankingHandler,
DDRGameSaveCourseHandler,
DDRGameSaveHandler,
DDRGameScoreHandler,
DDRGameShopHandler,
DDRGameTraceHandler,
DDRBase,
):
name: str = "DanceDanceRevolution X2"
version: int = VersionConstants.DDR_X2
GAME_STYLE_SINGLE: Final[int] = 0
GAME_STYLE_DOUBLE: Final[int] = 1
GAME_STYLE_VERSUS: Final[int] = 2
GAME_RANK_AAA: Final[int] = 1
GAME_RANK_AA: Final[int] = 2
GAME_RANK_A: Final[int] = 3
GAME_RANK_B: Final[int] = 4
GAME_RANK_C: Final[int] = 5
GAME_RANK_D: Final[int] = 6
GAME_RANK_E: Final[int] = 7
GAME_CHART_SINGLE_BEGINNER: Final[int] = 0
GAME_CHART_SINGLE_BASIC: Final[int] = 1
GAME_CHART_SINGLE_DIFFICULT: Final[int] = 2
GAME_CHART_SINGLE_EXPERT: Final[int] = 3
GAME_CHART_SINGLE_CHALLENGE: Final[int] = 4
GAME_CHART_DOUBLE_BASIC: Final[int] = 5
GAME_CHART_DOUBLE_DIFFICULT: Final[int] = 6
GAME_CHART_DOUBLE_EXPERT: Final[int] = 7
GAME_CHART_DOUBLE_CHALLENGE: Final[int] = 8
GAME_HALO_NONE: Final[int] = 0
GAME_HALO_FULL_COMBO: Final[int] = 1
GAME_HALO_PERFECT_COMBO: Final[int] = 2
GAME_HALO_MARVELOUS_COMBO: Final[int] = 3
GAME_MAX_SONGS: Final[int] = 600
def previous_version(self) -> Optional[DDRBase]:
return DDRX(self.data, self.config, self.model)
def game_to_db_rank(self, game_rank: int) -> int:
return {
self.GAME_RANK_AAA: self.RANK_AAA,
self.GAME_RANK_AA: self.RANK_AA,
self.GAME_RANK_A: self.RANK_A,
self.GAME_RANK_B: self.RANK_B,
self.GAME_RANK_C: self.RANK_C,
self.GAME_RANK_D: self.RANK_D,
self.GAME_RANK_E: self.RANK_E,
}[game_rank]
def db_to_game_rank(self, db_rank: int) -> int:
return {
self.RANK_AAA: self.GAME_RANK_AAA,
self.RANK_AA_PLUS: self.GAME_RANK_AA,
self.RANK_AA: self.GAME_RANK_AA,
self.RANK_AA_MINUS: self.GAME_RANK_A,
self.RANK_A_PLUS: self.GAME_RANK_A,
self.RANK_A: self.GAME_RANK_A,
self.RANK_A_MINUS: self.GAME_RANK_B,
self.RANK_B_PLUS: self.GAME_RANK_B,
self.RANK_B: self.GAME_RANK_B,
self.RANK_B_MINUS: self.GAME_RANK_C,
self.RANK_C_PLUS: self.GAME_RANK_C,
self.RANK_C: self.GAME_RANK_C,
self.RANK_C_MINUS: self.GAME_RANK_D,
self.RANK_D_PLUS: self.GAME_RANK_D,
self.RANK_D: self.GAME_RANK_D,
self.RANK_E: self.GAME_RANK_E,
}[db_rank]
def game_to_db_chart(self, game_chart: int) -> int:
return {
self.GAME_CHART_SINGLE_BEGINNER: self.CHART_SINGLE_BEGINNER,
self.GAME_CHART_SINGLE_BASIC: self.CHART_SINGLE_BASIC,
self.GAME_CHART_SINGLE_DIFFICULT: self.CHART_SINGLE_DIFFICULT,
self.GAME_CHART_SINGLE_EXPERT: self.CHART_SINGLE_EXPERT,
self.GAME_CHART_SINGLE_CHALLENGE: self.CHART_SINGLE_CHALLENGE,
self.GAME_CHART_DOUBLE_BASIC: self.CHART_DOUBLE_BASIC,
self.GAME_CHART_DOUBLE_DIFFICULT: self.CHART_DOUBLE_DIFFICULT,
self.GAME_CHART_DOUBLE_EXPERT: self.CHART_DOUBLE_EXPERT,
self.GAME_CHART_DOUBLE_CHALLENGE: self.CHART_DOUBLE_CHALLENGE,
}[game_chart]
def db_to_game_chart(self, db_chart: int) -> int:
return {
self.CHART_SINGLE_BEGINNER: self.GAME_CHART_SINGLE_BEGINNER,
self.CHART_SINGLE_BASIC: self.GAME_CHART_SINGLE_BASIC,
self.CHART_SINGLE_DIFFICULT: self.GAME_CHART_SINGLE_DIFFICULT,
self.CHART_SINGLE_EXPERT: self.GAME_CHART_SINGLE_EXPERT,
self.CHART_SINGLE_CHALLENGE: self.GAME_CHART_SINGLE_CHALLENGE,
self.CHART_DOUBLE_BASIC: self.GAME_CHART_DOUBLE_BASIC,
self.CHART_DOUBLE_DIFFICULT: self.GAME_CHART_DOUBLE_DIFFICULT,
self.CHART_DOUBLE_EXPERT: self.GAME_CHART_DOUBLE_EXPERT,
self.CHART_DOUBLE_CHALLENGE: self.GAME_CHART_DOUBLE_CHALLENGE,
}[db_chart]
def db_to_game_halo(self, db_halo: int) -> int:
if db_halo == self.HALO_MARVELOUS_FULL_COMBO:
combo_type = self.GAME_HALO_MARVELOUS_COMBO
elif db_halo == self.HALO_PERFECT_FULL_COMBO:
combo_type = self.GAME_HALO_PERFECT_COMBO
elif db_halo == self.HALO_GREAT_FULL_COMBO:
combo_type = self.GAME_HALO_FULL_COMBO
else:
combo_type = self.GAME_HALO_NONE
return combo_type
def handle_game_common_request(self, request: Node) -> Node:
game = Node.void("game")
for flagid in range(256):
flag = Node.void("flag")
game.add_child(flag)
flag.set_attribute("id", str(flagid))
flag.set_attribute("s2", "0")
flag.set_attribute("s1", "0")
flag.set_attribute("t", "0")
hit_chart = self.data.local.music.get_hit_chart(self.game, self.music_version, self.GAME_MAX_SONGS)
counts_by_reflink = [0] * self.GAME_MAX_SONGS
for reflink, plays in hit_chart:
if reflink >= 0 and reflink < self.GAME_MAX_SONGS:
counts_by_reflink[reflink] = plays
game.add_child(Node.u32_array("cnt_music", counts_by_reflink))
return game
def handle_game_hiscore_request(self, request: Node) -> Node:
# This is almost identical to X3 and above, except X3 added a 'code' field
# that isn't present here. In the interest of correctness, keep a separate
# implementation here.
records = self.data.remote.music.get_all_records(self.game, self.music_version)
sortedrecords: Dict[int, Dict[int, Tuple[UserID, Score]]] = {}
missing_profiles = []
for userid, score in records:
if score.id not in sortedrecords:
sortedrecords[score.id] = {}
sortedrecords[score.id][score.chart] = (userid, score)
missing_profiles.append(userid)
users = {userid: profile for (userid, profile) in self.get_any_profiles(missing_profiles)}
game = Node.void("game")
for song in sortedrecords:
music = Node.void("music")
game.add_child(music)
music.set_attribute("reclink_num", str(song))
for chart in sortedrecords[song]:
userid, score = sortedrecords[song][chart]
try:
gamechart = self.db_to_game_chart(chart)
except KeyError:
# Don't support this chart in this game
continue
gamerank = self.db_to_game_rank(score.data.get_int("rank"))
combo_type = self.db_to_game_halo(score.data.get_int("halo"))
typenode = Node.void("type")
music.add_child(typenode)
typenode.set_attribute("diff", str(gamechart))
typenode.add_child(Node.string("name", users[userid].get_str("name")))
typenode.add_child(Node.u32("score", score.points))
typenode.add_child(Node.u16("area", users[userid].get_int("area", self.get_machine_region())))
typenode.add_child(Node.u8("rank", gamerank))
typenode.add_child(Node.u8("combo_type", combo_type))
return game
def handle_game_load_m_request(self, request: Node) -> Node:
extid = intish(request.attribute("code"))
refid = request.attribute("refid")
if extid is not None:
# Rival score loading
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
else:
# Self score loading
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
scores = self.data.remote.music.get_scores(self.game, self.music_version, userid)
else:
scores = []
sortedscores: Dict[int, Dict[int, Score]] = {}
for score in scores:
if score.id not in sortedscores:
sortedscores[score.id] = {}
sortedscores[score.id][score.chart] = score
game = Node.void("game")
for song in sortedscores:
music = Node.void("music")
game.add_child(music)
music.set_attribute("reclink", str(song))
for chart in sortedscores[song]:
score = sortedscores[song][chart]
try:
gamechart = self.db_to_game_chart(chart)
except KeyError:
# Don't support this chart in this game
continue
gamerank = self.db_to_game_rank(score.data.get_int("rank"))
combo_type = self.db_to_game_halo(score.data.get_int("halo"))
typenode = Node.void("type")
music.add_child(typenode)
typenode.set_attribute("diff", str(gamechart))
typenode.add_child(Node.u32("score", score.points))
typenode.add_child(Node.u16("count", score.plays))
typenode.add_child(Node.u8("rank", gamerank))
typenode.add_child(Node.u8("combo_type", combo_type))
return game
def handle_game_save_m_request(self, request: Node) -> Node:
refid = request.attribute("refid")
songid = int(request.attribute("mid"))
chart = self.game_to_db_chart(int(request.attribute("mtype")))
# Calculate statistics
data = request.child("data")
points = int(data.attribute("score"))
combo = int(data.attribute("combo"))
rank = self.game_to_db_rank(int(data.attribute("rank")))
if int(data.attribute("full")) == 0:
halo = self.HALO_NONE
elif int(data.attribute("perf")) == 0:
halo = self.HALO_GREAT_FULL_COMBO
elif points < 1000000:
halo = self.HALO_PERFECT_FULL_COMBO
else:
halo = self.HALO_MARVELOUS_FULL_COMBO
trace = request.child_value("trace")
# Save the score, regardless of whether we have a refid. If we save
# an anonymous score, it only goes into the DB to count against the
# number of plays for that song/chart.
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
self.update_score(
userid,
songid,
chart,
points,
rank,
halo,
combo,
trace,
)
# No response needed
game = Node.void("game")
return game
def format_profile(self, userid: UserID, profile: Profile) -> Node:
root = Node.void("game")
# Look up play stats we bridge to every mix
play_stats = self.get_play_statistics(userid)
# Basic game settings
root.add_child(Node.string("seq", ""))
root.add_child(Node.u32("code", profile.extid))
root.add_child(Node.string("name", profile.get_str("name")))
root.add_child(Node.u8("area", profile.get_int("area", self.get_machine_region())))
root.add_child(Node.u32("cnt_s", play_stats.get_int("single_plays")))
root.add_child(Node.u32("cnt_d", play_stats.get_int("double_plays")))
root.add_child(Node.u32("cnt_b", play_stats.get_int("battle_plays"))) # This could be wrong, its a guess
root.add_child(Node.u32("cnt_m0", play_stats.get_int("cnt_m0")))
root.add_child(Node.u32("cnt_m1", play_stats.get_int("cnt_m1")))
root.add_child(Node.u32("cnt_m2", play_stats.get_int("cnt_m2")))
root.add_child(Node.u32("cnt_m3", play_stats.get_int("cnt_m3")))
root.add_child(Node.u32("exp", play_stats.get_int("exp")))
root.add_child(Node.u32("exp_o", profile.get_int("exp_o")))
root.add_child(Node.u32("star", profile.get_int("star")))
root.add_child(Node.u32("star_c", profile.get_int("star_c")))
root.add_child(Node.u8("combo", profile.get_int("combo", 0)))
root.add_child(Node.u8("timing_diff", profile.get_int("early_late", 0)))
# Character stuff
chara = Node.void("chara")
root.add_child(chara)
if "chara" in profile:
chara.set_attribute("my", str(profile.get_int("chara")))
root.add_child(Node.u8_array("chara_opt", profile.get_int_array("chara_opt", 96)))
# Drill rankings
if "title" in profile:
title = Node.void("title")
root.add_child(title)
titledict = profile.get_dict("title")
if "t" in titledict:
title.set_attribute("t", str(titledict.get_int("t")))
if "s" in titledict:
title.set_attribute("s", str(titledict.get_int("s")))
if "d" in titledict:
title.set_attribute("d", str(titledict.get_int("d")))
if "title_gr" in profile:
title_gr = Node.void("title_gr")
root.add_child(title_gr)
title_grdict = profile.get_dict("title_gr")
if "t" in title_grdict:
title_gr.set_attribute("t", str(title_grdict.get_int("t")))
if "s" in title_grdict:
title_gr.set_attribute("s", str(title_grdict.get_int("s")))
if "d" in title_grdict:
title_gr.set_attribute("d", str(title_grdict.get_int("d")))
# Event progrses
if "event" in profile:
event = Node.void("event")
root.add_child(event)
event_dict = profile.get_dict("event")
if "diff_sum" in event_dict:
event.set_attribute("diff_sum", str(event_dict.get_int("diff_sum")))
if "welcome" in event_dict:
event.set_attribute("welcome", str(event_dict.get_int("welcome")))
if "e_flags" in event_dict:
event.set_attribute("e_flags", str(event_dict.get_int("e_flags")))
if "e_panel" in profile:
e_panel = Node.void("e_panel")
root.add_child(e_panel)
e_panel_dict = profile.get_dict("e_panel")
if "play_id" in e_panel_dict:
e_panel.set_attribute("play_id", str(e_panel_dict.get_int("play_id")))
e_panel.add_child(Node.u8_array("cell", e_panel_dict.get_int_array("cell", 24)))
e_panel.add_child(Node.u8_array("panel_state", e_panel_dict.get_int_array("panel_state", 6)))
if "e_pix" in profile:
e_pix = Node.void("e_pix")
root.add_child(e_pix)
e_pix_dict = profile.get_dict("e_pix")
if "max_distance" in e_pix_dict:
e_pix.set_attribute("max_distance", str(e_pix_dict.get_int("max_distance")))
if "max_planet" in e_pix_dict:
e_pix.set_attribute("max_planet", str(e_pix_dict.get_int("max_planet")))
if "total_distance" in e_pix_dict:
e_pix.set_attribute("total_distance", str(e_pix_dict.get_int("total_distance")))
if "total_planet" in e_pix_dict:
e_pix.set_attribute("total_planet", str(e_pix_dict.get_int("total_planet")))
if "border_character" in e_pix_dict:
e_pix.set_attribute("border_character", str(e_pix_dict.get_int("border_character")))
if "border_balloon" in e_pix_dict:
e_pix.set_attribute("border_balloon", str(e_pix_dict.get_int("border_balloon")))
if "border_music_aftr" in e_pix_dict:
e_pix.set_attribute("border_music_aftr", str(e_pix_dict.get_int("border_music_aftr")))
if "border_music_meii" in e_pix_dict:
e_pix.set_attribute("border_music_meii", str(e_pix_dict.get_int("border_music_meii")))
if "border_music_dirt" in e_pix_dict:
e_pix.set_attribute("border_music_dirt", str(e_pix_dict.get_int("border_music_dirt")))
if "flags" in e_pix_dict:
e_pix.set_attribute("flags", str(e_pix_dict.get_int("flags")))
# Calorie mode
if "weight" in profile:
workouts = self.data.local.user.get_time_based_achievements(
self.game,
self.version,
userid,
achievementtype="workout",
since=Time.now() - Time.SECONDS_IN_DAY,
)
total = sum([w.data.get_int("calories") for w in workouts])
workout = Node.void("workout")
root.add_child(workout)
workout.set_attribute("weight", str(profile.get_int("weight")))
workout.set_attribute("day", str(total))
workout.set_attribute("disp", "1")
# Last cursor settings
last = Node.void("last")
root.add_child(last)
lastdict = profile.get_dict("last")
last.set_attribute("fri", str(lastdict.get_int("fri")))
last.set_attribute("style", str(lastdict.get_int("style")))
last.set_attribute("mode", str(lastdict.get_int("mode")))
last.set_attribute("cate", str(lastdict.get_int("cate")))
last.set_attribute("sort", str(lastdict.get_int("sort")))
last.set_attribute("mid", str(lastdict.get_int("mid")))
last.set_attribute("mtype", str(lastdict.get_int("mtype")))
last.set_attribute("cid", str(lastdict.get_int("cid")))
last.set_attribute("ctype", str(lastdict.get_int("ctype")))
last.set_attribute("sid", str(lastdict.get_int("sid")))
# Groove gauge level-ups
gr_s = Node.void("gr_s")
root.add_child(gr_s)
index = 1
for entry in profile.get_int_array("gr_s", 5):
gr_s.set_attribute(f"gr{index}", str(entry))
index = index + 1
gr_d = Node.void("gr_d")
root.add_child(gr_d)
index = 1
for entry in profile.get_int_array("gr_d", 5):
gr_d.set_attribute(f"gr{index}", str(entry))
index = index + 1
# Options in menus
root.add_child(Node.s16_array("opt", profile.get_int_array("opt", 16)))
root.add_child(Node.s16_array("opt_ex", profile.get_int_array("opt_ex", 16)))
# Unlock flags
root.add_child(Node.u8_array("flag", profile.get_int_array("flag", 256, [1] * 256)))
# Ranking display?
root.add_child(Node.u16_array("rank", profile.get_int_array("rank", 100)))
# Rivals
links = self.data.local.user.get_links(self.game, self.version, userid)
for link in links:
if link.type[:7] != "friend_":
continue
pos = int(link.type[7:])
friend = self.get_profile(link.other_userid)
play_stats = self.get_play_statistics(link.other_userid)
if friend is not None:
friendnode = Node.void("friend")
root.add_child(friendnode)
friendnode.set_attribute("pos", str(pos))
friendnode.set_attribute("vs", "0")
friendnode.set_attribute("up", "0")
friendnode.add_child(Node.u32("code", friend.extid))
friendnode.add_child(Node.string("name", friend.get_str("name")))
friendnode.add_child(Node.u8("area", friend.get_int("area", self.get_machine_region())))
friendnode.add_child(Node.u32("exp", play_stats.get_int("exp")))
friendnode.add_child(Node.u32("star", friend.get_int("star")))
# Drill rankings
if "title" in friend:
title = Node.void("title")
friendnode.add_child(title)
titledict = friend.get_dict("title")
if "t" in titledict:
title.set_attribute("t", str(titledict.get_int("t")))
if "s" in titledict:
title.set_attribute("s", str(titledict.get_int("s")))
if "d" in titledict:
title.set_attribute("d", str(titledict.get_int("d")))
if "title_gr" in friend:
title_gr = Node.void("title_gr")
friendnode.add_child(title_gr)
title_grdict = friend.get_dict("title_gr")
if "t" in title_grdict:
title_gr.set_attribute("t", str(title_grdict.get_int("t")))
if "s" in title_grdict:
title_gr.set_attribute("s", str(title_grdict.get_int("s")))
if "d" in title_grdict:
title_gr.set_attribute("d", str(title_grdict.get_int("d")))
# Groove gauge level-ups
gr_s = Node.void("gr_s")
friendnode.add_child(gr_s)
index = 1
for entry in friend.get_int_array("gr_s", 5):
gr_s.set_attribute(f"gr{index}", str(entry))
index = index + 1
gr_d = Node.void("gr_d")
friendnode.add_child(gr_d)
index = 1
for entry in friend.get_int_array("gr_d", 5):
gr_d.set_attribute(f"gr{index}", str(entry))
index = index + 1
return root
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
newprofile = oldprofile.clone()
play_stats = self.get_play_statistics(userid)
# Grab last node and accessories so we can make decisions based on type
last = request.child("last")
lastdict = newprofile.get_dict("last")
mode = int(last.attribute("mode"))
style = int(last.attribute("style"))
is_dp = style == self.GAME_STYLE_DOUBLE
# Drill rankings
title = request.child("title")
title_gr = request.child("title_gr")
titledict = newprofile.get_dict("title")
title_grdict = newprofile.get_dict("title_gr")
# Groove radar level ups
gr = request.child("gr")
# Set the correct values depending on if we're single or double play
if is_dp:
play_stats.increment_int("double_plays")
if gr is not None:
newprofile.replace_int_array(
"gr_d",
5,
[
intish(gr.attribute("gr1")),
intish(gr.attribute("gr2")),
intish(gr.attribute("gr3")),
intish(gr.attribute("gr4")),
intish(gr.attribute("gr5")),
],
)
if title is not None:
titledict.replace_int("d", title.value)
newprofile.replace_dict("title", titledict)
if title_gr is not None:
title_grdict.replace_int("d", title.value)
newprofile.replace_dict("title_gr", title_grdict)
else:
play_stats.increment_int("single_plays")
if gr is not None:
newprofile.replace_int_array(
"gr_s",
5,
[
intish(gr.attribute("gr1")),
intish(gr.attribute("gr2")),
intish(gr.attribute("gr3")),
intish(gr.attribute("gr4")),
intish(gr.attribute("gr5")),
],
)
if title is not None:
titledict.replace_int("s", title.value)
newprofile.replace_dict("title", titledict)
if title_gr is not None:
title_grdict.replace_int("s", title.value)
newprofile.replace_dict("title_gr", title_grdict)
play_stats.increment_int(f"cnt_m{mode}")
# Update last attributes
lastdict.replace_int("fri", intish(last.attribute("fri")))
lastdict.replace_int("style", intish(last.attribute("style")))
lastdict.replace_int("mode", intish(last.attribute("mode")))
lastdict.replace_int("cate", intish(last.attribute("cate")))
lastdict.replace_int("sort", intish(last.attribute("sort")))
lastdict.replace_int("mid", intish(last.attribute("mid")))
lastdict.replace_int("mtype", intish(last.attribute("mtype")))
lastdict.replace_int("cid", intish(last.attribute("cid")))
lastdict.replace_int("ctype", intish(last.attribute("ctype")))
lastdict.replace_int("sid", intish(last.attribute("sid")))
newprofile.replace_dict("last", lastdict)
# Grab character options
chara = request.child("chara")
if chara is not None:
newprofile.replace_int("chara", intish(chara.attribute("my")))
newprofile.replace_int_array("chara_opt", 96, request.child_value("chara_opt"))
# Track event progress
event = request.child("event")
if event is not None:
event_dict = newprofile.get_dict("event")
event_dict.replace_int("diff_sum", intish(event.attribute("diff_sum")))
event_dict.replace_int("e_flags", intish(event.attribute("e_flags")))
event_dict.replace_int("welcome", intish(event.attribute("welcome")))
newprofile.replace_dict("event", event_dict)
e_panel = request.child("e_panel")
if e_panel is not None:
e_panel_dict = newprofile.get_dict("e_panel")
e_panel_dict.replace_int("play_id", intish(e_panel.attribute("play_id")))
e_panel_dict.replace_int_array("cell", 24, e_panel.child_value("cell"))
e_panel_dict.replace_int_array("panel_state", 6, e_panel.child_value("panel_state"))
newprofile.replace_dict("e_panel", e_panel_dict)
e_pix = request.child("e_pix")
if e_pix is not None:
e_pix_dict = newprofile.get_dict("e_pix")
max_distance = e_pix_dict.get_int("max_distance")
max_planet = e_pix_dict.get_int("max_planet")
total_distance = e_pix_dict.get_int("total_distance")
total_planet = e_pix_dict.get_int("total_planet")
cur_distance = intish(e_pix.attribute("this_distance"))
cur_planet = intish(e_pix.attribute("this_planet"))
if cur_distance is not None:
max_distance = max(max_distance, cur_distance)
total_distance += cur_distance
if cur_planet is not None:
max_planet = max(max_planet, cur_planet)
total_planet += cur_planet
e_pix_dict.replace_int("max_distance", max_distance)
e_pix_dict.replace_int("max_planet", max_planet)
e_pix_dict.replace_int("total_distance", total_distance)
e_pix_dict.replace_int("total_planet", total_planet)
e_pix_dict.replace_int("flags", intish(e_pix.attribute("flags")))
newprofile.replace_dict("e_pix", e_pix_dict)
# Options
opt = request.child("opt")
if opt is not None:
# A bug in old versions of AVS returns the wrong number for set
newprofile.replace_int_array("opt", 16, opt.value[:16])
# Experience and stars
exp = request.child_value("exp")
if exp is not None:
play_stats.replace_int("exp", play_stats.get_int("exp") + exp)
star = request.child_value("star")
if star is not None:
newprofile.replace_int("star", newprofile.get_int("star") + star)
star_c = request.child_value("star_c")
if star_c is not None:
newprofile.replace_int("star_c", newprofile.get_int("star_c") + exp)
# Update game flags
for child in request.children:
if child.name != "flag":
continue
try:
value = int(child.attribute("data"))
offset = int(child.attribute("no"))
except ValueError:
continue
flags = newprofile.get_int_array("flag", 256, [1] * 256)
if offset < 0 or offset >= len(flags):
continue
flags[offset] = value
newprofile.replace_int_array("flag", 256, flags)
# Workout mode support
newweight = -1
oldweight = newprofile.get_int("weight")
for child in request.children:
if child.name != "weight":
continue
newweight = child.value
if newweight < 0:
newweight = oldweight
# Either update or unset the weight depending on the game
if newweight == 0:
# Weight is unset or we declined to use this feature, remove from profile
if "weight" in newprofile:
del newprofile["weight"]
else:
# Weight has been set or previously retrieved, we should save calories
newprofile.replace_int("weight", newweight)
total = 0
for child in request.children:
if child.name != "calory":
continue
total += child.value
self.data.local.user.put_time_based_achievement(
self.game,
self.version,
userid,
0,
"workout",
{
"calories": total,
"weight": newweight,
},
)
# Look up old friends
oldfriends: List[Optional[UserID]] = [None] * 10
links = self.data.local.user.get_links(self.game, self.version, userid)
for link in links:
if link.type[:7] != "friend_":
continue
pos = int(link.type[7:])
oldfriends[pos] = link.other_userid
# Save any rivals that were added/removed/changed
newfriends = oldfriends[:]
for child in request.children:
if child.name != "friend":
continue
code = int(child.attribute("code"))
pos = int(child.attribute("pos"))
if pos >= 0 and pos < 10:
if code == 0:
# We cleared this friend
newfriends[pos] = None
else:
# Try looking up the userid
newfriends[pos] = self.data.remote.user.from_extid(self.game, self.version, code)
# Diff the set of links to determine updates
for i in range(10):
if newfriends[i] == oldfriends[i]:
continue
if newfriends[i] is None:
# Kill the rival in this location
self.data.local.user.destroy_link(
self.game,
self.version,
userid,
f"friend_{i}",
oldfriends[i],
)
elif oldfriends[i] is None:
# Add rival in this location
self.data.local.user.put_link(
self.game,
self.version,
userid,
f"friend_{i}",
newfriends[i],
{},
)
else:
# Changed the rival here, kill the old one, add the new one
self.data.local.user.destroy_link(
self.game,
self.version,
userid,
f"friend_{i}",
oldfriends[i],
)
self.data.local.user.put_link(
self.game,
self.version,
userid,
f"friend_{i}",
newfriends[i],
{},
)
# Keep track of play statistics
self.update_play_statistics(userid, play_stats)
return newprofile