from typing import Any, List, Dict import logging import inflection from math import floor from datetime import datetime, timedelta from core.config import CoreConfig from titles.wacca.config import WaccaConfig from titles.wacca.const import WaccaConstants from titles.wacca.database import WaccaData from titles.wacca.handlers import * from core.const import AllnetCountryCode class WaccaBase: def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: self.config = cfg # Config file self.game_config = game_cfg # Game Config file self.game = WaccaConstants.GAME_CODE # Game code self.version = WaccaConstants.VER_WACCA # Game version self.data = WaccaData(cfg) # Database self.logger = logging.getLogger("wacca") self.srvtime = datetime.now() self.season = 1 self.OPTIONS_DEFAULTS: Dict[str, Any] = { "note_speed": 5, "field_mask": 0, "note_sound": 105001, "note_color": 203001, "bgm_volume": 10, "bg_video": 0, "mirror": 0, "judge_display_pos": 0, "judge_detail_display": 0, "measure_guidelines": 1, "guideline_mask": 1, "judge_line_timing_adjust": 10, "note_design": 3, "bonus_effect": 1, "chara_voice": 1, "score_display_method": 0, "give_up": 0, "guideline_spacing": 1, "center_display": 1, "ranking_display": 1, "stage_up_icon_display": 1, "rating_display": 1, "player_level_display": 1, "touch_effect": 1, "guide_sound_vol": 3, "touch_note_vol": 8, "hold_note_vol": 8, "slide_note_vol": 8, "snap_note_vol": 8, "chain_note_vol": 8, "bonus_note_vol": 8, "gate_skip": 0, "key_beam_display": 1, "left_slide_note_color": 4, "right_slide_note_color": 3, "forward_slide_note_color": 1, "back_slide_note_color": 2, "master_vol": 3, "set_title_id": 104001, "set_icon_id": 102001, "set_nav_id": 210001, "set_plate_id": 211001, } self.allowed_stages = [] prefecture_name = ( inflection.underscore(game_cfg.server.prefecture_name) .replace(" ", "_") .upper() ) if prefecture_name not in [region.name for region in WaccaConstants.Region]: self.logger.warning( f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file" ) self.region_id = WaccaConstants.Region.HOKKAIDO else: self.region_id = WaccaConstants.Region[prefecture_name] async def handle_housing_get_request(self, data: Dict) -> Dict: req = BaseRequest(data) housing_id = 1337 self.logger.info(f"{req.chipId} -> {housing_id}") resp = HousingGetResponse(housing_id) return resp.make() async def handle_advertise_GetRanking_request(self, data: Dict) -> Dict: req = AdvertiseGetRankingRequest(data) return AdvertiseGetRankingResponse().make() async def handle_housing_start_request(self, data: Dict) -> Dict: req = HousingStartRequestV1(data) allnet_region_id = None machine = await self.data.arcade.get_machine(req.chipId) if machine is not None: arcade = await self.data.arcade.get_arcade(machine["arcade"]) allnet_region_id = arcade["region_id"] if req.appVersion.country == AllnetCountryCode.JAPAN.value: if allnet_region_id is not None: region = WaccaConstants.allnet_region_id_to_wacca_region( allnet_region_id ) if region is None: region_id = self.region_id else: region_id = region else: region_id = self.region_id elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES: region_id = WaccaConstants.Region[req.appVersion.country] else: region_id = WaccaConstants.Region.NONE resp = HousingStartResponseV1(region_id) return resp.make() async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV1() return resp.make() async def handle_user_status_logout_request(self, data: Dict) -> Dict: req = UserStatusLogoutRequest(data) self.logger.info(f"Log out user {req.userId} from {req.chipId}") return BaseResponse().make() async def handle_user_status_get_request(self, data: Dict) -> Dict: req = UserStatusGetRequest(data) resp = UserStatusGetV1Response() profile = await self.data.profile.get_profile(aime_id=req.aimeId) if profile is None: self.logger.info(f"No user exists for aime id {req.aimeId}") resp.profileStatus = ProfileStatus.ProfileRegister return resp.make() self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") if profile["last_game_ver"] is None: resp.lastGameVersion = ShortVersion(str(req.appVersion)) else: resp.lastGameVersion = ShortVersion(profile["last_game_ver"]) resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] resp.userStatus.danLevel = profile["dan_level"] resp.userStatus.danType = profile["dan_type"] resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] set_title_id = await self.data.profile.get_options( WaccaConstants.OPTIONS["set_title_id"], profile["user"] ) if set_title_id is None: set_title_id = self.OPTIONS_DEFAULTS["set_title_id"] resp.setTitleId = set_title_id set_icon_id = await self.data.profile.get_options( WaccaConstants.OPTIONS["set_title_id"], profile["user"] ) if set_icon_id is None: set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] resp.setIconId = set_icon_id if req.appVersion > resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionUpgrade elif req.appVersion < resp.lastGameVersion: resp.versionStatus = PlayVersionStatus.VersionTooNew return resp.make() async def handle_user_status_login_request(self, data: Dict) -> Dict: req = UserStatusLoginRequest(data) resp = UserStatusLoginResponseV1() is_consec_day = True if req.userId == 0: self.logger.info(f"Guest login on {req.chipId}") resp.lastLoginDate = 0 else: profile = await self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning( f"Unknown user id {req.userId} attempted login from {req.chipId}" ) return resp.make() self.logger.info(f"User {req.userId} login on {req.chipId}") last_login_time = int(profile["last_login_date"].timestamp()) resp.lastLoginDate = last_login_time midnight_today_ts = int( datetime.now() .replace(hour=0, minute=0, second=0, microsecond=0) .timestamp() ) # If somebodies login timestamp < midnight of current day, then they are logging in for the first time today if last_login_time < midnight_today_ts: resp.firstLoginDaily = True # If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak if midnight_today_ts - last_login_time > 86400: is_consec_day = False await self.data.profile.session_login( req.userId, resp.firstLoginDaily, is_consec_day ) if resp.firstLoginDaily: # TODO: Daily bonus pass # TODO: VIP dialy/monthly rewards return resp.make() async def handle_user_status_create_request(self, data: Dict) -> Dict: req = UserStatusCreateRequest(data) profileId = await self.data.profile.create_profile( req.aimeId, req.username, self.version ) if profileId is None: return BaseResponse().make() if profileId == 0: # We've already made this profile, just return success new_user = await self.data.profile.get_profile(aime_id=req.aimeId) profileId = new_user['id'] # Insert starting items await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001) await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002) await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003) await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005) await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001) await self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002) await self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001 ) await self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001 ) await self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001 ) await self.data.item.put_item( req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001 ) return UserStatusCreateResponseV2(profileId, req.username).make() async def handle_user_status_getDetail_request(self, data: Dict) -> Dict: req = UserStatusGetDetailRequest(data) resp = UserStatusGetDetailResponseV1() profile = await self.data.profile.get_profile(req.userId) if profile is None: self.logger.warning(f"Unknown profile {req.userId}") return resp.make() self.logger.info(f"Get detail for profile {req.userId}") user_id = profile["user"] profile_scores = await self.data.score.get_best_scores(user_id) profile_items = await self.data.item.get_items(user_id) profile_song_unlocks = await self.data.item.get_song_unlocks(user_id) profile_options = await self.data.profile.get_options(user_id) profile_trophies = await self.data.item.get_trophies(user_id) profile_tickets = await self.data.item.get_tickets(user_id) resp.songUpdateTime = int(profile["last_login_date"].timestamp()) resp.songPlayStatus = [profile["last_song_id"], 1] resp.userStatus.userId = profile["id"] resp.userStatus.username = profile["username"] resp.userStatus.xp = profile["xp"] resp.userStatus.danLevel = profile["dan_level"] resp.userStatus.danType = profile["dan_type"] resp.userStatus.wp = profile["wp"] resp.userStatus.useCount = profile["login_count"] if self.game_config.mods.infinite_wp: resp.userStatus.wp = 999999 if profile["friend_view_1"] is not None: pass if profile["friend_view_2"] is not None: pass if profile["friend_view_3"] is not None: pass resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 1, profile["playcount_single"]) ) resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 2, profile["playcount_multi_vs"]) ) resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 3, profile["playcount_multi_coop"]) ) resp.seasonalPlayModeCounts.append( PlayModeCounts(self.season, 4, profile["playcount_stageup"]) ) for opt in profile_options: resp.options.append(UserOption(opt["opt_id"], opt["value"])) for unlock in profile_song_unlocks: for x in range(1, unlock["highest_difficulty"] + 1): resp.userItems.songUnlocks.append( SongUnlock( unlock["song_id"], x, 0, int(unlock["acquire_date"].timestamp()) ) ) for song in profile_scores: resp.seasonInfo.cumulativeScore += song["score"] clear_cts = SongDetailClearCounts( song["play_ct"], song["clear_ct"], song["missless_ct"], song["fullcombo_ct"], song["allmarv_ct"], ) grade_cts = SongDetailGradeCountsV1( song["grade_d_ct"], song["grade_c_ct"], song["grade_b_ct"], song["grade_a_ct"], song["grade_aa_ct"], song["grade_aaa_ct"], song["grade_s_ct"], song["grade_ss_ct"], song["grade_sss_ct"], song["grade_master_ct"], ) deets = BestScoreDetailV1(song["song_id"], song["chart_id"]) deets.clearCounts = clear_cts deets.clearCountsSeason = clear_cts deets.gradeCounts = grade_cts deets.score = song["score"] deets.bestCombo = song["best_combo"] deets.lowestMissCtMaybe = song["lowest_miss_ct"] deets.rating = song["rating"] resp.scores.append(deets) for trophy in profile_trophies: resp.userItems.trophies.append( TrophyItem( trophy["trophy_id"], trophy["season"], trophy["progress"], trophy["badge_type"], ) ) if self.game_config.mods.infinite_tickets: for x in range(5): resp.userItems.tickets.append(TicketItem(x, 106002, 0)) else: for ticket in profile_tickets: if ticket["expire_date"] is None: expire = int((self.srvtime + timedelta(days=30)).timestamp()) else: expire = int(ticket["expire_date"].timestamp()) resp.userItems.tickets.append( TicketItem(ticket["id"], ticket["ticket_id"], expire) ) if profile_items: for item in profile_items: try: if item["type"] == WaccaConstants.ITEM_TYPES["icon"]: resp.userItems.icons.append( IconItem( item["item_id"], 1, item["use_count"], int(item["acquire_date"].timestamp()), ) ) else: itm_send = GenericItemSend( item["item_id"], 1, int(item["acquire_date"].timestamp()) ) if item["type"] == WaccaConstants.ITEM_TYPES["title"]: resp.userItems.titles.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["note_color"]: resp.userItems.noteColors.append(itm_send) elif item["type"] == WaccaConstants.ITEM_TYPES["note_sound"]: resp.userItems.noteSounds.append(itm_send) except Exception: self.logger.error( f"{__name__} Failed to load item {item['item_id']} for user {profile['user']}" ) resp.seasonInfo.level = profile["xp"] resp.seasonInfo.wpObtained = profile["wp_total"] resp.seasonInfo.wpSpent = profile["wp_spent"] resp.seasonInfo.titlesObtained = len(resp.userItems.titles) resp.seasonInfo.iconsObtained = len(resp.userItems.icons) resp.seasonInfo.noteColorsObtained = len(resp.userItems.noteColors) resp.seasonInfo.noteSoundsObtained = len(resp.userItems.noteSounds) return resp.make() async def handle_user_trial_get_request(self, data: Dict) -> Dict: req = UserTrialGetRequest(data) resp = UserTrialGetResponse() user_id = await self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: self.logger.error( f"handle_user_trial_get_request: No profile with id {req.profileId}" ) return resp.make() self.logger.info(f"Get trial info for user {req.profileId}") stages = await self.data.score.get_stageup(user_id, self.version) if stages is None: stages = [] tmp: List[StageInfo] = [] for d in self.allowed_stages: stage_info = StageInfo(d[0], d[1]) for score in stages: if score["stage_id"] == stage_info.danId: stage_info.clearStatus = score["clear_status"] stage_info.numSongCleared = score["clear_song_ct"] stage_info.song1BestScore = score["song1_score"] stage_info.song2BestScore = score["song2_score"] stage_info.song3BestScore = score["song3_score"] break tmp.append(stage_info) for x in range(len(tmp)): if tmp[x].danLevel >= 10 and ( tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1 ): resp.stageList.append(tmp[x]) elif tmp[x].danLevel < 10: resp.stageList.append(tmp[x]) return resp.make() async def handle_user_trial_update_request(self, data: Dict) -> Dict: req = UserTrialUpdateRequest(data) total_score = 0 for score in req.songScores: total_score += score while len(req.songScores) < 3: req.songScores.append(0) profile = await self.data.profile.get_profile(req.profileId) user_id = profile["user"] old_stage = await self.data.score.get_stageup_stage( user_id, self.version, req.stageId ) if old_stage is None: await self.data.score.put_stageup( user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, req.songScores[0], req.songScores[1], req.songScores[2], ) else: # We only care about total score for best of, even if one score happens to be lower (I think) if total_score > ( old_stage["song1_score"] + old_stage["song2_score"] + old_stage["song3_score"] ): best_score1 = req.songScores[0] best_score2 = req.songScores[1] best_score3 = req.songScores[2] else: best_score1 = old_stage["song1_score"] best_score2 = old_stage["song2_score"] best_score3 = old_stage["song3_score"] await self.data.score.put_stageup( user_id, self.version, req.stageId, req.clearType.value, req.numSongsCleared, best_score1, best_score2, best_score3, ) if ( req.stageLevel > 0 and req.stageLevel <= 14 and req.clearType.value > 0 ): # For some reason, special stages send dan level 1001... if req.stageLevel > profile["dan_level"] or ( req.stageLevel == profile["dan_level"] and req.clearType.value > profile["dan_type"] ): await self.data.profile.update_profile_dan( req.profileId, req.stageLevel, req.clearType.value ) await self.util_put_items(req.profileId, user_id, req.itemsObtained) # user/status/update isn't called after stageup so we have to do some things now current_icon = await self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_icon_id"] ) current_nav = await self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_nav_id"] ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] else: current_icon = current_icon["value"] if current_nav is None: current_nav = self.OPTIONS_DEFAULTS["set_nav_id"] else: current_nav = current_nav["value"] await self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon ) await self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav ) await self.data.profile.update_profile_playtype( req.profileId, 4, data["appVersion"][:7] ) return BaseResponse().make() async def handle_user_sugoroku_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") resp = BaseResponse() if int(ver_split[0]) <= 2 and int(ver_split[1]) < 53: req = UserSugarokuUpdateRequestV1(data) mission_flg = 0 else: req = UserSugarokuUpdateRequestV2(data) mission_flg = req.mission_flag user_id = await self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: self.logger.info( f"handle_user_sugoroku_update_request unknwon profile ID {req.profileId}" ) return resp.make() await self.util_put_items(req.profileId, user_id, req.itemsObtainted) await self.data.profile.update_gate( user_id, req.gateId, req.page, req.progress, req.loops, mission_flg, req.totalPts, ) return resp.make() async def handle_user_info_getMyroom_request(self, data: Dict) -> Dict: return UserInfogetMyroomResponseV1().make() async def handle_user_music_unlock_request(self, data: Dict) -> Dict: req = UserMusicUnlockRequest(data) profile = await self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() user_id = profile["user"] current_wp = profile["wp"] tickets = await self.data.item.get_tickets(user_id) new_tickets: List[TicketItem] = [] for ticket in tickets: new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)) for item in req.itemsUsed: if ( item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp ): if current_wp >= item.quantity: current_wp -= item.quantity await self.data.profile.spend_wp(req.profileId, item.quantity) else: return BaseResponse().make() elif ( item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets ): for x in range(len(new_tickets)): if new_tickets[x].ticketId == item.itemId: self.logger.debug( f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}" ) await self.data.item.spend_ticket(new_tickets[x].userTicketId) new_tickets.pop(x) break # wp, ticket info if req.difficulty > WaccaConstants.Difficulty.HARD.value: old_score = await self.data.score.get_best_score( user_id, req.songId, req.difficulty ) if not old_score: await self.data.score.put_best_score( user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0 ) await self.data.item.unlock_song( user_id, req.songId, req.difficulty if req.difficulty > WaccaConstants.Difficulty.HARD.value else WaccaConstants.Difficulty.HARD.value, ) if self.game_config.mods.infinite_tickets: for x in range(5): new_tickets.append(TicketItem(x, 106002, 0)) if self.game_config.mods.infinite_wp: current_wp = 999999 return UserMusicUnlockResponse(current_wp, new_tickets).make() async def handle_user_info_getRanking_request(self, data: Dict) -> Dict: # total score, high score by song, cumulative socre, stage up score, other score, WP ranking # This likely requies calculating standings at regular intervals and caching the results return UserInfogetRankingResponse().make() async def handle_user_music_update_request(self, data: Dict) -> Dict: ver_split = data["appVersion"].split(".") if int(ver_split[0]) >= 3: resp = UserMusicUpdateResponseV3() req = UserMusicUpdateRequestV2(data) elif int(ver_split[0]) >= 2: resp = UserMusicUpdateResponseV2() req = UserMusicUpdateRequestV2(data) else: resp = UserMusicUpdateResponseV1() req = UserMusicUpdateRequestV1(data) resp.songDetail.songId = req.songDetail.songId resp.songDetail.difficulty = req.songDetail.difficulty if req.profileId == 0: self.logger.info( f"Guest score for song {req.songDetail.songId} difficulty {req.songDetail.difficulty}" ) return resp.make() profile = await self.data.profile.get_profile(req.profileId) if profile is None: self.logger.warning( f"handle_user_music_update_request: No profile for game_id {req.profileId}" ) return resp.make() user_id = profile["user"] await self.util_put_items(req.profileId, user_id, req.itemsObtained) playlog_clear_status = ( req.songDetail.flagCleared + req.songDetail.flagMissless + req.songDetail.flagFullcombo + req.songDetail.flagAllMarvelous ) await self.data.score.put_playlog( user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, playlog_clear_status, req.songDetail.grade.value, req.songDetail.maxCombo, req.songDetail.judgements.marvCt, req.songDetail.judgements.greatCt, req.songDetail.judgements.goodCt, req.songDetail.judgements.missCt, req.songDetail.fastCt, req.songDetail.slowCt, self.season, ) old_score = await self.data.score.get_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty ) if not old_score: grades = [0] * 13 clears = [0] * 5 clears[0] = 1 clears[1] = 1 if req.songDetail.flagCleared else 0 clears[2] = 1 if req.songDetail.flagMissless else 0 clears[3] = 1 if req.songDetail.flagFullcombo else 0 clears[4] = 1 if req.songDetail.flagAllMarvelous else 0 grades[req.songDetail.grade.value - 1] = 1 await self.data.score.put_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty, req.songDetail.score, clears, grades, req.songDetail.maxCombo, req.songDetail.judgements.missCt, ) resp.songDetail.score = req.songDetail.score resp.songDetail.lowestMissCount = req.songDetail.judgements.missCt else: grades = [ old_score["grade_d_ct"], old_score["grade_c_ct"], old_score["grade_b_ct"], old_score["grade_a_ct"], old_score["grade_aa_ct"], old_score["grade_aaa_ct"], old_score["grade_s_ct"], old_score["grade_ss_ct"], old_score["grade_sss_ct"], old_score["grade_master_ct"], old_score["grade_sp_ct"], old_score["grade_ssp_ct"], old_score["grade_sssp_ct"], ] clears = [ old_score["play_ct"], old_score["clear_ct"], old_score["missless_ct"], old_score["fullcombo_ct"], old_score["allmarv_ct"], ] clears[0] += 1 clears[1] += 1 if req.songDetail.flagCleared else 0 clears[2] += 1 if req.songDetail.flagMissless else 0 clears[3] += 1 if req.songDetail.flagFullcombo else 0 clears[4] += 1 if req.songDetail.flagAllMarvelous else 0 grades[req.songDetail.grade.value - 1] += 1 best_score = max(req.songDetail.score, old_score["score"]) best_max_combo = max(req.songDetail.maxCombo, old_score["best_combo"]) lowest_miss_ct = min( req.songDetail.judgements.missCt, old_score["lowest_miss_ct"] ) best_rating = max( self.util_calc_song_rating(req.songDetail.score, req.songDetail.level), old_score["rating"], ) await self.data.score.put_best_score( user_id, req.songDetail.songId, req.songDetail.difficulty, best_score, clears, grades, best_max_combo, lowest_miss_ct, ) resp.songDetail.score = best_score resp.songDetail.lowestMissCount = lowest_miss_ct resp.songDetail.rating = best_rating resp.songDetail.clearCounts = SongDetailClearCounts(counts=clears) resp.songDetail.clearCountsSeason = SongDetailClearCounts(counts=clears) if int(ver_split[0]) >= 3: resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades) else: resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades) resp.songDetail.lockState = 1 return resp.make() # TODO: Coop and vs data async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] return self.handle_user_music_update_request(data) async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] return self.handle_user_music_update_request(data) async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: return self.handle_user_music_update_request(data) async def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data) page_status = req.params[1][1] profile = await self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() if len(req.itemsObtained) > 0: await self.util_put_items(req.profileId, profile["user"], req.itemsObtained) await self.data.profile.update_bingo( profile["user"], req.bingoDetail.pageNumber, page_status ) await self.data.profile.update_tutorial_flags(req.profileId, req.params[3]) return BaseResponse().make() async def handle_user_goods_purchase_request(self, data: Dict) -> Dict: req = UserGoodsPurchaseRequest(data) resp = UserGoodsPurchaseResponse() profile = await self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() user_id = profile["user"] resp.currentWp = profile["wp"] if ( req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp ): resp.currentWp -= req.cost await self.data.profile.spend_wp(req.profileId, req.cost) elif req.purchaseType == PurchaseType.PurchaseTypeCredit: self.logger.info( f"User {req.profileId} Purchased item {req.itemObtained.itemType} id {req.itemObtained.itemId} for {req.cost} credits on machine {req.chipId}" ) await self.util_put_items(req.profileId, user_id, [req.itemObtained]) if self.game_config.mods.infinite_tickets: for x in range(5): resp.tickets.append(TicketItem(x, 106002, 0)) else: tickets = await self.data.item.get_tickets(user_id) for ticket in tickets: resp.tickets.append( TicketItem( ticket["id"], ticket["ticket_id"], int((self.srvtime + timedelta(days=30)).timestamp()), ) ) if self.game_config.mods.infinite_wp: resp.currentWp = 999999 return resp.make() async def handle_competition_status_login_request(self, data: Dict) -> Dict: return BaseResponse().make() async def handle_competition_status_update_request(self, data: Dict) -> Dict: return BaseResponse().make() async def handle_user_rating_update_request(self, data: Dict) -> Dict: req = UserRatingUpdateRequest(data) user_id = await self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: self.logger.error( f"handle_user_rating_update_request: No profild with ID {req.profileId}" ) return BaseResponse().make() for song in req.songs: await self.data.score.update_song_rating( user_id, song.songId, song.difficulty, song.rating ) await self.data.profile.update_user_rating(req.profileId, req.totalRating) return BaseResponse().make() async def handle_user_status_update_request(self, data: Dict) -> Dict: req = UserStatusUpdateRequestV1(data) user_id = await self.data.profile.profile_to_aime_user(req.profileId) if user_id is None: self.logger.info( f"handle_user_status_update_request: No profile with ID {req.profileId}" ) return BaseResponse().make() await self.util_put_items(req.profileId, user_id, req.itemsRecieved) await self.data.profile.update_profile_playtype( req.profileId, req.playType.value, data["appVersion"][:7] ) current_icon = await self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_icon_id"] ) current_nav = await self.data.profile.get_options( user_id, WaccaConstants.OPTIONS["set_nav_id"] ) if current_icon is None: current_icon = self.OPTIONS_DEFAULTS["set_icon_id"] else: current_icon = current_icon["value"] if current_nav is None: current_nav = self.OPTIONS_DEFAULTS["set_nav_id"] else: current_nav = current_nav["value"] await self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon ) await self.data.item.put_item( user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav ) return BaseResponse().make() async def handle_user_info_update_request(self, data: Dict) -> Dict: req = UserInfoUpdateRequest(data) user_id = await self.data.profile.profile_to_aime_user(req.profileId) for opt in req.optsUpdated: await self.data.profile.update_option(user_id, opt.optId, opt.optVal) for update in req.datesUpdated: pass for fav in req.favoritesAdded: await self.data.profile.add_favorite_song(user_id, fav) for unfav in req.favoritesRemoved: await self.data.profile.remove_favorite_song(user_id, unfav) return BaseResponse().make() async def handle_user_vip_get_request(self, data: Dict) -> Dict: req = UserVipGetRequest(data) resp = UserVipGetResponse() profile = await self.data.profile.get_profile(req.profileId) if profile is None: self.logger.warning( f"handle_user_vip_get_request no profile with ID {req.profileId}" ) return BaseResponse().make() if ( "vip_expire_time" in profile and profile["vip_expire_time"] is not None and profile["vip_expire_time"].timestamp() > int(self.srvtime.timestamp()) ): resp.vipDays = int( (profile["vip_expire_time"].timestamp() - int(self.srvtime.timestamp())) / 86400 ) resp.vipDays += 30 resp.presents.append(VipLoginBonus(1, 0, 16, 211025, 1)) resp.presents.append(VipLoginBonus(2, 0, 6, 202086, 1)) resp.presents.append(VipLoginBonus(3, 0, 11, 205008, 1)) resp.presents.append(VipLoginBonus(4, 0, 10, 203009, 1)) resp.presents.append(VipLoginBonus(5, 0, 16, 211026, 1)) resp.presents.append(VipLoginBonus(6, 0, 9, 206001, 1)) return resp.make() async def handle_user_vip_start_request(self, data: Dict) -> Dict: req = UserVipStartRequest(data) profile = await self.data.profile.get_profile(req.profileId) if profile is None: return BaseResponse().make() # This should never happen because wacca stops you from buying VIP # if you have more then 10 days remaining, but this IS wacca we're dealing with... if ( "always_vip" in profile and profile["always_vip"] or self.game_config.mods.always_vip ): return UserVipStartResponse( int((self.srvtime + timedelta(days=req.days)).timestamp()) ).make() vip_exp_time = self.srvtime + timedelta(days=req.days) await self.data.profile.update_vip_time(req.profileId, vip_exp_time) return UserVipStartResponse(int(vip_exp_time.timestamp())).make() async def util_put_items( self, profile_id: int, user_id: int, items_obtained: List[GenericItemRecv] ) -> None: if user_id is None or profile_id <= 0: return None if items_obtained: for item in items_obtained: if item.itemType == WaccaConstants.ITEM_TYPES["xp"]: await self.data.profile.add_xp(profile_id, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["wp"]: await self.data.profile.add_wp(profile_id, item.quantity) elif ( item.itemType == WaccaConstants.ITEM_TYPES["music_difficulty_unlock"] or item.itemType == WaccaConstants.ITEM_TYPES["music_unlock"] ): if item.quantity > WaccaConstants.Difficulty.HARD.value: old_score = await self.data.score.get_best_score( user_id, item.itemId, item.quantity ) if not old_score: await self.data.score.put_best_score( user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0, ) if item.quantity == 0: item.quantity = WaccaConstants.Difficulty.HARD.value await self.data.item.unlock_song(user_id, item.itemId, item.quantity) elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]: await self.data.item.add_ticket(user_id, item.itemId) elif item.itemType == WaccaConstants.ITEM_TYPES["trophy"]: await self.data.item.update_trophy( user_id, item.itemId, self.season, item.quantity, 0 ) else: await self.data.item.put_item(user_id, item.itemType, item.itemId) def util_calc_song_rating(self, score: int, difficulty: float) -> int: if score >= 990000: const = 4.00 elif score >= 980000 and score < 990000: const = 3.75 elif score >= 970000 and score < 980000: const = 3.50 elif score >= 960000 and score < 970000: const = 3.25 elif score >= 950000 and score < 960000: const = 3.00 elif score >= 940000 and score < 950000: const = 2.75 elif score >= 930000 and score < 940000: const = 2.50 elif score >= 920000 and score < 930000: const = 2.25 elif score >= 910000 and score < 920000: const = 2.00 elif score >= 900000 and score < 910000: const = 1.00 else: const = 0.00 return floor((difficulty * const) * 10)