805 lines
32 KiB
Python
805 lines
32 KiB
Python
# vim: set fileencoding=utf-8
|
|
import copy
|
|
from typing import Dict, List, Optional, Union
|
|
|
|
from bemani.backend.ddr.base import DDRBase
|
|
from bemani.backend.ddr.ddrx2 import DDRX2
|
|
from bemani.backend.ddr.common import (
|
|
DDRGameAreaHiscoreHandler,
|
|
DDRGameFriendHandler,
|
|
DDRGameHiscoreHandler,
|
|
DDRGameLoadCourseHandler,
|
|
DDRGameLoadDailyHandler,
|
|
DDRGameLoadHandler,
|
|
DDRGameLockHandler,
|
|
DDRGameLogHandler,
|
|
DDRGameMessageHandler,
|
|
DDRGameNewHandler,
|
|
DDRGameOldHandler,
|
|
DDRGameRankingHandler,
|
|
DDRGameSaveCourseHandler,
|
|
DDRGameSaveHandler,
|
|
DDRGameScoreHandler,
|
|
DDRGameShopHandler,
|
|
DDRGameTraceHandler,
|
|
)
|
|
from bemani.common import Profile, VersionConstants, Time, intish
|
|
from bemani.data import Achievement, Score, UserID
|
|
from bemani.protocol import Node
|
|
|
|
|
|
class DDRX3(
|
|
DDRGameAreaHiscoreHandler,
|
|
DDRGameFriendHandler,
|
|
DDRGameHiscoreHandler,
|
|
DDRGameLockHandler,
|
|
DDRGameLoadCourseHandler,
|
|
DDRGameLoadDailyHandler,
|
|
DDRGameLoadHandler,
|
|
DDRGameLogHandler,
|
|
DDRGameMessageHandler,
|
|
DDRGameNewHandler,
|
|
DDRGameOldHandler,
|
|
DDRGameRankingHandler,
|
|
DDRGameSaveCourseHandler,
|
|
DDRGameSaveHandler,
|
|
DDRGameScoreHandler,
|
|
DDRGameShopHandler,
|
|
DDRGameTraceHandler,
|
|
DDRBase,
|
|
):
|
|
|
|
name = 'DanceDanceRevolution X3 VS 2ndMIX'
|
|
version = VersionConstants.DDR_X3_VS_2NDMIX
|
|
|
|
GAME_STYLE_SINGLE = 0
|
|
GAME_STYLE_DOUBLE = 1
|
|
GAME_STYLE_VERSUS = 2
|
|
|
|
GAME_RANK_AAA = 1
|
|
GAME_RANK_AA = 2
|
|
GAME_RANK_A = 3
|
|
GAME_RANK_B = 4
|
|
GAME_RANK_C = 5
|
|
GAME_RANK_D = 6
|
|
GAME_RANK_E = 7
|
|
|
|
GAME_CHART_SINGLE_BEGINNER = 0
|
|
GAME_CHART_SINGLE_BASIC = 1
|
|
GAME_CHART_SINGLE_DIFFICULT = 2
|
|
GAME_CHART_SINGLE_EXPERT = 3
|
|
GAME_CHART_SINGLE_CHALLENGE = 4
|
|
GAME_CHART_DOUBLE_BASIC = 5
|
|
GAME_CHART_DOUBLE_DIFFICULT = 6
|
|
GAME_CHART_DOUBLE_EXPERT = 7
|
|
GAME_CHART_DOUBLE_CHALLENGE = 8
|
|
|
|
GAME_HALO_NONE = 0
|
|
GAME_HALO_FULL_COMBO = 1
|
|
GAME_HALO_PERFECT_COMBO = 2
|
|
GAME_HALO_MARVELOUS_COMBO = 3
|
|
|
|
GAME_PLAY_MODE_NORMAL = 1
|
|
GAME_PLAY_MODE_2NDMIX = 5
|
|
|
|
GAME_MAX_SONGS = 700
|
|
|
|
def previous_version(self) -> Optional[DDRBase]:
|
|
return DDRX2(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_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)
|
|
old_scores = [
|
|
score for score in self.data.local.user.get_achievements(self.game, self.music_version, userid)
|
|
if score.type == '2ndmix'
|
|
]
|
|
else:
|
|
scores = []
|
|
old_scores = []
|
|
|
|
sortedscores: Dict[int, Dict[int, Dict[str, Union[Score, Achievement]]]] = {}
|
|
|
|
for score in scores:
|
|
if score.id not in sortedscores:
|
|
sortedscores[score.id] = {}
|
|
if score.chart not in sortedscores[score.id]:
|
|
sortedscores[score.id][score.chart] = {}
|
|
sortedscores[score.id][score.chart]['score'] = score
|
|
|
|
for oldscore in old_scores:
|
|
songid = int(oldscore.id / 100)
|
|
chart = int(oldscore.id % 100)
|
|
if songid not in sortedscores:
|
|
sortedscores[songid] = {}
|
|
if chart not in sortedscores[songid]:
|
|
sortedscores[songid][chart] = {}
|
|
sortedscores[songid][chart]['oldscore'] = oldscore
|
|
|
|
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]:
|
|
try:
|
|
gamechart = self.db_to_game_chart(chart)
|
|
except KeyError:
|
|
# Don't support this chart in this game
|
|
continue
|
|
scoredict = sortedscores[song][chart]
|
|
|
|
if 'score' in scoredict:
|
|
# We played the normal version of this song
|
|
gamerank = self.db_to_game_rank(scoredict['score'].data.get_int('rank'))
|
|
combo_type = self.db_to_game_halo(scoredict['score'].data.get_int('halo'))
|
|
points = scoredict['score'].points # type: ignore
|
|
plays = scoredict['score'].plays # type: ignore
|
|
else:
|
|
# We only played 2nd mix version of this song
|
|
gamerank = 0
|
|
combo_type = self.GAME_HALO_NONE
|
|
points = 0
|
|
plays = 0
|
|
|
|
if 'oldscore' in scoredict:
|
|
# We played the 2nd mix version of this song
|
|
oldpoints = scoredict['oldscore'].data.get_int('points')
|
|
oldrank = scoredict['oldscore'].data.get_int('rank')
|
|
oldplays = scoredict['oldscore'].data.get_int('plays')
|
|
else:
|
|
oldpoints = 0
|
|
oldrank = 0
|
|
oldplays = 0
|
|
|
|
typenode = Node.void('type')
|
|
music.add_child(typenode)
|
|
typenode.set_attribute('diff', str(gamechart))
|
|
|
|
typenode.add_child(Node.u32('score', points))
|
|
typenode.add_child(Node.u16('count', plays))
|
|
typenode.add_child(Node.u8('rank', gamerank))
|
|
typenode.add_child(Node.u8('combo_type', combo_type))
|
|
typenode.add_child(Node.u32('score_2nd', oldpoints))
|
|
typenode.add_child(Node.u8('rank_2nd', oldrank))
|
|
typenode.add_child(Node.u16('cnt_2nd', oldplays))
|
|
|
|
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')))
|
|
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
|
|
|
# Calculate statistics
|
|
data = request.child('data')
|
|
playmode = int(data.attribute('playmode'))
|
|
if playmode == self.GAME_PLAY_MODE_2NDMIX:
|
|
if userid is not None:
|
|
# These are special cased and treated like achievements
|
|
points = int(data.attribute('score_2nd'))
|
|
combo = int(data.attribute('combo_2nd'))
|
|
rank = int(data.attribute('rank_2nd'))
|
|
|
|
# Grab the old 2nd mix score
|
|
existingscore = self.data.local.user.get_achievement(
|
|
self.game,
|
|
self.music_version,
|
|
userid,
|
|
(songid * 100) + chart,
|
|
'2ndmix',
|
|
)
|
|
|
|
if existingscore is not None:
|
|
highscore = points > existingscore.get_int('points')
|
|
|
|
plays = existingscore.get_int('plays', 0) + 1
|
|
points = max(points, existingscore.get_int('points'))
|
|
if not highscore:
|
|
combo = existingscore.get_int('combo', combo)
|
|
rank = existingscore.get_int('rank', rank)
|
|
else:
|
|
plays = 1
|
|
|
|
self.data.local.user.put_achievement(
|
|
self.game,
|
|
self.music_version,
|
|
userid,
|
|
(songid * 100) + chart,
|
|
'2ndmix',
|
|
{
|
|
'points': points,
|
|
'combo': combo,
|
|
'rank': rank,
|
|
'plays': plays,
|
|
},
|
|
)
|
|
else:
|
|
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.
|
|
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('cnt_m4', play_stats.get_int('cnt_m4')))
|
|
root.add_child(Node.u32('cnt_m5', play_stats.get_int('cnt_m5')))
|
|
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)
|
|
chara.set_attribute('my', str(profile.get_int('chara', 30)))
|
|
root.add_child(Node.u16_array('chara_opt', profile.get_int_array('chara_opt', 96, [208] * 96)))
|
|
|
|
# Drill rankings
|
|
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')))
|
|
|
|
# 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')
|
|
|
|
# Daily play counts
|
|
daycount = Node.void('daycount')
|
|
root.add_child(daycount)
|
|
daycount.set_attribute('playcount', str(play_stats.today_plays))
|
|
|
|
# Daily combo stuff, unknown how this works
|
|
dailycombo = Node.void('dailycombo')
|
|
root.add_child(dailycombo)
|
|
dailycombo.set_attribute('daily_combo', str(0))
|
|
dailycombo.set_attribute('daily_combo_lv', str(0))
|
|
|
|
# Last cursor settings
|
|
last = Node.void('last')
|
|
root.add_child(last)
|
|
lastdict = profile.get_dict('last')
|
|
last.set_attribute('rival1', str(lastdict.get_int('rival1', -1)))
|
|
last.set_attribute('rival2', str(lastdict.get_int('rival2', -1)))
|
|
last.set_attribute('rival3', str(lastdict.get_int('rival3', -1)))
|
|
last.set_attribute('fri', str(lastdict.get_int('rival1', -1))) # This literally goes to the same memory in X3
|
|
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')))
|
|
|
|
# Result stars
|
|
result_star = Node.void('result_star')
|
|
root.add_child(result_star)
|
|
result_stars = profile.get_int_array('result_stars', 9)
|
|
for i in range(9):
|
|
result_star.set_attribute(f'slot{i + 1}', str(result_stars[i]))
|
|
|
|
# Target stuff
|
|
target = Node.void('target')
|
|
root.add_child(target)
|
|
target.set_attribute('flag', str(profile.get_int('target_flag')))
|
|
target.set_attribute('setnum', str(profile.get_int('target_setnum')))
|
|
|
|
# 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
|
|
|
|
# Play area
|
|
areas = profile.get_int_array('play_area', 55)
|
|
play_area = Node.void('play_area')
|
|
root.add_child(play_area)
|
|
for i in range(len(areas)):
|
|
play_area.set_attribute(f'play_cnt{i}', str(areas[i]))
|
|
|
|
return root
|
|
|
|
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
|
|
newprofile = copy.deepcopy(oldprofile)
|
|
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}')
|
|
|
|
# Result stars
|
|
result_star = request.child('result_star')
|
|
if result_star is not None:
|
|
newprofile.replace_int_array(
|
|
'result_stars',
|
|
9,
|
|
[
|
|
intish(result_star.attribute('slot1')),
|
|
intish(result_star.attribute('slot2')),
|
|
intish(result_star.attribute('slot3')),
|
|
intish(result_star.attribute('slot4')),
|
|
intish(result_star.attribute('slot5')),
|
|
intish(result_star.attribute('slot6')),
|
|
intish(result_star.attribute('slot7')),
|
|
intish(result_star.attribute('slot8')),
|
|
intish(result_star.attribute('slot9')),
|
|
],
|
|
)
|
|
|
|
# Target stuff
|
|
target = request.child('target')
|
|
if target is not None:
|
|
newprofile.replace_int('target_flag', intish(target.attribute('flag')))
|
|
newprofile.replace_int('target_setnum', intish(target.attribute('setnum')))
|
|
|
|
# Update last attributes
|
|
lastdict.replace_int('rival1', intish(last.attribute('rival1')))
|
|
lastdict.replace_int('rival2', intish(last.attribute('rival2')))
|
|
lastdict.replace_int('rival3', intish(last.attribute('rival3')))
|
|
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')))
|
|
chara_opt = request.child('chara_opt')
|
|
if chara_opt is not None:
|
|
# A bug in old versions of AVS returns the wrong number for set
|
|
newprofile.replace_int_array('chara_opt', 96, chara_opt.value[:96])
|
|
|
|
# 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],
|
|
{},
|
|
)
|
|
|
|
# Play area counter
|
|
shop_area = int(request.attribute('shop_area'))
|
|
if shop_area >= 0 and shop_area < 55:
|
|
areas = newprofile.get_int_array('play_area', 55)
|
|
areas[shop_area] = areas[shop_area] + 1
|
|
newprofile.replace_int_array('play_area', 55, areas)
|
|
|
|
# Keep track of play statistics
|
|
self.update_play_statistics(userid, play_stats)
|
|
|
|
return newprofile
|