def __format_profile(self, cardids: List[str], profile: ValidatedDict, settings: ValidatedDict, exact: bool) -> Dict[str, Any]: base = { 'name': profile.get_str('name'), 'cards': cardids, 'registered': settings.get_int('first_play_timestamp', -1), 'updated': settings.get_int('last_play_timestamp', -1), 'plays': settings.get_int('total_plays', -1), 'match': 'exact' if exact else 'partial', } if self.game == GameConstants.DDR: base.update(self.__format_ddr_profile(profile, exact)) if self.game == GameConstants.IIDX: base.update(self.__format_iidx_profile(profile, exact)) if self.game == GameConstants.JUBEAT: base.update(self.__format_jubeat_profile(profile, exact)) if self.game == GameConstants.MUSECA: base.update(self.__format_museca_profile(profile, exact)) if self.game == GameConstants.POPN_MUSIC: base.update(self.__format_popn_profile(profile, exact)) if self.game == GameConstants.REFLEC_BEAT: base.update(self.__format_reflec_profile(profile, exact)) if self.game == GameConstants.SDVX: base.update(self.__format_sdvx_profile(profile, exact)) return base
def format_profile(self, profile: ValidatedDict, playstats: ValidatedDict) -> Dict[str, Any]: return { 'name': profile.get_str('name'), 'extid': ID.format_extid(profile.get_int('extid')), 'first_play_time': playstats.get_int('first_play_timestamp'), 'last_play_time': playstats.get_int('last_play_timestamp'), }
def __format_iidx_profile(self, profile: ValidatedDict) -> Dict[str, Any]: updates: Dict[str, Any] = { 'qpro': {}, } area = profile.get_int('area', -1) if area != -1: updates['pid'] = area qpro = profile.get_dict('qpro') head = qpro.get_int('head', -1) if head != -1: updates['qpro']['head'] = head hair = qpro.get_int('hair', -1) if hair != -1: updates['qpro']['hair'] = hair face = qpro.get_int('face', -1) if face != -1: updates['qpro']['face'] = face body = qpro.get_int('body', -1) if body != -1: updates['qpro']['body'] = body hand = qpro.get_int('hand', -1) if hand != -1: updates['qpro']['hand'] = hand return updates
def format_conversion(self, userid: UserID, profile: ValidatedDict) -> Node: root = Node.void('playerdata') root.add_child(Node.string('name', profile.get_str('name', 'なし'))) root.add_child(Node.s16('chara', profile.get_int('chara', -1))) root.add_child(Node.s32('option', profile.get_int('option', 0))) root.add_child(Node.u8('version', 0)) root.add_child(Node.u8('kind', 0)) root.add_child(Node.u8('season', 0)) clear_medal = [0] * self.GAME_MAX_MUSIC_ID scores = self.data.remote.music.get_scores(self.game, self.version, userid) for score in scores: if score.id > self.GAME_MAX_MUSIC_ID: continue # Skip any scores for chart types we don't support if score.chart not in [ self.CHART_TYPE_EASY, self.CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER, self.CHART_TYPE_EX, ]: continue clear_medal[score.id] = clear_medal[ score.id] | self.__format_medal_for_score(score) root.add_child(Node.u16_array('clear_medal', clear_medal)) return root
def format_profile(self, profile: ValidatedDict, playstats: ValidatedDict) -> Dict[str, Any]: name = 'なし' # Nothing shop = '未設定' # Not set shop_area = '未設定' # Not set for i in range(len(profile['strdatas'])): strdata = profile['strdatas'][i] # Figure out the profile type csvs = strdata.split(b',') if len(csvs) < 2: # Not long enough to care about continue datatype = csvs[1].decode('ascii') if datatype != 'IBBDAT00': # Not the right profile type requested continue name = self.__update_value(name, csvs[27]) shop = self.__update_value(shop, csvs[30]) shop_area = self.__update_value(shop_area, csvs[31]) return { 'name': name, 'extid': ID.format_extid(profile.get_int('extid')), 'shop': shop, 'shop_area': shop_area, 'first_play_time': playstats.get_int('first_play_timestamp'), 'last_play_time': playstats.get_int('last_play_timestamp'), 'plays': playstats.get_int('total_plays'), }
def get_play_statistics(self, userid: UserID) -> ValidatedDict: """ Given a user ID, get the play statistics. Note that games wishing to use this when generating profiles to send to a game should call update_play_statistics when parsing a profile save. Parameters: userid - The user ID we are binding the profile for. Returns a dictionary optionally containing the following attributes: total_plays - Integer count of total plays for this game series first_play_timestamp - Unix timestamp of first play time last_play_timestamp - Unix timestamp of last play time last_play_date - List of ints in the form of [YYYY, MM, DD] of last play date today_plays - Number of times played today total_days - Total individual days played consecutive_days - Number of consecutive days played at this time. """ if RemoteUser.is_remote(userid): return ValidatedDict({}) settings = self.data.local.game.get_settings(self.game, userid) if settings is None: return ValidatedDict({}) return settings
def format_profile(self, profile: ValidatedDict, playstats: ValidatedDict) -> Dict[str, Any]: formatted_profile = super().format_profile(profile, playstats) formatted_profile['plays'] = playstats.get_int('total_plays') formatted_profile['emblem'] = self.format_emblem( profile.get_dict('last').get_int_array('emblem', 5)) return formatted_profile
def format_qpro(self, qpro_dict: ValidatedDict) -> Dict[str, Any]: return { 'body': qpro_dict.get_int('body'), 'face': qpro_dict.get_int('face'), 'hair': qpro_dict.get_int('hair'), 'hand': qpro_dict.get_int('hand'), 'head': qpro_dict.get_int('head'), }
def format_conversion(self, userid: UserID, profile: ValidatedDict) -> Node: # Circular import, ugh from bemani.backend.popn.lapistoria import PopnMusicLapistoria root = Node.void('playerdata') root.add_child(Node.string('name', profile.get_str('name', 'なし'))) root.add_child(Node.s16('chara', profile.get_int('chara', -1))) root.add_child(Node.s32('option', profile.get_int('option', 0))) root.add_child(Node.s8('result', 1)) scores = self.data.remote.music.get_scores(self.game, self.version, userid) for score in scores: if score.id > self.GAME_MAX_MUSIC_ID: continue # Skip any scores for chart types we don't support if score.chart not in [ self.CHART_TYPE_EASY, self.CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER, self.CHART_TYPE_EX, ]: continue points = score.points medal = score.data.get_int('medal') music = Node.void('music') root.add_child(music) music.add_child(Node.s16('music_num', score.id)) music.add_child(Node.u8('sheet_num', { self.CHART_TYPE_EASY: PopnMusicLapistoria.GAME_CHART_TYPE_EASY, self.CHART_TYPE_NORMAL: PopnMusicLapistoria.GAME_CHART_TYPE_NORMAL, self.CHART_TYPE_HYPER: PopnMusicLapistoria.GAME_CHART_TYPE_HYPER, self.CHART_TYPE_EX: PopnMusicLapistoria.GAME_CHART_TYPE_EX, }[score.chart])) music.add_child(Node.s16('cnt', score.plays)) music.add_child(Node.s32('score', 0)) music.add_child(Node.u8('clear_type', 0)) music.add_child(Node.s32('old_score', points)) music.add_child(Node.u8('old_clear_type', { self.PLAY_MEDAL_CIRCLE_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_FAILED, self.PLAY_MEDAL_DIAMOND_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_FAILED, self.PLAY_MEDAL_STAR_FAILED: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_FAILED, self.PLAY_MEDAL_EASY_CLEAR: PopnMusicLapistoria.GAME_PLAY_MEDAL_EASY_CLEAR, self.PLAY_MEDAL_CIRCLE_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_CLEARED, self.PLAY_MEDAL_DIAMOND_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_CLEARED, self.PLAY_MEDAL_STAR_CLEARED: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_CLEARED, self.PLAY_MEDAL_CIRCLE_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_CIRCLE_FULL_COMBO, self.PLAY_MEDAL_DIAMOND_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_DIAMOND_FULL_COMBO, self.PLAY_MEDAL_STAR_FULL_COMBO: PopnMusicLapistoria.GAME_PLAY_MEDAL_STAR_FULL_COMBO, self.PLAY_MEDAL_PERFECT: PopnMusicLapistoria.GAME_PLAY_MEDAL_PERFECT, }[medal])) return root
def __format_profile(self, profile: ValidatedDict) -> ValidatedDict: base = { 'name': profile.get('name', ''), 'game': profile['game'], 'version': profile['version'], 'refid': profile['refid'], 'extid': profile['extid'], } if profile.get('game') == GameConstants.DDR: base.update(self.__format_ddr_profile(profile)) if profile.get('game') == GameConstants.IIDX: base.update(self.__format_iidx_profile(profile)) if profile.get('game') == GameConstants.JUBEAT: base.update(self.__format_jubeat_profile(profile)) if profile.get('game') == GameConstants.MUSECA: base.update(self.__format_museca_profile(profile)) if profile.get('game') == GameConstants.POPN_MUSIC: base.update(self.__format_popn_profile(profile)) if profile.get('game') == GameConstants.REFLEC_BEAT: base.update(self.__format_reflec_profile(profile)) if profile.get('game') == GameConstants.SDVX: base.update(self.__format_sdvx_profile(profile)) return ValidatedDict(base)
def __format_iidx_profile(self, profile: ValidatedDict, exact: bool) -> Dict[str, Any]: qpro = profile.get_dict('qpro') return { 'area': profile.get_int('pid', -1), 'qpro': { 'head': qpro.get_int('head', -1) if exact else -1, 'hair': qpro.get_int('hair', -1) if exact else -1, 'face': qpro.get_int('face', -1) if exact else -1, 'body': qpro.get_int('body', -1) if exact else -1, 'hand': qpro.get_int('hand', -1) if exact else -1, } }
def get_settings(self, game: str, userid: UserID) -> Optional[ValidatedDict]: """ Given a game and a user ID, look up game-wide settings as a dictionary. It is expected that game classes call this function, and provide a consistent game name from version to version, so game settings can be looked up across all versions in a game series. Parameters: game - String identifying a game series. userid - Integer identifying a user, as possibly looked up by UserData. Returns: A dictionary representing game settings stored by a game class, or None if there are no settings for this game/user. """ sql = "SELECT data FROM game_settings WHERE game = :game AND userid = :userid" cursor = self.execute(sql, {'game': game, 'userid': userid}) if cursor.rowcount != 1: # Settings doesn't exist return None result = cursor.fetchone() return ValidatedDict(self.deserialize(result['data']))
def get_item(self, game: str, version: int, catid: int, cattype: str) -> Optional[ValidatedDict]: """ Given a game/userid and catalog id/type, find that catalog entry. Note that there can be more than one catalog entry with the same ID and game/userid as long as each one is a different type. Essentially, cattype namespaces catalog entry. Parameters: game - String identifier of the game looking up this entry. version - Integer identifier of the version looking up this entry. catid - Integer ID, as provided by a game. cattype - The type of catalog entry. Returns: A dictionary as stored by a game class previously, or None if not found. """ sql = ( "SELECT data FROM catalog " "WHERE game = :game AND version = :version AND id = :id AND type = :type" ) cursor = self.execute(sql, { 'game': game, 'version': version, 'id': catid, 'type': cattype }) if cursor.rowcount != 1: # entry doesn't exist return None result = cursor.fetchone() return ValidatedDict(self.deserialize(result['data']))
def get_all_time_sensitive_settings(self, game: str, version: int, name: str) -> List[ValidatedDict]: """ Given a game/version/name, look up all of the time-sensitive settings for this game. Parameters: game - String identifier of the game we want settings for. version - Integer identifying the game version we want settings for. name - The name of the setting we are concerned with. Returns: A list of ValidatedDict of stored settings if there were settings found, or [] otherwise. If settings were found, they are guaranteed to include the attributes 'start_time' and 'end_time' which will both be seconds since the unix epoch (UTC). """ sql = ( "SELECT data, start_time, end_time FROM time_sensitive_settings WHERE " "game = :game AND version = :version AND name = :name") cursor = self.execute(sql, { 'game': game, 'version': version, 'name': name }) if cursor.rowcount == 0: # setting doesn't exist return [] settings = [] for result in cursor.fetchall(): retval = ValidatedDict(self.deserialize(result['data'])) retval['start_time'] = result['start_time'] retval['end_time'] = result['end_time'] settings.append(retval) return settings
def get_time_sensitive_settings(self, game: str, version: int, name: str) -> Optional[ValidatedDict]: """ Given a game/version/name, look up the current time-sensitive settings for this game. Parameters: game - String identifier of the game we want settings for. version - Integer identifying the game version we want settings for. name - The name of the setting we are concerned with. Returns: A ValidatedDict of stored settings if the current setting is found, or None otherwise. If settings were found, they are guaranteed to include the attributes 'start_time' and 'end_time' which will both be seconds since the unix epoch (UTC). """ sql = ( "SELECT data, start_time, end_time FROM time_sensitive_settings WHERE " "game = :game AND version = :version AND name = :name AND start_time <= :time AND end_time > :time" ) cursor = self.execute(sql, { 'game': game, 'version': version, 'name': name, 'time': Time.now() }) if cursor.rowcount != 1: # setting doesn't exist return None result = cursor.fetchone() retval = ValidatedDict(self.deserialize(result['data'])) retval['start_time'] = result['start_time'] retval['end_time'] = result['end_time'] return retval
def get_achievement(self, game: str, userid: UserID, achievementid: int, achievementtype: str) -> Optional[ValidatedDict]: """ Given a game/userid and achievement id/type, find that achievement. Note that there can be more than one achievement with the same ID and game/userid as long as each one is a different type. Essentially, achievementtype namespaces achievements. Parameters: game - String identifier of the game looking up the user. userid - Integer user ID, as looked up by one of the above functions. achievementid - Integer ID, as provided by a game. achievementtype - The type of achievement. Returns: A dictionary as stored by a game class previously, or None if not found. """ sql = ( "SELECT data FROM series_achievement " "WHERE game = :game AND userid = :userid AND id = :id AND type = :type" ) cursor = self.execute( sql, { 'game': game, 'userid': userid, 'id': achievementid, 'type': achievementtype }) if cursor.rowcount != 1: # score doesn't exist return None result = cursor.fetchone() return ValidatedDict(self.deserialize(result['data']))
def __init__( self, game: str, version: int, songid: int, songchart: int, name: Optional[str], artist: Optional[str], genre: Optional[str], data: Dict[str, Any], ) -> None: """ Initialize the song object. Parameters: game - The song's game series. version - The song's game version. songid - The song's ID according to the game. songchart - The song's chart number, according to the game. name - The name of the song, from the DB. artist - The artist of the song, from the DB. genre - The genre of the song, from the DB. data - Any optional data that a game class uses for a song. """ self.game = game self.version = version self.id = songid self.chart = songchart self.name = name self.artist = artist self.genre = genre self.data = ValidatedDict(data)
def new_profile_by_refid(self, refid: Optional[str], name: Optional[str]) -> Node: """ Given a RefID and an optional name, create a profile and then return a formatted profile node. Similar rationale to get_profile_by_refid. """ if refid is None: return None if name is None: name = 'なし' # First, create and save the default profile userid = self.data.remote.user.from_refid(self.game, self.version, refid) defaultprofile = ValidatedDict({ 'name': name, }) self.put_profile(userid, defaultprofile) # Now, reload and format the profile, looking up the has old version flag profile = self.get_profile(userid) oldversion = self.previous_version() oldprofile = oldversion.get_profile(userid) profile['has_old_version'] = oldprofile is not None return self.format_profile(userid, profile)
def get_all_play_session_infos( self, game: str, version: int) -> List[Tuple[UserID, ValidatedDict]]: """ Given a game and version, look up all play session information. Parameters: game - String identifying a game series. version - Integer identifying the version of the game in the series. Returns: A list of Tuples, consisting of a UserID and the dictionary that would be returned for that user if get_play_session_info() was called for that user. """ sql = ("SELECT id, time, userid, data FROM playsession " "WHERE game = :game AND version = :version " "AND time > :time") cursor = self.execute( sql, { 'game': game, 'version': version, 'time': Time.now() - Time.SECONDS_IN_HOUR, }, ) ret = [] for result in cursor.fetchall(): data = ValidatedDict(self.deserialize(result['data'])) data['id'] = result['id'] data['time'] = result['time'] ret.append((UserID(result['userid']), data)) return ret
def __init__( self, key: int, songid: int, songchart: int, points: int, timestamp: int, update: int, location: int, plays: int, data: Dict[str, Any], ) -> None: """ Initialize the score object. Parameters: key - A unique key identifying this exact score. songid - The song's ID according to the game. songchart - The song's chart number, according to the game. points - The points achieved on this song, from the DB. timestamp - The timestamp when the record was earned. update - The timestamp when the record was last updated (including play count). plays - The number of plays the user has recorded for this song and chart. location - The ID of the machine that this score was earned on. data - Any optional data that a game class recorded with this score. """ self.key = key self.id = songid self.chart = songchart self.points = points self.timestamp = timestamp self.update = update self.location = location self.plays = plays self.data = ValidatedDict(data)
def get_all_player_info(self, userids: List[UserID], limit: Optional[int]=None, allow_remote: bool=False) -> Dict[UserID, Dict[int, Dict[str, Any]]]: info: Dict[UserID, Dict[int, Dict[str, Any]]] = {} playstats: Dict[UserID, ValidatedDict] = {} # Find all versions of the users' profiles, sorted newest to oldest. versions = sorted([version for (game, version, name) in self.all_games()], reverse=True) for userid in userids: info[userid] = {} userlimit = limit for version in versions: if allow_remote: profile = self.data.remote.user.get_profile(self.game, version, userid) else: profile = self.data.local.user.get_profile(self.game, version, userid) if profile is not None: if userid not in playstats: stats = self.data.local.game.get_settings(self.game, userid) if stats is None: stats = ValidatedDict() playstats[userid] = stats info[userid][version] = self.format_profile(profile, playstats[userid]) info[userid][version]['remote'] = RemoteUser.is_remote(userid) # Exit out if we've hit the limit if userlimit is not None: userlimit = userlimit - 1 if userlimit == 0: break return info
def get_settings(self, arcadeid: ArcadeID, game: str, version: int, setting: str) -> Optional[ValidatedDict]: """ Given an arcade and a game/version combo, look up this particular setting. Parameters: arcadeid - Integer specifying the arcade to delete. game - String identifying a game series. version - String identifying a game version. setting - String identifying the particular setting we're interestsed in. Returns: A dictionary representing game settings, or None if there are no settings for this game/user. """ sql = "SELECT data FROM arcade_settings WHERE arcadeid = :id AND game = :game AND version = :version AND type = :type" cursor = self.execute(sql, { 'id': arcadeid, 'game': game, 'version': version, 'type': setting }) if cursor.rowcount != 1: # Settings doesn't exist return None result = cursor.fetchone() return ValidatedDict(self.deserialize(result['data']))
def format_flags(self, settings_dict: ValidatedDict) -> Dict[str, Any]: flags = settings_dict.get_int('flags') return { 'grade': (flags & 0x001) != 0, 'status': (flags & 0x002) != 0, 'difficulty': (flags & 0x004) != 0, 'alphabet': (flags & 0x008) != 0, 'rival_played': (flags & 0x010) != 0, 'rival_win_lose': (flags & 0x040) != 0, 'rival_info': (flags & 0x080) != 0, 'hide_play_count': (flags & 0x100) != 0, 'disable_song_preview': settings_dict.get_int('disable_song_preview') != 0, 'effector_lock': settings_dict.get_int('effector_lock') != 0, }
def get_server_info(self) -> ValidatedDict: resp = self.__exchange_data('', {}) return ValidatedDict({ 'name': resp['name'], 'email': resp['email'], 'versions': resp['versions'], })
def get_all_profiles(self, game: str, version: int) -> List[Tuple[UserID, ValidatedDict]]: """ Given a game/version, look up all user profiles for that game. Parameters: game - String identifier of the game we want all user profiles for. version - Integer version of the game we want all user profiles for. Returns: A list of (UserID, dictionaries) previously stored by a game class for each profile. """ sql = ( "SELECT refid.userid AS userid, refid.refid AS refid, extid.extid AS extid, profile.data AS data " "FROM refid, profile, extid " "WHERE refid.game = :game AND refid.version = :version " "AND refid.refid = profile.refid AND extid.game = refid.game AND extid.userid = refid.userid" ) cursor = self.execute(sql, {'game': game, 'version': version}) profiles = [] for result in cursor.fetchall(): profile = { 'refid': result['refid'], 'extid': result['extid'], 'game': game, 'version': version, } profile.update(self.deserialize(result['data'])) profiles.append(( UserID(result['userid']), ValidatedDict(profile), )) return profiles
def __init__( self, key: int, songid: int, songchart: int, points: int, timestamp: int, location: int, new_record: bool, data: Dict[str, Any], ) -> None: """ Initialize the score object. Parameters: key - A unique key identifying this exact attempt. songid - The song's ID according to the game. songchart - The song's chart number, according to the game. points - The points achieved on this song, from the DB. timestamp - The timestamp of the attempt. location - The ID of the machine that this score was earned on. new_record - Whether this attempt resulted in a new record for this user. data - Any optional data that a game class recorded with this score. """ self.key = key self.id = songid self.chart = songchart self.points = points self.timestamp = timestamp self.location = location self.new_record = new_record self.data = ValidatedDict(data)
def get_link(self, game: str, version: int, userid: UserID, linktype: str, other_userid: UserID) -> Optional[ValidatedDict]: """ Given a game/version/userid and link type + other userid, find that link. Note that there can be more than one link with the same user IDs and game/version as long as each one is a different type. Parameters: game - String identifier of the game looking up the user. version - Integer version of the game looking up the user. userid - Integer user ID, as looked up by one of the above functions. linktype - The type of link. other_userid - Integer user ID of the account we're linked to. Returns: A dictionary as stored by a game class previously, or None if not found. """ sql = ( "SELECT data FROM link WHERE game = :game AND version = :version AND userid = :userid AND type = :type AND other_userid = :other_userid" ) cursor = self.execute(sql, {'game': game, 'version': version, 'userid': userid, 'type': linktype, 'other_userid': other_userid}) if cursor.rowcount != 1: # score doesn't exist return None result = cursor.fetchone() return ValidatedDict(self.deserialize(result['data']))
def get_all_lobbies(self, game: str, version: int) -> List[Tuple[UserID, ValidatedDict]]: """ Given a game and version, look up all active lobbies. Parameters: game - String identifying a game series. version - Integer identifying the version of the game in the series. Returns: A list of dictionaries representing lobby info stored by a game class. """ sql = ("SELECT userid, id, data FROM lobby " "WHERE game = :game AND version = :version AND time > :time") cursor = self.execute( sql, { 'game': game, 'version': version, 'time': Time.now() - Time.SECONDS_IN_HOUR, }, ) ret = [] for result in cursor.fetchall(): data = ValidatedDict(self.deserialize(result['data'])) data['id'] = result['id'] ret.append((UserID(result['userid']), data)) return ret
def new_profile_by_refid(self, refid: Optional[str], name: Optional[str], pid: Optional[int]) -> ValidatedDict: """ Given a RefID and an optional name, create a profile and then return that newly created profile. """ if refid is None: return None if name is None: name = 'なし' if pid is None: pid = 51 userid = self.data.remote.user.from_refid(self.game, self.version, refid) defaultprofile = ValidatedDict({ 'name': name, 'pid': pid, 'settings': { 'flags': 223 # Default to turning on all optional folders }, }) self.put_profile(userid, defaultprofile) profile = self.get_profile(userid) return profile
def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node: # Look up user by refid refid = request.child_value('data/eaid') userid = self.data.remote.user.from_refid(self.game, self.version, refid) if userid is None: root = Node.void('playerdata') root.add_child(Node.s32( 'result', 1)) # Unclear if this is the right thing to do here. return root # Extract new profile info from old profile oldprofile = self.get_profile(userid) is_new = False if oldprofile is None: oldprofile = ValidatedDict() is_new = True newprofile = self.unformat_profile(userid, request, oldprofile, is_new) # Write new profile self.put_profile(userid, newprofile) # Return success! root = Node.void('playerdata') root.add_child(Node.s32('result', 0)) return root