1
0
mirror of synced 2024-12-15 15:51:15 +01:00
bemaniutils/bemani/backend/ddr/ddrace.py

861 lines
38 KiB
Python
Raw Normal View History

# vim: set fileencoding=utf-8
import base64
from typing import Any, Dict, List, Optional, Tuple
from typing_extensions import Final
from bemani.backend.ess import EventLogHandler
from bemani.backend.ddr.base import DDRBase
from bemani.backend.ddr.ddr2014 import DDR2014
from bemani.common import Profile, ValidatedDict, VersionConstants, CardCipher, Time, ID, intish
from bemani.data import Data, Achievement, Machine, Score, UserID
from bemani.protocol import Node
class DDRAce(
DDRBase,
EventLogHandler,
):
name: str = 'DanceDanceRevolution A'
version: int = VersionConstants.DDR_ACE
GAME_STYLE_SINGLE: Final[int] = 0
GAME_STYLE_DOUBLE: Final[int] = 1
GAME_STYLE_VERSUS: Final[int] = 2
GAME_RIVAL_TYPE_RIVAL3: Final[int] = 32
GAME_RIVAL_TYPE_RIVAL2: Final[int] = 16
GAME_RIVAL_TYPE_RIVAL1: Final[int] = 8
GAME_RIVAL_TYPE_WORLD: Final[int] = 4
GAME_RIVAL_TYPE_AREA: Final[int] = 2
GAME_RIVAL_TYPE_MACHINE: Final[int] = 1
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] = 6
GAME_HALO_GOOD_COMBO: Final[int] = 7
GAME_HALO_GREAT_COMBO: Final[int] = 8
GAME_HALO_PERFECT_COMBO: Final[int] = 9
GAME_HALO_MARVELOUS_COMBO: Final[int] = 10
GAME_RANK_E: Final[int] = 15
GAME_RANK_D: Final[int] = 14
GAME_RANK_D_PLUS: Final[int] = 13
GAME_RANK_C_MINUS: Final[int] = 12
GAME_RANK_C: Final[int] = 11
GAME_RANK_C_PLUS: Final[int] = 10
GAME_RANK_B_MINUS: Final[int] = 9
GAME_RANK_B: Final[int] = 8
GAME_RANK_B_PLUS: Final[int] = 7
GAME_RANK_A_MINUS: Final[int] = 6
GAME_RANK_A: Final[int] = 5
GAME_RANK_A_PLUS: Final[int] = 4
GAME_RANK_AA_MINUS: Final[int] = 3
GAME_RANK_AA: Final[int] = 2
GAME_RANK_AA_PLUS: Final[int] = 1
GAME_RANK_AAA: Final[int] = 0
GAME_MAX_SONGS: Final[int] = 1024
GAME_COMMON_AREA_OFFSET: Final[int] = 1
GAME_COMMON_WEIGHT_DISPLAY_OFFSET: Final[int] = 3
GAME_COMMON_CHARACTER_OFFSET: Final[int] = 4
GAME_COMMON_EXTRA_CHARGE_OFFSET: Final[int] = 5
GAME_COMMON_TOTAL_PLAYS_OFFSET: Final[int] = 9
GAME_COMMON_SINGLE_PLAYS_OFFSET: Final[int] = 11
GAME_COMMON_DOUBLE_PLAYS_OFFSET: Final[int] = 12
GAME_COMMON_WEIGHT_OFFSET: Final[int] = 17
GAME_COMMON_NAME_OFFSET: Final[int] = 25
GAME_COMMON_SEQ_OFFSET: Final[int] = 26
GAME_OPTION_SPEED_OFFSET: Final[int] = 1
GAME_OPTION_BOOST_OFFSET: Final[int] = 2
GAME_OPTION_APPEARANCE_OFFSET: Final[int] = 3
GAME_OPTION_TURN_OFFSET: Final[int] = 4
GAME_OPTION_STEP_ZONE_OFFSET: Final[int] = 5
GAME_OPTION_SCROLL_OFFSET: Final[int] = 6
GAME_OPTION_ARROW_COLOR_OFFSET: Final[int] = 7
GAME_OPTION_CUT_OFFSET: Final[int] = 8
GAME_OPTION_FREEZE_OFFSET: Final[int] = 9
GAME_OPTION_JUMPS_OFFSET: Final[int] = 10
GAME_OPTION_ARROW_SKIN_OFFSET: Final[int] = 11
GAME_OPTION_FILTER_OFFSET: Final[int] = 12
GAME_OPTION_GUIDELINE_OFFSET: Final[int] = 13
GAME_OPTION_GAUGE_OFFSET: Final[int] = 14
GAME_OPTION_COMBO_POSITION_OFFSET: Final[int] = 15
GAME_OPTION_FAST_SLOW_OFFSET: Final[int] = 16
GAME_LAST_CALORIES_OFFSET: Final[int] = 10
GAME_RIVAL_SLOT_1_ACTIVE_OFFSET: Final[int] = 1
GAME_RIVAL_SLOT_2_ACTIVE_OFFSET: Final[int] = 2
GAME_RIVAL_SLOT_3_ACTIVE_OFFSET: Final[int] = 3
GAME_RIVAL_SLOT_1_DDRCODE_OFFSET: Final[int] = 9
GAME_RIVAL_SLOT_2_DDRCODE_OFFSET: Final[int] = 10
GAME_RIVAL_SLOT_3_DDRCODE_OFFSET: Final[int] = 11
def previous_version(self) -> Optional[DDRBase]:
return DDR2014(self.data, self.config, self.model)
@classmethod
def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
# DDR Ace has a weird bug where it sends a profile save for a blank
# profile before reading it back when creating a new profile. If there
# is no profile on read-back, it errors out, and it also uses the name
# and area ID as the takeover/succession data if the user had previous
# data on an old game. However, if for some reason the user cancels out
# of the name entry, loses power or disconnects from the network at the
# right time, then the profile exists in a broken state forever until they
# edit it on the front-end. As a work-around to this, we remember the last
# time each profile was written to, and we look up profiles that are older
# than a few minutes (the maximum possible time for DDR Ace to write back
# a new profile after creating a blank one) and have blank names and delete
# them in order to keep the profiles on the network in sane order. This
# should normally never delete any profiles.
profiles = data.local.user.get_all_profiles(cls.game, cls.version)
several_minutes_ago = Time.now() - (Time.SECONDS_IN_MINUTE * 5)
events = []
for userid, profile in profiles:
if profile.get_str('name') == "" and profile.get_int('write_time') < several_minutes_ago:
data.local.user.delete_profile(cls.game, cls.version, userid)
events.append((
'ddr_profile_purge',
{
'userid': userid,
},
))
return events
@property
def supports_paseli(self) -> bool:
if self.model.dest != 'J':
# DDR Ace in USA mode doesn't support PASELI properly.
# When in Asia mode it shows PASELI but won't let you select it.
return False
else:
# All other modes should work with PASELI.
return True
def game_to_db_rank(self, game_rank: int) -> int:
return {
self.GAME_RANK_AAA: self.RANK_AAA,
self.GAME_RANK_AA_PLUS: self.RANK_AA_PLUS,
self.GAME_RANK_AA: self.RANK_AA,
self.GAME_RANK_AA_MINUS: self.RANK_AA_MINUS,
self.GAME_RANK_A_PLUS: self.RANK_A_PLUS,
self.GAME_RANK_A: self.RANK_A,
self.GAME_RANK_A_MINUS: self.RANK_A_MINUS,
self.GAME_RANK_B_PLUS: self.RANK_B_PLUS,
self.GAME_RANK_B: self.RANK_B,
self.GAME_RANK_B_MINUS: self.RANK_B_MINUS,
self.GAME_RANK_C_PLUS: self.RANK_C_PLUS,
self.GAME_RANK_C: self.RANK_C,
self.GAME_RANK_C_MINUS: self.RANK_C_MINUS,
self.GAME_RANK_D_PLUS: self.RANK_D_PLUS,
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_PLUS,
self.RANK_AA: self.GAME_RANK_AA,
self.RANK_AA_MINUS: self.GAME_RANK_AA_MINUS,
self.RANK_A_PLUS: self.GAME_RANK_A_PLUS,
self.RANK_A: self.GAME_RANK_A,
self.RANK_A_MINUS: self.GAME_RANK_A_MINUS,
self.RANK_B_PLUS: self.GAME_RANK_B_PLUS,
self.RANK_B: self.GAME_RANK_B,
self.RANK_B_MINUS: self.GAME_RANK_B_MINUS,
self.RANK_C_PLUS: self.GAME_RANK_C_PLUS,
self.RANK_C: self.GAME_RANK_C,
self.RANK_C_MINUS: self.GAME_RANK_C_MINUS,
self.RANK_D_PLUS: self.GAME_RANK_D_PLUS,
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 game_to_db_halo(self, game_halo: int) -> int:
if game_halo == self.GAME_HALO_MARVELOUS_COMBO:
return self.HALO_MARVELOUS_FULL_COMBO
elif game_halo == self.GAME_HALO_PERFECT_COMBO:
return self.HALO_PERFECT_FULL_COMBO
elif game_halo == self.GAME_HALO_GREAT_COMBO:
return self.HALO_GREAT_FULL_COMBO
elif game_halo == self.GAME_HALO_GOOD_COMBO:
return self.HALO_GOOD_FULL_COMBO
else:
return self.HALO_NONE
def db_to_game_halo(self, db_halo: int) -> int:
if db_halo == self.HALO_MARVELOUS_FULL_COMBO:
return self.GAME_HALO_MARVELOUS_COMBO
elif db_halo == self.HALO_PERFECT_FULL_COMBO:
return self.GAME_HALO_PERFECT_COMBO
elif db_halo == self.HALO_GREAT_FULL_COMBO:
return self.GAME_HALO_GREAT_COMBO
elif db_halo == self.HALO_GOOD_FULL_COMBO:
return self.GAME_HALO_GOOD_COMBO
else:
return self.GAME_HALO_NONE
def handle_tax_get_phase_request(self, request: Node) -> Node:
tax = Node.void('tax')
tax.add_child(Node.s32('phase', 0))
return tax
def __handle_userload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
has_profile: bool = False
achievements: List[Achievement] = []
scores: List[Score] = []
if userid is not None:
has_profile = self.has_profile(userid)
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
scores = self.data.remote.music.get_scores(self.game, self.music_version, userid)
# Place scores into an arrangement for easier distribution to Ace.
scores_by_mcode: Dict[int, List[Optional[Score]]] = {}
for score in scores:
if score.id not in scores_by_mcode:
scores_by_mcode[score.id] = [None] * 9
scores_by_mcode[score.id][self.db_to_game_chart(score.chart)] = score
# First, set new flag
response.add_child(Node.bool('is_new', not has_profile))
# Now, return the scores to Ace
for mcode in scores_by_mcode:
music = Node.void('music')
response.add_child(music)
music.add_child(Node.u32('mcode', mcode))
scores_that_matter = scores_by_mcode[mcode]
while scores_that_matter[-1] is None:
scores_that_matter = scores_that_matter[:-1]
for score in scores_that_matter:
note = Node.void('note')
music.add_child(note)
if score is None:
note.add_child(Node.u16('count', 0))
note.add_child(Node.u8('rank', 0))
note.add_child(Node.u8('clearkind', 0))
note.add_child(Node.s32('score', 0))
note.add_child(Node.s32('ghostid', 0))
else:
note.add_child(Node.u16('count', score.plays))
note.add_child(Node.u8('rank', self.db_to_game_rank(score.data.get_int('rank'))))
note.add_child(Node.u8('clearkind', self.db_to_game_halo(score.data.get_int('halo'))))
note.add_child(Node.s32('score', score.points))
note.add_child(Node.s32('ghostid', score.key))
# Active event settings
activeevents = [
1,
3,
5,
9,
10,
11,
12,
13,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
]
# Event reward settings
rewards = {
'30': {
999: 5,
}
}
# Now handle event progress and activation.
events = {ach.id: ach.data for ach in achievements if ach.type == '9999'}
progress = [ach for ach in achievements if ach.type != '9999']
# Make sure we always send a babylon's adventure save event or the game won't send progress
babylon_included = False
for evtprogress in progress:
if evtprogress.id == 999 and evtprogress.type == '30':
babylon_included = True
break
if not babylon_included:
progress.append(Achievement(
999,
'30',
None,
{
'completed': False,
'progress': 0,
},
))
for event in activeevents:
# Get completion data
playerstats = events.get(event, ValidatedDict({'completed': False}))
# Return the data
eventdata = Node.void('eventdata')
response.add_child(eventdata)
eventdata.add_child(Node.u32('eventid', event))
eventdata.add_child(Node.s32('eventtype', 9999))
eventdata.add_child(Node.u32('eventno', 0))
eventdata.add_child(Node.s64('condition', 0))
eventdata.add_child(Node.u32('reward', 0))
eventdata.add_child(Node.s32('comptime', 1 if playerstats.get_bool('completed') else 0))
eventdata.add_child(Node.s64('savedata', 0))
for evtprogress in progress:
# Babylon's adventure progres and anything else the game sends
eventdata = Node.void('eventdata')
response.add_child(eventdata)
eventdata.add_child(Node.u32('eventid', evtprogress.id))
eventdata.add_child(Node.s32('eventtype', int(evtprogress.type)))
eventdata.add_child(Node.u32('eventno', 0))
eventdata.add_child(Node.s64('condition', 0))
eventdata.add_child(Node.u32('reward', rewards.get(evtprogress.type, {}).get(evtprogress.id)))
eventdata.add_child(Node.s32('comptime', 1 if evtprogress.data.get_bool('completed') else 0))
eventdata.add_child(Node.s64('savedata', evtprogress.data.get_int('progress')))
def __handle_usersave(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
if userid is None:
# the game sends us empty user ID strings when a guest is playing.
# Return early so it doesn't wait a minute and a half to show the
# results screen.
return
if requestdata.child_value('isgameover'):
style = int(requestdata.child_value('playstyle'))
is_dp = style == self.GAME_STYLE_DOUBLE
# We don't save anything for gameover requests, since we
# already saved scores on individual ones. So, just use this
# as a spot to bump play counts and such
play_stats = self.get_play_statistics(userid)
if is_dp:
play_stats.increment_int('double_plays')
else:
play_stats.increment_int('single_plays')
self.update_play_statistics(userid, play_stats)
# Now is a good time to check if we have workout mode enabled,
# and if so, store the calories earned for this set.
profile = self.get_profile(userid)
enabled = profile.get_bool('workout_mode')
weight = profile.get_int('weight')
if enabled and weight > 0:
# We enabled weight display, find the calories and save them
total = 0
for child in requestdata.children:
if child.name != 'note':
continue
total = total + (child.child_value('calorie') or 0)
self.data.local.user.put_time_based_achievement(
self.game,
self.version,
userid,
0,
'workout',
{
'calories': total,
'weight': weight,
},
)
# Find any event updates
for child in requestdata.children:
if child.name != 'event':
continue
# Skip empty events or events we don't support
eventid = child.child_value('eventid')
eventtype = child.child_value('eventtype')
if eventid == 0 or eventtype == 0:
continue
# Save data to replay to the client later
completed = child.child_value('comptime') != 0
progress = child.child_value('savedata')
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
eventid,
str(eventtype),
{
'completed': completed,
'progress': progress,
},
)
return
# Find the highest stagenum played
score = None
stagenum = 0
for child in requestdata.children:
if child.name != 'note':
continue
if child.child_value('stagenum') > stagenum:
score = child
stagenum = child.child_value('stagenum')
if score is None:
raise Exception('Couldn\'t find newest score to save!')
songid = score.child_value('mcode')
chart = self.game_to_db_chart(score.child_value('notetype'))
rank = self.game_to_db_rank(score.child_value('rank'))
halo = self.game_to_db_halo(score.child_value('clearkind'))
points = score.child_value('score')
combo = score.child_value('maxcombo')
ghost = score.child_value('ghost')
self.update_score(
userid,
songid,
chart,
points,
rank,
halo,
combo,
ghost=ghost,
)
def __handle_rivalload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
data = Node.void('data')
response.add_child(data)
data.add_child(Node.s32('recordtype', requestdata.child_value('loadflag')))
thismachine = self.data.local.machine.get_machine(self.config.machine.pcbid)
machines_by_id: Dict[int, Optional[Machine]] = {thismachine.id: thismachine}
loadkind = requestdata.child_value('loadflag')
profiles_by_userid: Dict[UserID, Profile] = {}
def get_machine(lid: int) -> Optional[Machine]:
if lid not in machines_by_id:
pcbid = self.data.local.machine.from_machine_id(lid)
if pcbid is None:
machines_by_id[lid] = None
return None
machine = self.data.local.machine.get_machine(pcbid)
if machine is None:
machines_by_id[lid] = None
return None
machines_by_id[lid] = machine
return machines_by_id[lid]
if loadkind == self.GAME_RIVAL_TYPE_WORLD:
# Just load all scores for this network
scores = self.data.remote.music.get_all_records(self.game, self.music_version)
elif loadkind == self.GAME_RIVAL_TYPE_AREA:
if thismachine.arcade is not None:
match_arcade = thismachine.arcade
match_machine = None
else:
match_arcade = None
match_machine = thismachine.id
# Load up all scores by any user registered on a machine in the same arcade
profiles = self.data.local.user.get_all_profiles(self.game, self.version)
userids: List[UserID] = []
for userid, profiledata in profiles:
profiles_by_userid[userid] = profiledata
# If we have an arcade to match, see if this user's location matches the arcade.
# If we don't, just match lid directly
if match_arcade is not None:
theirmachine = get_machine(profiledata.get_int('lid'))
if theirmachine is not None and theirmachine.arcade == match_arcade:
userids.append(userid)
elif match_machine is not None:
if profiledata.get_int('lid') == match_machine:
userids.append(userid)
# Load all scores for users in the area
scores = self.data.local.music.get_all_records(self.game, self.music_version, userlist=userids)
elif loadkind == self.GAME_RIVAL_TYPE_MACHINE:
# Load up all scores and filter them by those earned at this location
scores = self.data.local.music.get_all_records(self.game, self.music_version, locationlist=[thismachine.id])
elif loadkind in [
self.GAME_RIVAL_TYPE_RIVAL1,
self.GAME_RIVAL_TYPE_RIVAL2,
self.GAME_RIVAL_TYPE_RIVAL3,
]:
# Load up this user's highscores, format the way the below code expects it
extid = requestdata.child_value('ddrcode')
otherid = self.data.remote.user.from_extid(self.game, self.version, extid)
userscores = self.data.remote.music.get_scores(self.game, self.music_version, otherid)
scores = [(otherid, score) for score in userscores]
else:
# Nothing here
scores = []
missing_users = [userid for (userid, _) in scores if userid not in profiles_by_userid]
for (userid, profile) in self.get_any_profiles(missing_users):
profiles_by_userid[userid] = profile
for userid, score in scores:
if profiles_by_userid.get(userid) is None:
raise Exception(f'Logic error, couldn\'t find any profile for {userid}')
profiledata = profiles_by_userid[userid]
record = Node.void('record')
data.add_child(record)
record.add_child(Node.u32('mcode', score.id))
record.add_child(Node.u8('notetype', self.db_to_game_chart(score.chart)))
record.add_child(Node.u8('rank', self.db_to_game_rank(score.data.get_int('rank'))))
record.add_child(Node.u8('clearkind', self.db_to_game_halo(score.data.get_int('halo'))))
record.add_child(Node.u8('flagdata', 0))
record.add_child(Node.string('name', profiledata.get_str('name')))
record.add_child(Node.s32('area', profiledata.get_int('area', 58)))
record.add_child(Node.s32('code', profiledata.extid))
record.add_child(Node.s32('score', score.points))
record.add_child(Node.s32('ghostid', score.key))
def __handle_usernew(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
if userid is None:
raise Exception('Expecting valid UserID to create new profile!')
machine = self.data.local.machine.get_machine(self.config.machine.pcbid)
profile = Profile(
self.game,
self.version,
"",
0,
{
'lid': machine.id,
},
)
self.put_profile(userid, profile)
response.add_child(Node.string('seq', ID.format_extid(profile.extid)))
response.add_child(Node.s32('code', profile.extid))
response.add_child(Node.string('shoparea', ''))
def __handle_inheritance(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
if userid is not None:
previous_version = self.previous_version()
profile = previous_version.get_profile(userid)
else:
profile = None
response.add_child(Node.s32('InheritanceStatus', 1 if profile is not None else 0))
def __handle_ghostload(self, userid: Optional[UserID], requestdata: Node, response: Node) -> None:
ghostid = requestdata.child_value('ghostid')
ghost = self.data.local.music.get_score_by_key(self.game, self.music_version, ghostid)
if ghost is None:
return
userid, score = ghost
profile = self.get_profile(userid)
if profile is None:
return
if 'ghost' not in score.data:
return
ghostdata = Node.void('ghostdata')
response.add_child(ghostdata)
ghostdata.add_child(Node.s32('code', profile.extid))
ghostdata.add_child(Node.u32('mcode', score.id))
ghostdata.add_child(Node.u8('notetype', self.db_to_game_chart(score.chart)))
ghostdata.add_child(Node.s32('ghostsize', len(score.data['ghost'])))
ghostdata.add_child(Node.string('ghost', score.data['ghost']))
def handle_playerdata_usergamedata_advanced_request(self, request: Node) -> Optional[Node]:
playerdata = Node.void('playerdata')
# DDR Ace decides to be difficult and have a third level of packet switching
mode = request.child_value('data/mode')
refid = request.child_value('data/refid')
extid = request.child_value('data/ddrcode')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is None:
# Possibly look up by extid instead
userid = self.data.remote.user.from_extid(self.game, self.version, extid)
if mode == 'userload':
self.__handle_userload(userid, request.child('data'), playerdata)
elif mode == 'usersave':
self.__handle_usersave(userid, request.child('data'), playerdata)
elif mode == 'rivalload':
self.__handle_rivalload(userid, request.child('data'), playerdata)
elif mode == 'usernew':
self.__handle_usernew(userid, request.child('data'), playerdata)
elif mode == 'inheritance':
self.__handle_inheritance(userid, request.child('data'), playerdata)
elif mode == 'ghostload':
self.__handle_ghostload(userid, request.child('data'), playerdata)
else:
# We don't support this
return None
playerdata.add_child(Node.s32('result', 0))
return playerdata
def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node:
playerdata = Node.void('playerdata')
refid = request.child_value('data/refid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
profile = self.get_profile(userid) or Profile(self.game, self.version, refid, 0)
usergamedata = profile.get_dict('usergamedata')
for record in request.child('data/record').children:
if record.name != 'd':
continue
strdata = base64.b64decode(record.value)
bindata = base64.b64decode(record.child_value('bin1'))
# Grab and format the profile objects
strdatalist = strdata.split(b',')
profiletype = strdatalist[1].decode('utf-8')
strdatalist = strdatalist[2:]
# Extract relevant bits for frontend/API
if profiletype == 'COMMON':
profile.replace_str('name', strdatalist[self.GAME_COMMON_NAME_OFFSET].decode('ascii'))
profile.replace_int('area', intish(strdatalist[self.GAME_COMMON_AREA_OFFSET].decode('ascii'), 16))
profile.replace_bool('workout_mode', int(strdatalist[self.GAME_COMMON_WEIGHT_DISPLAY_OFFSET].decode('ascii'), 16) != 0)
profile.replace_int('weight', int(float(strdatalist[self.GAME_COMMON_WEIGHT_OFFSET].decode('ascii')) * 10))
profile.replace_int('character', int(strdatalist[self.GAME_COMMON_CHARACTER_OFFSET].decode('ascii'), 16))
if profiletype == 'OPTION':
profile.replace_int('combo', int(strdatalist[self.GAME_OPTION_COMBO_POSITION_OFFSET].decode('ascii'), 16))
profile.replace_int('early_late', int(strdatalist[self.GAME_OPTION_FAST_SLOW_OFFSET].decode('ascii'), 16))
profile.replace_int('arrowskin', int(strdatalist[self.GAME_OPTION_ARROW_SKIN_OFFSET].decode('ascii'), 16))
profile.replace_int('guidelines', int(strdatalist[self.GAME_OPTION_GUIDELINE_OFFSET].decode('ascii'), 16))
profile.replace_int('filter', int(strdatalist[self.GAME_OPTION_FILTER_OFFSET].decode('ascii'), 16))
usergamedata[profiletype] = {
'strdata': b','.join(strdatalist),
'bindata': bindata,
}
profile.replace_dict('usergamedata', usergamedata)
profile.replace_int('write_time', Time.now())
self.put_profile(userid, profile)
playerdata.add_child(Node.s32('result', 0))
return playerdata
def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node:
playerdata = Node.void('playerdata')
player = Node.void('player')
playerdata.add_child(player)
refid = request.child_value('data/refid')
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
if userid is not None:
profile = self.get_profile(userid)
links = self.data.local.user.get_links(self.game, self.version, userid)
records = 0
record = Node.void('record')
player.add_child(record)
def acehex(val: int) -> str:
return hex(val)[2:]
if profile is None:
# Just return a default empty node
record.add_child(Node.string('d', '<NODATA>'))
records = 1
else:
# Figure out what profiles are being requested
profiletypes = request.child_value('data/recv_csv').split(',')[::2]
usergamedata = profile.get_dict('usergamedata')
for ptype in profiletypes:
if ptype in usergamedata:
records = records + 1
if ptype == "COMMON":
# Return basic profile options
name = profile.get_str('name')
area = profile.get_int('area', self.get_machine_region())
if name == "":
# This is a bogus profile created by the first login, substitute the
# previous version values so that profile succession works.
previous_version = self.previous_version()
old_profile = previous_version.get_profile(userid)
if old_profile is not None:
name = old_profile.get_str('name')
area = old_profile.get_int('area', self.get_machine_region())
else:
area = self.get_machine_region()
common = usergamedata[ptype]['strdata'].split(b',')
common[self.GAME_COMMON_NAME_OFFSET] = name.encode('ascii')
common[self.GAME_COMMON_AREA_OFFSET] = acehex(area).encode('ascii')
common[self.GAME_COMMON_WEIGHT_DISPLAY_OFFSET] = b'1' if profile.get_bool('workout_mode') else b'0'
common[self.GAME_COMMON_WEIGHT_OFFSET] = str(float(profile.get_int('weight')) / 10.0).encode('ascii')
common[self.GAME_COMMON_CHARACTER_OFFSET] = acehex(profile.get_int('character')).encode('ascii')
usergamedata[ptype]['strdata'] = b','.join(common)
if ptype == "OPTION":
# Return user settings for frontend
option = usergamedata[ptype]['strdata'].split(b',')
option[self.GAME_OPTION_FAST_SLOW_OFFSET] = acehex(profile.get_int('early_late')).encode('ascii')
option[self.GAME_OPTION_COMBO_POSITION_OFFSET] = acehex(profile.get_int('combo')).encode('ascii')
option[self.GAME_OPTION_ARROW_SKIN_OFFSET] = acehex(profile.get_int('arrowskin')).encode('ascii')
option[self.GAME_OPTION_GUIDELINE_OFFSET] = acehex(profile.get_int('guidelines')).encode('ascii')
option[self.GAME_OPTION_FILTER_OFFSET] = acehex(profile.get_int('filter')).encode('ascii')
usergamedata[ptype]['strdata'] = b','.join(option)
if ptype == "LAST":
# Return the number of calories expended in the last day
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])
last = usergamedata[ptype]['strdata'].split(b',')
last[self.GAME_LAST_CALORIES_OFFSET] = acehex(total).encode('ascii')
usergamedata[ptype]['strdata'] = b','.join(last)
if ptype == "RIVAL":
# Fill in the DDR code and active status of the three active
# rivals.
rival = usergamedata[ptype]['strdata'].split(b',')
lastdict = profile.get_dict('last')
friends: Dict[int, Optional[Profile]] = {}
for link in links:
if link.type[:7] != 'friend_':
continue
pos = int(link.type[7:])
friends[pos] = self.get_profile(link.other_userid)
for rivalno in [1, 2, 3]:
activeslot = {
1: self.GAME_RIVAL_SLOT_1_ACTIVE_OFFSET,
2: self.GAME_RIVAL_SLOT_2_ACTIVE_OFFSET,
3: self.GAME_RIVAL_SLOT_3_ACTIVE_OFFSET,
}[rivalno]
whichfriend = lastdict.get_int(f'rival{rivalno}') - 1
if whichfriend < 0:
# This rival isn't active
rival[activeslot] = b'0'
continue
friendprofile = friends.get(whichfriend)
if friendprofile is None:
# This rival doesn't exist
rival[activeslot] = b'0'
continue
ddrcodeslot = {
1: self.GAME_RIVAL_SLOT_1_DDRCODE_OFFSET,
2: self.GAME_RIVAL_SLOT_2_DDRCODE_OFFSET,
3: self.GAME_RIVAL_SLOT_3_DDRCODE_OFFSET,
}[rivalno]
rival[activeslot] = acehex(rivalno).encode('ascii')
rival[ddrcodeslot] = acehex(friendprofile.extid).encode('ascii')
usergamedata[ptype]['strdata'] = b','.join(rival)
dnode = Node.string('d', base64.b64encode(usergamedata[ptype]['strdata']).decode('ascii'))
dnode.add_child(Node.string('bin1', base64.b64encode(usergamedata[ptype]['bindata']).decode('ascii')))
record.add_child(dnode)
player.add_child(Node.u32('record_num', records))
playerdata.add_child(Node.s32('result', 0))
return playerdata
def handle_system_convcardnumber_request(self, request: Node) -> Node:
cardid = request.child_value('data/card_id')
cardnumber = CardCipher.encode(cardid)
system = Node.void('system')
data = Node.void('data')
system.add_child(data)
system.add_child(Node.s32('result', 0))
data.add_child(Node.string('card_number', cardnumber))
return system