810 lines
35 KiB
Python
810 lines
35 KiB
Python
# vim: set fileencoding=utf-8
|
|
import base64
|
|
from typing import Dict, List, Optional
|
|
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 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)
|
|
|
|
@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:
|
|
response.add_child(Node.s32('InheritanceStatus', 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)
|
|
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
|
|
common = usergamedata[ptype]['strdata'].split(b',')
|
|
common[self.GAME_COMMON_NAME_OFFSET] = profile.get_str('name').encode('ascii')
|
|
common[self.GAME_COMMON_AREA_OFFSET] = acehex(profile.get_int('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
|