add back games, conform them to new title dispatch

This commit is contained in:
Hay1tsme
2023-02-17 01:02:21 -05:00
parent f18e939dd0
commit 7e3396a7ff
214 changed files with 19412 additions and 23 deletions

18
titles/wacca/__init__.py Normal file
View File

@ -0,0 +1,18 @@
from titles.wacca.const import WaccaConstants
from titles.wacca.index import WaccaServlet
from titles.wacca.read import WaccaReader
from titles.wacca.database import WaccaData
index = WaccaServlet
database = WaccaData
reader = WaccaReader
use_default_title = True
include_protocol = True
title_secure = False
game_codes = [WaccaConstants.GAME_CODE]
trailing_slash = False
use_default_host = False
host = ""
current_schema_version = 3

941
titles/wacca/base.py Normal file
View File

@ -0,0 +1,941 @@
from typing import Any, List, Dict
import logging
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 *
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 = []
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()
def handle_housing_start_request(self, data: Dict) -> Dict:
req = HousingStartRequest(data)
resp = HousingStartResponseV1(
1,
[ # Recomended songs
1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32,
1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119,
1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275,
1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247,
1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030,
1023,1015
]
)
return resp.make()
def handle_advertise_GetNews_request(self, data: Dict) -> Dict:
resp = GetNewsResponseV1()
return resp.make()
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()
def handle_user_status_login_request(self, data: Dict) -> List[Any]:
req = UserStatusLoginRequest(data)
resp = UserStatusLoginResponseV1()
is_new_day = False
is_consec_day = False
is_consec_day = True
if req.userId == 0:
self.logger.info(f"Guest login on {req.chipId}")
resp.lastLoginDate = 0
else:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(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
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()):
is_new_day = True
is_consec_day = True
# If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak
elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()):
is_consec_day = False
# else, they are simply logging in again on the same day, and we don't need to do anything for that
self.data.profile.session_login(req.userId, is_new_day, is_consec_day)
resp.firstLoginDaily = int(is_new_day)
return resp.make()
def handle_user_status_get_request(self, data: Dict) -> List[Any]:
req = UserStatusGetRequest(data)
resp = UserStatusGetV1Response()
ver_split = req.appVersion.split(".")
profile = 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}")
return resp.make()
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None:
profile_ver_split = ver_split
resp.lastGameVersion = req.appVersion
else:
profile_ver_split = profile["last_game_ver"].split(".")
resp.lastGameVersion = 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"]
resp.userStatus.loginDays = profile["login_count_days"]
resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"]
set_title_id = 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 = 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 profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()):
resp.userStatus.loginConsecutiveDays = 0
if int(ver_split[0]) > int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[1]) > int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[1]) < int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[2]) > int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[2]) < int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
if profile["always_vip"]:
resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp())
elif profile["vip_expire_time"] is not None:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
return resp.make()
def handle_user_status_login_request(self, data: Dict) -> List[Any]:
req = UserStatusLoginRequest(data)
resp = UserStatusLoginResponseV2()
is_new_day = False
is_consec_day = False
is_consec_day = True
if req.userId == 0:
self.logger.info(f"Guest login on {req.chipId}")
resp.lastLoginDate = 0
else:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(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
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()):
is_new_day = True
is_consec_day = True
# If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak
elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()):
is_consec_day = False
# else, they are simply logging in again on the same day, and we don't need to do anything for that
self.data.profile.session_login(req.userId, is_new_day, is_consec_day)
resp.vipInfo.pageYear = datetime.now().year
resp.vipInfo.pageMonth = datetime.now().month
resp.vipInfo.pageDay = datetime.now().day
resp.vipInfo.numItem = 1
resp.firstLoginDaily = int(is_new_day)
return resp.make()
def handle_user_status_create_request(self, data: Dict) -> List[Any]:
req = UserStatusCreateRequest(data)
profileId = self.data.profile.create_profile(req.aimeId, req.username, self.version)
if profileId is None: return BaseResponse().make()
# Insert starting items
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104002)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104003)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["title"], 104005)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["icon"], 102002)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 103001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_color"], 203001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 105001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["note_sound"], 205005) # Added lily
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210001)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["user_plate"], 211001) # Added lily
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312000) # Added reverse
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001) # Added reverse
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002) # Added reverse
return UserStatusCreateResponseV2(profileId, req.username).make()
def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]:
req = UserStatusGetDetailRequest(data)
resp = UserStatusGetDetailResponseV1()
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")
user_id = profile["user"]
profile_scores = self.data.score.get_best_scores(user_id)
profile_items = self.data.item.get_items(user_id)
profile_song_unlocks = self.data.item.get_song_unlocks(user_id)
profile_options = self.data.profile.get_options(user_id)
profile_trophies = self.data.item.get_trophies(user_id)
profile_tickets = self.data.item.get_tickets(user_id)
if profile["vip_expire_time"] is None:
resp.userStatus.vipExpireTime = 0
else:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
if profile["always_vip"] or self.game_config.mods.always_vip:
resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp())
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"]
resp.userStatus.loginDays = profile["login_count_days"]
resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"]
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())))
if x > 2:
resp.scores.append(BestScoreDetailV1(unlock["song_id"], x))
empty_scores = len(resp.scores)
for song in profile_scores:
resp.seasonInfo.cumulativeScore += song["score"]
empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores)
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"]
)
if empty_score_idx is not None:
resp.scores[empty_score_idx].clearCounts = clear_cts
resp.scores[empty_score_idx].clearCountsSeason = clear_cts
resp.scores[empty_score_idx].gradeCounts = grade_cts
resp.scores[empty_score_idx].score = song["score"]
resp.scores[empty_score_idx].bestCombo = song["best_combo"]
resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"]
resp.scores[empty_score_idx].rating = song["rating"]
else:
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"]
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:
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()
def handle_user_trial_get_request(self, data: Dict) -> List[Any]:
req = UserTrialGetRequest(data)
resp = UserTrialGetResponse()
user_id = 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}")
for d in self.allowed_stages:
if d[1] > 0 and d[1] < 10:
resp.stageList.append(StageInfo(d[0], d[1]))
stages = self.data.score.get_stageup(user_id, self.version)
if stages is None:
stages = []
add_next = True
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
if add_next or stage_info.danLevel < 9:
resp.stageList.append(stage_info)
if stage_info.danLevel >= 9 and stage_info.clearStatus < 1:
add_next = False
return resp.make()
def handle_user_trial_update_request(self, data: Dict) -> List[Any]:
req = UserTrialUpdateRequest(data)
total_score = 0
for score in req.songScores:
total_score += score
while len(req.songScores) < 3:
req.songScores.append(0)
profile = self.data.profile.get_profile(req.profileId)
user_id = profile["user"]
old_stage = self.data.score.get_stageup_stage(user_id, self.version, req.stageId)
if old_stage is None:
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[2]
best_score3 = req.songScores[3]
else:
best_score1 = old_stage["song1_score"]
best_score2 = old_stage["song2_score"]
best_score3 = old_stage["song3_score"]
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"]):
self.data.profile.update_profile_dan(req.profileId, req.stageLevel, req.clearType.value)
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 = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"])
current_nav = 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"]
self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon)
self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav)
self.data.profile.update_profile_playtype(req.profileId, 4, data["appVersion"][:7])
return BaseResponse.make()
def handle_user_sugoroku_update_request(self, data: Dict) -> List[Any]:
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 = 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()
self.util_put_items(req.profileId, user_id, req.itemsObtainted)
self.data.profile.update_gate(user_id, req.gateId, req.page, req.progress, req.loops, mission_flg, req.totalPts)
return resp.make()
def handle_user_info_getMyroom_request(self, data: Dict) -> List[Any]:
return UserInfogetMyroomResponse().make()
def handle_user_music_unlock_request(self, data: Dict) -> List[Any]:
req = UserMusicUnlockRequest(data)
profile = self.data.profile.get_profile(req.profileId)
if profile is None: return BaseResponse().make()
user_id = profile["user"]
current_wp = profile["wp"]
tickets = self.data.item.get_tickets(user_id)
new_tickets = []
for ticket in tickets:
new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999])
for item in req.itemsUsed:
if item.itemType == WaccaConstants.ITEM_TYPES["wp"]:
if current_wp >= item.quantity:
current_wp -= item.quantity
self.data.profile.spend_wp(req.profileId, item.quantity)
else: return BaseResponse().make()
elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]:
for x in range(len(new_tickets)):
if new_tickets[x][1] == item.itemId:
self.data.item.spend_ticket(new_tickets[x][0])
new_tickets.pop(x)
break
# wp, ticket info
if req.difficulty > WaccaConstants.Difficulty.HARD.value:
old_score = self.data.score.get_best_score(user_id, req.songId, req.difficulty)
if not old_score:
self.data.score.put_best_score(user_id, req.songId, req.difficulty, 0, [0] * 5, [0] * 13, 0, 0)
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:
new_tickets = [
[0, 106002, 0],
[1, 106002, 0],
[2, 106002, 0],
[3, 106002, 0],
[4, 106002, 0],
]
if self.game_config.mods.infinite_wp:
current_wp = 999999
return UserMusicUnlockResponse(current_wp, new_tickets).make()
def handle_user_info_getRanking_request(self, data: Dict) -> List[Any]:
# 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()
def handle_user_music_update_request(self, data: Dict) -> List[Any]:
req = UserMusicUpdateRequest(data)
ver_split = req.appVersion.split(".")
if int(ver_split[0]) >= 3:
resp = UserMusicUpdateResponseV3()
elif int(ver_split[0]) >= 2:
resp = UserMusicUpdateResponseV2()
else:
resp = UserMusicUpdateResponseV1()
resp.songDetail.songId = req.songDetail.songId
resp.songDetail.difficulty = req.songDetail.difficulty
profile = self.data.profile.get_profile(req.profileId)
if profile is None:
self.logger.warn(f"handle_user_music_update_request: No profile for game_id {req.profileId}")
return BaseResponse().make()
user_id = profile["user"]
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
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 = 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
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"])
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)
return resp.make()
#TODO: Coop and vs data
def handle_user_music_updateCoop_request(self, data: Dict) -> List[Any]:
coop_info = data["params"][4]
return self.handle_user_music_update_request(data)
def handle_user_music_updateVersus_request(self, data: Dict) -> List[Any]:
vs_info = data["params"][4]
return self.handle_user_music_update_request(data)
def handle_user_music_updateTrial_request(self, data: Dict) -> List[Any]:
return self.handle_user_music_update_request(data)
def handle_user_mission_update_request(self, data: Dict) -> List[Any]:
req = UserMissionUpdateRequest(data)
page_status = req.params[1][1]
profile = self.data.profile.get_profile(req.profileId)
if profile is None:
return BaseResponse().make()
if len(req.itemsObtained) > 0:
self.util_put_items(req.profileId, profile["user"], req.itemsObtained)
self.data.profile.update_bingo(profile["user"], req.bingoDetail.pageNumber, page_status)
self.data.profile.update_tutorial_flags(req.profileId, req.params[3])
return BaseResponse().make()
def handle_user_goods_purchase_request(self, data: Dict) -> List[Any]:
req = UserGoodsPurchaseRequest(data)
resp = UserGoodsPurchaseResponse()
profile = 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:
resp.currentWp -= req.cost
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}")
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 = 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()
def handle_competition_status_login_request(self, data: Dict) -> List[Any]:
return BaseResponse().make()
def handle_competition_status_update_request(self, data: Dict) -> List[Any]:
return BaseResponse().make()
def handle_user_rating_update_request(self, data: Dict) -> List[Any]:
req = UserRatingUpdateRequest(data)
user_id = 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:
self.data.score.update_song_rating(user_id, song.songId, song.difficulty, song.rating)
self.data.profile.update_user_rating(req.profileId, req.totalRating)
return BaseResponse().make()
def handle_user_status_update_request(self, data: Dict) -> List[Any]:
req = UserStatusUpdateRequestV2(data)
user_id = 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()
self.util_put_items(req.profileId, user_id, req.itemsRecieved)
self.data.profile.update_profile_playtype(req.profileId, req.playType.value, data["appVersion"][:7])
self.data.profile.update_profile_lastplayed(req.profileId, req.lastSongInfo.lastSongId, req.lastSongInfo.lastSongDiff,
req.lastSongInfo.lastFolderOrd, req.lastSongInfo.lastFolderId, req.lastSongInfo.lastSongOrd)
current_icon = self.data.profile.get_options(user_id, WaccaConstants.OPTIONS["set_icon_id"])
current_nav = 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"]
self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["icon"], current_icon)
self.data.item.put_item(user_id, WaccaConstants.ITEM_TYPES["navigator"], current_nav)
return BaseResponse().make()
def handle_user_info_update_request(self, data: Dict) -> List[Any]:
req = UserInfoUpdateRequest(data)
user_id = self.data.profile.profile_to_aime_user(req.profileId)
for opt in req.optsUpdated:
self.data.profile.update_option(user_id, opt.id, opt.val)
for update in req.datesUpdated:
pass
for fav in req.favoritesAdded:
self.data.profile.add_favorite_song(user_id, fav)
for unfav in req.favoritesRemoved:
self.data.profile.remove_favorite_song(user_id, unfav)
return BaseResponse().make()
def handle_user_vip_get_request(self, data: Dict) -> List[Any]:
req = UserVipGetRequest(data)
resp = UserVipGetResponse()
profile = self.data.profile.get_profile(req.profileId)
if profile is None:
self.logger.warn(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()
def handle_user_vip_start_request(self, data: Dict) -> List[Any]:
req = UserVipStartRequest(data)
profile = 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()
profile["vip_expire_time"] = int((self.srvtime + timedelta(days=req.days)).timestamp())
self.data.profile.update_vip_time(req.profileId, self.srvtime + timedelta(days=req.days))
return UserVipStartResponse(profile["vip_expire_time"]).make()
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"]:
self.data.profile.add_xp(profile_id, item.quantity)
elif item.itemType == WaccaConstants.ITEM_TYPES["wp"]:
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 = self.data.score.get_best_score(user_id, item.itemId, item.quantity)
if not old_score:
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
self.data.item.unlock_song(user_id, item.itemId, item.quantity)
elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"]:
self.data.item.add_ticket(user_id, item.itemId)
elif item.itemType == WaccaConstants.ITEM_TYPES["trophy"]:
self.data.item.update_trophy(user_id, item.itemId, self.season, item.quantity, 0)
else:
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)

44
titles/wacca/config.py Normal file
View File

@ -0,0 +1,44 @@
from typing import Dict, List
from core.config import CoreConfig
class WaccaServerConfig():
def __init__(self, parent_config: "WaccaConfig") -> None:
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'enable', default=True)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'loglevel', default="info"))
class WaccaModsConfig():
def __init__(self, parent_config: "WaccaConfig") -> None:
self.__config = parent_config
@property
def always_vip(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'always_vip', default=True)
@property
def infinite_tickets(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_tickets', default=True)
@property
def infinite_wp(self) -> bool:
return CoreConfig.get_config_field(self.__config, 'wacca', 'mods', 'infinite_wp', default=True)
class WaccaGateConfig():
def __init__(self, parent_config: "WaccaConfig") -> None:
self.__config = parent_config
@property
def enabled_gates(self) -> List[int]:
return CoreConfig.get_config_field(self.__config, 'wacca', 'gates', 'enabled_gates', default=[])
class WaccaConfig(dict):
def __init__(self) -> None:
self.server = WaccaServerConfig(self)
self.mods = WaccaModsConfig(self)
self.gates = WaccaGateConfig(self)

113
titles/wacca/const.py Normal file
View File

@ -0,0 +1,113 @@
from enum import Enum
class WaccaConstants():
CONFIG_NAME = "wacca.yaml"
GAME_CODE = "SDFE"
VER_WACCA = 0
VER_WACCA_S = 1
VER_WACCA_LILY = 2
VER_WACCA_LILY_R = 3
VER_WACCA_REVERSE = 4
VERSION_NAMES = ("WACCA", "WACCA S", "WACCA Lily", "WACCA Lily R", "WACCA Reverse")
class GRADES(Enum):
D = 1
C = 2
B = 3
A = 4
AA = 5
AAA = 6
S = 7
SS = 8
SSS = 9
MASTER = 10
S_PLUS = 11
SS_PLUS = 12
SSS_PLUS = 13
ITEM_TYPES = {
"xp": 1,
"wp": 2,
"music_unlock": 3,
"music_difficulty_unlock": 4,
"title": 5,
"icon": 6,
"trophy": 7,
"skill": 8,
"ticket": 9,
"note_color": 10,
"note_sound": 11,
"baht_do_not_send": 12,
"boost_badge": 13,
"gate_point": 14,
"navigator": 15,
"user_plate": 16,
"touch_effect": 17,
}
OPTIONS = {
"note_speed": 1, # 1.0 - 6.0
"field_mask": 2, # 0-4
"note_sound": 3, # ID
"note_color": 4, # ID
"bgm_volume": 5, # 0-100 incremements of 10
"bg_video": 7, # ask, on, or off
"mirror": 101, # none or left+right swap
"judge_display_pos": 102, # center, under, over, top or off
"judge_detail_display": 103, # on or off
"measure_guidelines": 105, # on or off
"guideline_mask": 106, # 0 - 5
"judge_line_timing_adjust": 108, # -10 - 10
"note_design": 110, # 1 - 5
"bonus_effect": 114, # on or off
"chara_voice": 115, # "usually" or none
"score_display_method": 116, # add or subtract
"give_up": 117, # off, no touch, can't achieve s, ss, sss, pb
"guideline_spacing": 118, # none, or a-g type
"center_display": 119, # none, combo, score add, score sub, s ss sss pb boarder
"ranking_display": 120, # on or off
"stage_up_icon_display": 121, # on or off
"rating_display": 122, # on or off
"player_level_display": 123, # on or off
"touch_effect": 124, # on or off
"guide_sound_vol": 125, # 0-100 incremements of 10
"touch_note_vol": 126, # 0-100 incremements of 10
"hold_note_vol": 127, # 0-100 incremements of 10
"slide_note_vol": 128, # 0-100 incremements of 10
"snap_note_vol": 129, # 0-100 incremements of 10
"chain_note_vol": 130, # 0-100 incremements of 10
"bonus_note_vol": 131, # 0-100 incremements of 10
"gate_skip": 132, # on or off
"key_beam_display": 133, # on or off
"left_slide_note_color": 201, # red blue green or orange
"right_slide_note_color": 202, # red blue green or orange
"forward_slide_note_color": 203, # red blue green or orange
"back_slide_note_color": 204, # red blue green or orange
"master_vol": 1001, # 0-100 incremements of 10
"set_title_id": 1002, # ID
"set_icon_id": 1003, # ID
"set_nav_id": 1004, # ID
"set_plate_id": 1005, # ID
}
DIFFICULTIES = {
"Normal": 1,
"Hard": 2,
"Expert": 3,
"Inferno": 4,
}
class Difficulty(Enum):
NORMAL = 1
HARD = 2
EXPERT = 3
INFERNO = 4
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]

12
titles/wacca/database.py Normal file
View File

@ -0,0 +1,12 @@
from core.data import Data
from core.config import CoreConfig
from titles.wacca.schema import *
class WaccaData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)
self.profile = WaccaProfileData(self.config, self.session)
self.score = WaccaScoreData(self.config, self.session)
self.item = WaccaItemData(self.config, self.session)
self.static = WaccaStaticData(self.config, self.session)

View File

@ -0,0 +1,9 @@
from titles.wacca.handlers.base import *
from titles.wacca.handlers.advertise import *
from titles.wacca.handlers.housing import *
from titles.wacca.handlers.user_info import *
from titles.wacca.handlers.user_misc import *
from titles.wacca.handlers.user_music import *
from titles.wacca.handlers.user_status import *
from titles.wacca.handlers.user_trial import *
from titles.wacca.handlers.user_vip import *

View File

@ -0,0 +1,45 @@
from typing import List, Dict
from titles.wacca.handlers.base import BaseResponse
from titles.wacca.handlers.helpers import Notice
# ---advertise/GetNews---
class GetNewsResponseV1(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.notices: list[Notice] = []
self.copywrightListings: list[str] = []
self.stoppedSongs: list[int] = []
self.stoppedJackets: list[int] = []
self.stoppedMovies: list[int] = []
self.stoppedIcons: list[int] = []
def make(self) -> Dict:
note = []
for notice in self.notices:
note.append(notice.make())
self.params = [
note,
self.copywrightListings,
self.stoppedSongs,
self.stoppedJackets,
self.stoppedMovies,
self.stoppedIcons
]
return super().make()
class GetNewsResponseV2(GetNewsResponseV1):
stoppedProducts: list[int] = []
stoppedNavs: list[int] = []
stoppedNavVoices: list[int] = []
def make(self) -> Dict:
super().make()
self.params.append(self.stoppedProducts)
self.params.append(self.stoppedNavs)
self.params.append(self.stoppedNavVoices)
return super(GetNewsResponseV1, self).make()

View File

@ -0,0 +1,31 @@
from typing import Dict, List
from datetime import datetime
class BaseRequest():
def __init__(self, data: Dict) -> None:
self.requestNo: int = data["requestNo"]
self.appVersion: str = data["appVersion"]
self.boardId: str = data["boardId"]
self.chipId: str = data["chipId"]
self.params: List = data["params"]
class BaseResponse():
def __init__(self) -> None:
self.status: int = 0
self.message: str = ""
self.serverTime: int = int(datetime.now().timestamp())
self.maintNoticeTime: int = 0
self.maintNotPlayableTime: int = 0
self.maintStartTime: int = 0
self.params: List = []
def make(self) -> Dict:
return {
"status": self.status,
"message": self.message,
"serverTime": self.serverTime,
"maintNoticeTime": self.maintNoticeTime,
"maintNotPlayableTime": self.maintNotPlayableTime,
"maintStartTime": self.maintStartTime,
"params": self.params
}

View File

@ -0,0 +1,786 @@
from typing import List, Dict, Any
from enum import Enum
from titles.wacca.const import WaccaConstants
class HousingInfo():
"""
1 is lan install role, 2 is country
"""
id: int = 0
val: str = ""
def __init__(self, id: int = 0, val: str = "") -> None:
self.id = id
self.val = val
def make(self) -> List:
return [ self.id, self.val ]
class Notice():
name: str = ""
title: str = ""
message: str = ""
unknown3: str = ""
unknown4: str = ""
showTitleScreen: bool = True
showWelcomeScreen: bool = True
startTime: int = 0
endTime: int = 0
voiceline: int = 0
def __init__(self, name: str = "", title: str = "", message: str = "", start: int = 0, end: int = 0) -> None:
self.name = name
self.title = title
self.message = message
self.startTime = start
self.endTime = end
def make(self) -> List:
return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen),
int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline]
class UserOption():
opt_id: int
opt_val: Any
def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None:
self.opt_id = opt_id
self.opt_val = opt_val
def make(self) -> List:
return [self.opt_id, self.opt_val]
class UserStatusV1():
def __init__(self) -> None:
self.userId: int = -1
self.username: str = ""
self.userType: int = 1
self.xp: int = 0
self.danLevel: int = 0
self.danType: int = 0
self.wp: int = 0
self.titlePartIds: List[int] = [0, 0, 0]
self.useCount: int = 0
self.loginDays: int = 0
self.loginConsecutive: int = 0
self.loginConsecutiveDays: int = 0
self.vipExpireTime: int = 0
def make(self) -> List:
return [
self.userId,
self.username,
self.userType,
self.xp,
self.danLevel,
self.danType,
self.wp,
self.titlePartIds,
self.useCount,
self.loginDays,
self.loginConsecutive,
self.loginConsecutiveDays,
self.vipExpireTime
]
class UserStatusV2(UserStatusV1):
def __init__(self) -> None:
super().__init__()
self.loginsToday: int = 0
self.rating: int = 0
def make(self) -> List:
ret = super().make()
ret.append(self.loginsToday)
ret.append(self.rating)
return ret
class ProfileStatus(Enum):
ProfileGood = 0
ProfileRegister = 1
ProfileInUse = 2
ProfileWrongRegion = 3
class PlayVersionStatus(Enum):
VersionGood = 0
VersionTooNew = 1
VersionUpgrade = 2
class PlayModeCounts():
seasonId: int = 0
modeId: int = 0
playNum: int = 0
def __init__(self, seasonId: int, modeId: int, playNum: int) -> None:
self.seasonId = seasonId
self.modeId = modeId
self.playNum = playNum
def make(self) -> List:
return [
self.seasonId,
self.modeId,
self.playNum
]
class SongUnlock():
songId: int = 0
difficulty: int = 0
whenAppeared: int = 0
whenUnlocked: int = 0
def __init__(self, song_id: int = 0, difficulty: int = 1, whenAppered: int = 0, whenUnlocked: int = 0) -> None:
self.songId = song_id
self.difficulty = difficulty
self.whenAppeared = whenAppered
self.whenUnlocked = whenUnlocked
def make(self) -> List:
return [
self.songId,
self.difficulty,
self.whenAppeared,
self.whenUnlocked
]
class GenericItemRecv():
def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None:
self.itemId = item_id
self.itemType = item_type
self.quantity = quantity
def make(self) -> List:
return [ self.itemType, self.itemId, self.quantity ]
class GenericItemSend():
def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None:
self.itemId = itemId
self.itemType = itemType
self.whenAcquired = whenAcquired
def make(self) -> List:
return [
self.itemId,
self.itemType,
self.whenAcquired
]
class IconItem(GenericItemSend):
uses: int = 0
def __init__(self, itemId: int, itemType: int, uses: int, whenAcquired: int) -> None:
super().__init__(itemId, itemType, whenAcquired)
self.uses = uses
def make(self) -> List:
return [
self.itemId,
self.itemType,
self.uses,
self.whenAcquired
]
class TrophyItem():
trophyId: int = 0
season: int = 1
progress: int = 0
badgeType: int = 0
def __init__(self, trophyId: int, season: int, progress: int, badgeType: int) -> None:
self.trophyId = trophyId
self.season = season
self.progress = progress
self.badgeType = badgeType
def make(self) -> List:
return [
self.trophyId,
self.season,
self.progress,
self.badgeType
]
class TicketItem():
userTicketId: int = 0
ticketId: int = 0
whenExpires: int = 0
def __init__(self, userTicketId: int, ticketId: int, whenExpires: int) -> None:
self.userTicketId = userTicketId
self.ticketId = ticketId
self.whenExpires = whenExpires
def make(self) -> List:
return [
self.userTicketId,
self.ticketId,
self.whenExpires
]
class NavigatorItem(IconItem):
usesToday: int = 0
def __init__(self, itemId: int, itemType: int, whenAcquired: int, uses: int, usesToday: int) -> None:
super().__init__(itemId, itemType, uses, whenAcquired)
self.usesToday = usesToday
def make(self) -> List:
return [
self.itemId,
self.itemType,
self.whenAcquired,
self.uses,
self.usesToday
]
class SkillItem():
skill_type: int
level: int
flag: int
badge: int
def make(self) -> List:
return [
self.skill_type,
self.level,
self.flag,
self.badge
]
class UserItemInfoV1():
def __init__(self) -> None:
self.songUnlocks: List[SongUnlock] = []
self.titles: List[GenericItemSend] = []
self.icons: List[IconItem] = []
self.trophies: List[TrophyItem] = []
self.skills: List[SkillItem] = []
self.tickets: List[TicketItem] = []
self.noteColors: List[GenericItemSend] = []
self.noteSounds: List[GenericItemSend] = []
def make(self) -> List:
unlocks = []
titles = []
icons = []
trophies = []
skills = []
tickets = []
colors = []
sounds = []
for x in self.songUnlocks:
unlocks.append(x.make())
for x in self.titles:
titles.append(x.make())
for x in self.icons:
icons.append(x.make())
for x in self.trophies:
trophies.append(x.make())
for x in self.skills:
skills.append(x.make())
for x in self.tickets:
tickets.append(x.make())
for x in self.noteColors:
colors.append(x.make())
for x in self.noteSounds:
sounds.append(x.make())
return [
unlocks,
titles,
icons,
trophies,
skills,
tickets,
colors,
sounds,
]
class UserItemInfoV2(UserItemInfoV1):
def __init__(self) -> None:
super().__init__()
self.navigators: List[NavigatorItem] = []
self.plates: List[GenericItemSend] = []
def make(self) -> List:
ret = super().make()
plates = []
navs = []
for x in self.navigators:
navs.append(x.make())
for x in self.plates:
plates.append(x.make())
ret.append(navs)
ret.append(plates)
return ret
class UserItemInfoV3(UserItemInfoV2):
def __init__(self) -> None:
super().__init__()
self.touchEffect: List[GenericItemSend] = []
def make(self) -> List:
ret = super().make()
effect = []
for x in self.touchEffect:
effect.append(x.make())
ret.append(effect)
return ret
class SongDetailClearCounts():
def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0,
am_ct: int = 0, counts: List[int] = None) -> None:
if counts is None:
self.playCt = play_ct
self.clearCt = clear_ct
self.misslessCt = ml_ct
self.fullComboCt = fc_ct
self.allMarvelousCt = am_ct
else:
self.playCt = counts[0]
self.clearCt = counts[1]
self.misslessCt = counts[2]
self.fullComboCt = counts[3]
self.allMarvelousCt = counts[4]
def make(self) -> List:
return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt]
class SongDetailGradeCountsV1():
dCt: int
cCt: int
bCt: int
aCt: int
aaCt: int
aaaCt: int
sCt: int
ssCt: int
sssCt: int
masterCt: int
def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0,
ss: int = 0, sss: int = 0, master: int = 0, counts: List[int] = None) -> None:
if counts is None:
self.dCt = d
self.cCt = c
self.bCt = b
self.aCt = a
self.aaCt = aa
self.aaaCt = aaa
self.sCt = s
self.ssCt = ss
self.sssCt = sss
self.masterCt = master
else:
self.dCt = counts[0]
self.cCt = counts[1]
self.bCt = counts[2]
self.aCt = counts[3]
self.aaCt = counts[4]
self.aaaCt = counts[5]
self.sCt = counts[6]
self.ssCt = counts[7]
self.sssCt = counts[8]
self.masterCt =counts[9]
def make(self) -> List:
return [self.dCt, self.cCt, self.bCt, self.aCt, self.aaCt, self.aaaCt, self.sCt, self.ssCt, self.sssCt, self.masterCt]
class SongDetailGradeCountsV2(SongDetailGradeCountsV1):
spCt: int
sspCt: int
ssspCt: int
def __init__(self, d: int = 0, c: int = 0, b: int = 0, a: int = 0, aa: int = 0, aaa: int = 0, s: int = 0,
ss: int = 0, sss: int = 0, master: int = 0, sp: int = 0, ssp: int = 0, sssp: int = 0, counts: List[int] = None, ) -> None:
super().__init__(d, c, b, a, aa, aaa, s, ss, sss, master, counts)
if counts is None:
self.spCt = sp
self.sspCt = ssp
self.ssspCt = sssp
else:
self.spCt = counts[10]
self.sspCt = counts[11]
self.ssspCt = counts[12]
def make(self) -> List:
return super().make() + [self.spCt, self.sspCt, self.ssspCt]
class BestScoreDetailV1():
songId: int = 0
difficulty: int = 1
clearCounts: SongDetailClearCounts = SongDetailClearCounts()
clearCountsSeason: SongDetailClearCounts = SongDetailClearCounts()
gradeCounts: SongDetailGradeCountsV1 = SongDetailGradeCountsV1()
score: int = 0
bestCombo: int = 0
lowestMissCtMaybe: int = 0
isUnlock: int = 1
rating: int = 0
def __init__(self, song_id: int, difficulty: int = 1) -> None:
self.songId = song_id
self.difficulty = difficulty
def make(self) -> List:
return [
self.songId,
self.difficulty,
self.clearCounts.make(),
self.clearCountsSeason.make(),
self.gradeCounts.make(),
self.score,
self.bestCombo,
self.lowestMissCtMaybe,
self.isUnlock,
self.rating
]
class BestScoreDetailV2(BestScoreDetailV1):
gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2()
class SongUpdateJudgementCounts():
marvCt: int
greatCt: int
goodCt: int
missCt: int
def __init__(self, marvs: int = 0, greats: int = 0, goods: int = 0, misses: int = 0) -> None:
self.marvCt = marvs
self.greatCt = greats
self.goodCt = goods
self.missCt = misses
def make(self) -> List:
return [self.marvCt, self.greatCt, self.goodCt, self.missCt]
class SongUpdateDetail():
songId: int
difficulty: int
level: float
score: int
judgements: SongUpdateJudgementCounts
maxCombo: int
grade: WaccaConstants.GRADES
flagCleared: bool
flagMissless: bool
flagFullcombo: bool
flagAllMarvelous: bool
flagGiveUp: bool
skillPt: int
fastCt: int
slowCt: int
flagNewRecord: bool
def __init__(self, data: List = None) -> None:
if data is not None:
self.songId = data[0]
self.difficulty = data[1]
self.level = data[2]
self.score = data[3]
self.judgements = SongUpdateJudgementCounts(data[4][0], data[4][1], data[4][2], data[4][3])
self.maxCombo = data[5]
self.grade = WaccaConstants.GRADES(data[6]) # .value to get number, .name to get letter
self.flagCleared = False if data[7] == 0 else True
self.flagMissless = False if data[8] == 0 else True
self.flagFullcombo = False if data[9] == 0 else True
self.flagAllMarvelous = False if data[10] == 0 else True
self.flagGiveUp = False if data[11] == 0 else True
self.skillPt = data[12]
self.fastCt = data[13]
self.slowCt = data[14]
self.flagNewRecord = False if data[15] == 0 else True
class SeasonalInfoV1():
def __init__(self) -> None:
self.level: int = 0
self.wpObtained: int = 0
self.wpSpent: int = 0
self.cumulativeScore: int = 0
self.titlesObtained: int = 0
self.iconsObtained: int = 0
self.skillPts: int = 0
self.noteColorsObtained: int = 0
self.noteSoundsObtained: int = 0
def make(self) -> List:
return [
self.level,
self.wpObtained,
self.wpSpent,
self.cumulativeScore,
self.titlesObtained,
self.iconsObtained,
self.skillPts,
self.noteColorsObtained,
self.noteSoundsObtained
]
class SeasonalInfoV2(SeasonalInfoV1):
def __init__(self) -> None:
super().__init__()
self.platesObtained: int = 0
self.cumulativeGatePts: int = 0
def make(self) -> List:
return super().make() + [self.platesObtained, self.cumulativeGatePts]
class BingoPageStatus():
id = 0
location = 1
progress = 0
def __init__(self, id: int = 0, location: int = 1, progress: int = 0) -> None:
self.id = id
self.location = location
self.progress = progress
def make(self) -> List:
return [self.id, self.location, self.progress]
class BingoDetail():
def __init__(self, pageNumber: int) -> None:
self.pageNumber = pageNumber
self.pageStatus: List[BingoPageStatus] = []
def make(self) -> List:
status = []
for x in self.pageStatus:
status.append(x.make())
return [
self.pageNumber,
status
]
class GateDetailV1():
def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None:
self.id = gate_id
self.page = page
self.progress = progress
self.loops = loops
self.lastUsed = last_used
self.missionFlg = mission_flg
def make(self) -> List:
return [self.id, 1, self.page, self.progress, self.loops, self.lastUsed]
class GateDetailV2(GateDetailV1):
def make(self) -> List:
return super().make() + [self.missionFlg]
class GachaInfo():
def make() -> List:
return []
class LastSongDetail():
lastSongId = 90
lastSongDiff = 1
lastFolderOrd = 1
lastFolderId = 1
lastSongOrd = 1
def __init__(self, last_song: int = 90, last_diff: int = 1, last_folder_ord: int = 1,
last_folder_id: int = 1, last_song_ord: int = 1) -> None:
self.lastSongId = last_song
self.lastSongDiff = last_diff
self.lastFolderOrd = last_folder_ord
self.lastFolderId = last_folder_id
self.lastSongOrd = last_song_ord
def make(self) -> List:
return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId,
self.lastSongOrd]
class FriendDetail():
def make(self) -> List:
return []
class UserOption():
id = 1
val = 1
def __init__(self, id: int = 1, val: int = val) -> None:
self.id = id
self.val = val
def make(self) -> List:
return [self.id, self.val]
class LoginBonusInfo():
def __init__(self) -> None:
self.tickets: List[TicketItem] = []
self.items: List[GenericItemRecv] = []
self.message: str = ""
def make(self) -> List:
tks = []
itms = []
for ticket in self.tickets:
tks.append(ticket.make())
for item in self.items:
itms.append(item.make())
return [ tks, itms, self.message ]
class VipLoginBonus():
id = 1
unknown = 0
item: GenericItemRecv
def __init__(self, id: int = 1, unk: int = 0, item_type: int = 1, item_id: int = 1, item_qt: int = 1) -> None:
self.id = id
self.unknown = unk
self.item = GenericItemRecv(item_type, item_id, item_qt)
def make(self) -> List:
return [ self.id, self.unknown, self.item.make() ]
class VipInfo():
def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None:
self.pageYear = year
self.pageMonth = month
self.pageDay = day
self.numItem = num_item
self.presentInfo: List[LoginBonusInfo] = []
self.vipLoginBonus: List[VipLoginBonus] = []
def make(self) -> List:
pres = []
vipBonus = []
for present in self.presentInfo:
pres.append(present.make())
for b in self.vipLoginBonus:
vipBonus.append(b.make())
return [ self.pageYear, self.pageMonth, self.pageDay, self.numItem, pres, vipBonus ]
class PurchaseType(Enum):
PurchaseTypeCredit = 1
PurchaseTypeWP = 2
class PlayType(Enum):
PlayTypeSingle = 1
PlayTypeVs = 2
PlayTypeCoop = 3
PlayTypeStageup = 4
class SongRatingUpdate():
song_id = 0
difficulty = 0
rating = 0
def __init__(self, song: int = 0, difficulty: int = 0, rating: int = 0) -> None:
self.song_id = song
self.difficulty = difficulty
self.rating = rating
def make(self) -> List:
return [self.song_id, self.difficulty, self.rating]
class StageInfo():
danId: int = 0
danLevel: int = 0
clearStatus: int = 0
numSongCleared: int = 0
song1BestScore: int = 0
song2BestScore: int = 0
song3BestScore: int = 0
unk5: int = 1
def __init__(self, dan_id: int = 0, dan_level: int = 0) -> None:
self.danId = dan_id
self.danLevel = dan_level
def make(self) -> List:
return [
self.danId,
self.danLevel,
self.clearStatus,
self.numSongCleared,
[
self.song1BestScore,
self.song2BestScore,
self.song3BestScore,
],
self.unk5
]
class StageupClearType(Enum):
FAIL = 0
CLEAR_BLUE = 1
CLEAR_SILVER = 2
CLEAR_GOLD = 3
class MusicUpdateDetailV1():
def __init__(self) -> None:
self.songId = 0
self.difficulty = 1
self.clearCounts: SongDetailClearCounts = SongDetailClearCounts()
self.clearCountsSeason: SongDetailClearCounts = SongDetailClearCounts()
self.grades: SongDetailGradeCountsV1 = SongDetailGradeCountsV1()
self.score = 0
self.lowestMissCount = 0
self.maxSkillPts = 0
self.locked = 0
self.rating = 0
def make(self) -> List:
return [
self.songId,
self.difficulty,
self.clearCounts.make(),
self.clearCountsSeason.make(),
self.grades.make(),
self.score,
self.lowestMissCount,
self.maxSkillPts,
self.locked,
self.rating
]
class MusicUpdateDetailV2(MusicUpdateDetailV1):
def __init__(self) -> None:
super().__init__()
self.grades = SongDetailGradeCountsV2()
class SongRatingUpdate():
def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None:
self.songId = song_id
self.difficulty = difficulty
self.rating = new_rating
def make(self) -> List:
return [
self.songId,
self.difficulty,
self.rating,
]
class GateTutorialFlag():
def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None:
self.tutorialId = tutorial_id
self.flagWatched = flg_watched
def make(self) -> List:
return [
self.tutorialId,
int(self.flagWatched)
]

View File

@ -0,0 +1,38 @@
from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import HousingInfo
# ---housing/get----
class HousingGetResponse(BaseResponse):
def __init__(self, housingId: int) -> None:
super().__init__()
self.housingId: int = housingId
self.regionId: int = 0
def make(self) -> Dict:
self.params = [self.housingId, self.regionId]
return super().make()
# ---housing/start----
class HousingStartRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.unknown0: str = self.params[0]
self.errorLog: str = self.params[1]
self.unknown2: str = self.params[2]
self.info: List[HousingInfo] = []
for info in self.params[3]:
self.info.append(HousingInfo(info[0], info[1]))
class HousingStartResponseV1(BaseResponse):
def __init__(self, regionId: int, songList: List[int]) -> None:
super().__init__()
self.regionId = regionId
self.songList = songList
def make(self) -> Dict:
self.params = [self.regionId, self.songList]
return super().make()

View File

@ -0,0 +1,61 @@
from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import UserOption
# ---user/info/update---
class UserInfoUpdateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = int(self.params[0])
self.optsUpdated: List[UserOption] = []
self.datesUpdated: List = self.params[3]
self.favoritesAdded: List[int] = self.params[4]
self.favoritesRemoved: List[int] = self.params[5]
for x in self.params[2]:
self.optsUpdated.append(UserOption(x[0], x[1]))
# ---user/info/getMyroom--- TODO: Understand this better
class UserInfogetMyroomRequest(BaseRequest):
game_id = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.game_id = int(self.params[0])
class UserInfogetMyroomResponse(BaseResponse):
def make(self) -> Dict:
self.params = [
0,0,0,0,0,[],0,0,0
]
return super().make()
# ---user/info/getRanking---
class UserInfogetRankingRequest(BaseRequest):
game_id = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.game_id = int(self.params[0])
class UserInfogetRankingResponse(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.total_score_rank = 0
self.high_score_by_song_rank = 0
self.cumulative_score_rank = 0
self.state_up_score_rank = 0
self.other_score_ranking = 0
self.wacca_points_ranking = 0
def make(self) -> Dict:
self.params = [
self.total_score_rank,
self.high_score_by_song_rank,
self.cumulative_score_rank,
self.state_up_score_rank,
self.other_score_ranking,
self.wacca_points_ranking,
]
return super().make()

View File

@ -0,0 +1,85 @@
from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import PurchaseType, GenericItemRecv
from titles.wacca.handlers.helpers import TicketItem, SongRatingUpdate, BingoDetail
from titles.wacca.handlers.helpers import BingoPageStatus, GateTutorialFlag
# ---user/goods/purchase---
class UserGoodsPurchaseRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = int(self.params[0])
self.purchaseId = int(self.params[1])
self.purchaseCount = int(self.params[2])
self.purchaseType = PurchaseType(self.params[3])
self.cost = int(self.params[4])
self.itemObtained: GenericItemRecv = GenericItemRecv(self.params[5][0], self.params[5][1], self.params[5][2])
class UserGoodsPurchaseResponse(BaseResponse):
def __init__(self, wp: int = 0, tickets: List = []) -> None:
super().__init__()
self.currentWp = wp
self.tickets: List[TicketItem] = []
for ticket in tickets:
self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2]))
def make(self) -> List:
tix = []
for ticket in self.tickets:
tix.append(ticket.make())
self.params = [self.currentWp, tix]
return super().make()
# ---user/sugaroku/update---
class UserSugarokuUpdateRequestV1(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = int(self.params[0])
self.gateId = int(self.params[1])
self.page = int(self.params[2])
self.progress = int(self.params[3])
self.loops = int(self.params[4])
self.boostsUsed = self.params[5]
self.totalPts = int(self.params[7])
self.itemsObtainted: List[GenericItemRecv] = []
for item in self.params[6]:
self.itemsObtainted.append(GenericItemRecv(item[0], item[1], item[2]))
class UserSugarokuUpdateRequestV2(UserSugarokuUpdateRequestV1):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.mission_flag = int(self.params[8])
# ---user/rating/update---
class UserRatingUpdateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
self.totalRating = self.params[1]
self.songs: List[SongRatingUpdate] = []
for x in self.params[2]:
self.songs.append(SongRatingUpdate(x[0], x[1], x[2]))
# ---user/mission/update---
class UserMissionUpdateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
self.bingoDetail = BingoDetail(self.params[1][0])
self.itemsObtained: List[GenericItemRecv] = []
self.gateTutorialFlags: List[GateTutorialFlag] = []
for x in self.params[1][1]:
self.bingoDetail.pageStatus.append(BingoPageStatus(x[0], x[1], x[2]))
for x in self.params[2]:
self.itemsObtained.append(GenericItemRecv(x[0], x[1], x[2]))
for x in self.params[3]:
self.gateTutorialFlags.append(GateTutorialFlag(x[0], x[1]))

View File

@ -0,0 +1,92 @@
from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import GenericItemRecv, SongUpdateDetail, TicketItem
from titles.wacca.handlers.helpers import MusicUpdateDetailV1, MusicUpdateDetailV2
from titles.wacca.handlers.helpers import SeasonalInfoV2, SeasonalInfoV1
# ---user/music/update---
class UserMusicUpdateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId: int = self.params[0]
self.songNumber: int = self.params[1]
self.songDetail = SongUpdateDetail(self.params[2])
self.itemsObtained: List[GenericItemRecv] = []
for itm in data["params"][3]:
self.itemsObtained.append(GenericItemRecv(itm[0], itm[1], itm[2]))
class UserMusicUpdateResponseV1(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.songDetail = MusicUpdateDetailV1()
self.seasonInfo = SeasonalInfoV1()
self.rankingInfo: List[List[int]] = []
def make(self) -> Dict:
self.params = [
self.songDetail.make(),
[self.songDetail.songId, self.songDetail.clearCounts.playCt],
self.seasonInfo.make(),
self.rankingInfo
]
return super().make()
class UserMusicUpdateResponseV2(UserMusicUpdateResponseV1):
def __init__(self) -> None:
super().__init__()
self.seasonInfo = SeasonalInfoV2()
class UserMusicUpdateResponseV3(UserMusicUpdateResponseV2):
def __init__(self) -> None:
super().__init__()
self.songDetail = MusicUpdateDetailV2()
# ---user/music/updateCoop---
class UserMusicUpdateCoopRequest(UserMusicUpdateRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.coopData = self.params[4]
# ---user/music/updateVs---
class UserMusicUpdateVsRequest(UserMusicUpdateRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.vsData = self.params[4]
# ---user/music/unlock---
class UserMusicUnlockRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
self.songId = self.params[1]
self.difficulty = self.params[2]
self.itemsUsed: List[GenericItemRecv] = []
for itm in self.params[3]:
self.itemsUsed.append(GenericItemRecv(itm[0], itm[1], itm[2]))
class UserMusicUnlockResponse(BaseResponse):
def __init__(self, current_wp: int = 0, tickets_remaining: List = []) -> None:
super().__init__()
self.wp = current_wp
self.tickets: List[TicketItem] = []
for ticket in tickets_remaining:
self.tickets.append(TicketItem(ticket[0], ticket[1], ticket[2]))
def make(self) -> List:
tickets = []
for ticket in self.tickets:
tickets.append(ticket.make())
self.params = [
self.wp,
tickets
]
return super().make()

View File

@ -0,0 +1,289 @@
from typing import List, Dict, Optional
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import *
# ---user/status/get----
class UserStatusGetRequest(BaseRequest):
aimeId: int = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.aimeId = int(data["params"][0])
class UserStatusGetV1Response(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.userStatus: UserStatusV1 = UserStatusV1()
self.setTitleId: int = 0
self.setIconId: int = 0
self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood
self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood
self.lastGameVersion: str = ""
def make(self) -> Dict:
self.params = [
self.userStatus.make(),
self.setTitleId,
self.setIconId,
self.profileStatus.value,
[
self.versionStatus.value,
self.lastGameVersion
]
]
return super().make()
class UserStatusGetV2Response(UserStatusGetV1Response):
def __init__(self) -> None:
super().__init__()
self.userStatus: UserStatusV2 = UserStatusV2()
self.unknownArr: List = []
def make(self) -> Dict:
super().make()
self.params.append(self.unknownArr)
return super(UserStatusGetV1Response, self).make()
# ---user/status/getDetail----
class UserStatusGetDetailRequest(BaseRequest):
userId: int = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.userId = data["params"][0]
class UserStatusGetDetailResponseV1(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.userStatus: UserStatusV1 = UserStatusV1()
self.options: List[UserOption] = []
self.seasonalPlayModeCounts: List[PlayModeCounts] = []
self.userItems: UserItemInfoV1 = UserItemInfoV1()
self.scores: List[BestScoreDetailV1] = []
self.songPlayStatus: List[int] = [0,0]
self.seasonInfo: SeasonalInfoV1 = []
self.playAreaList: List = [ [0],[0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0],[0,0,0,0],[0,0,0,0,0,0,0],[0] ]
self.songUpdateTime: int = 0
def make(self) -> List:
opts = []
play_modes = []
scores = []
for x in self.seasonalPlayModeCounts:
play_modes.append(x.make())
for x in self.scores:
scores.append(x.make())
for x in self.options:
opts.append(x.make())
self.params = [
self.userStatus.make(),
opts,
play_modes,
self.userItems.make(),
scores,
self.songPlayStatus,
self.seasonInfo.make(),
self.playAreaList,
self.songUpdateTime
]
return super().make()
def find_score_idx(self, song_id: int, difficulty: int = 1, start_idx: int = 0, stop_idx: int = None) -> Optional[int]:
if stop_idx is None or stop_idx > len(self.scores):
stop_idx = len(self.scores)
for x in range(start_idx, stop_idx):
if self.scores[x].songId == song_id and self.scores[x].difficulty == difficulty:
return x
return None
class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1):
def __init__(self) -> None:
super().__init__()
self.userStatus: UserStatusV2 = UserStatusV2()
self.seasonInfo: SeasonalInfoV2 = SeasonalInfoV2()
self.userItems: UserItemInfoV2 = UserItemInfoV2()
self.favorites: List[int] = []
self.stoppedSongIds: List[int] = []
self.eventInfo: List[int] = []
self.gateInfo: List[GateDetailV1] = []
self.lastSongInfo: LastSongDetail = LastSongDetail()
self.gateTutorialFlags: List[GateTutorialFlag] = []
self.gatchaInfo: List[GachaInfo] = []
self.friendList: List[FriendDetail] = []
def make(self) -> List:
super().make()
gates = []
friends = []
tut_flg = []
for x in self.gateInfo:
gates.append(x.make())
for x in self.friendList:
friends.append(x.make())
for x in self.gateTutorialFlags:
tut_flg.append(x.make())
while len(tut_flg) < 5:
flag_id = len(tut_flg) + 1
tut_flg.append([flag_id, 0])
self.params.append(self.favorites)
self.params.append(self.stoppedSongIds)
self.params.append(self.eventInfo)
self.params.append(gates)
self.params.append(self.lastSongInfo.make())
self.params.append(tut_flg)
self.params.append(self.gatchaInfo)
self.params.append(friends)
return super(UserStatusGetDetailResponseV1, self).make()
class UserStatusGetDetailResponseV3(UserStatusGetDetailResponseV2):
def __init__(self) -> None:
super().__init__()
self.gateInfo: List[GateDetailV2] = []
class UserStatusGetDetailResponseV4(UserStatusGetDetailResponseV3):
def __init__(self) -> None:
super().__init__()
self.userItems: UserItemInfoV3 = UserItemInfoV3()
self.bingoStatus: BingoDetail = BingoDetail(0)
self.scores: List[BestScoreDetailV2] = []
def make(self) -> List:
super().make()
self.params.append(self.bingoStatus.make())
return super(UserStatusGetDetailResponseV1, self).make()
# ---user/status/login----
class UserStatusLoginRequest(BaseRequest):
userId: int = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.userId = data["params"][0]
class UserStatusLoginResponseV1(BaseResponse):
def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None:
super().__init__()
self.dailyBonus: List[LoginBonusInfo] = []
self.consecBonus: List[LoginBonusInfo] = []
self.otherBonus: List[LoginBonusInfo] = []
self.firstLoginDaily = is_first_login_daily
self.lastLoginDate = last_login_date
def make(self) -> List:
daily = []
consec = []
other = []
for bonus in self.dailyBonus:
daily.append(bonus.make())
for bonus in self.consecBonus:
consec.append(bonus.make())
for bonus in self.otherBonus:
other.append(bonus.make())
self.params = [ daily, consec, other, int(self.firstLoginDaily)]
return super().make()
class UserStatusLoginResponseV2(UserStatusLoginResponseV1):
vipInfo: VipInfo
lastLoginDate: int = 0
def __init__(self, is_first_login_daily: bool = False, last_login_date: int = 0) -> None:
super().__init__(is_first_login_daily)
self.lastLoginDate = last_login_date
self.vipInfo = VipInfo()
def make(self) -> List:
super().make()
self.params.append(self.vipInfo.make())
self.params.append(self.lastLoginDate)
return super(UserStatusLoginResponseV1, self).make()
class UserStatusLoginResponseV3(UserStatusLoginResponseV2):
unk: List = []
def make(self) -> List:
super().make()
self.params.append(self.unk)
return super(UserStatusLoginResponseV1, self).make()
# ---user/status/create---
class UserStatusCreateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.aimeId = data["params"][0]
self.username = data["params"][1]
class UserStatusCreateResponseV1(BaseResponse):
def __init__(self, userId: int, username: str) -> None:
super().__init__()
self.userStatus = UserStatusV1()
self.userStatus.userId = userId
self.userStatus.username = username
def make(self) -> List:
self.params = [
self.userStatus.make()
]
return super().make()
class UserStatusCreateResponseV2(UserStatusCreateResponseV1):
def __init__(self, userId: int, username: str) -> None:
super().__init__(userId, username)
self.userStatus: UserStatusV2 = UserStatusV2()
self.userStatus.userId = userId
self.userStatus.username = username
# ---user/status/logout---
class UserStatusLogoutRequest(BaseRequest):
userId: int
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.userId = data["params"][0]
# ---user/status/update---
class UserStatusUpdateRequestV1(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId: int = data["params"][0]
self.playType: PlayType = PlayType(data["params"][1])
self.itemsRecieved: List[GenericItemRecv] = []
for itm in data["params"][2]:
self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2]))
class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1):
isContinue = False
isFirstPlayFree = False
itemsUsed = []
lastSongInfo: LastSongDetail
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.isContinue = bool(data["params"][3])
self.isFirstPlayFree = bool(data["params"][4])
self.itemsUsed = data["params"][5]
self.lastSongInfo = LastSongDetail(data["params"][6][0], data["params"][6][1],
data["params"][6][2], data["params"][6][3], data["params"][6][4])

View File

@ -0,0 +1,48 @@
from typing import Dict, List
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import StageInfo, StageupClearType
# --user/trial/get--
class UserTrialGetRequest(BaseRequest):
profileId: int = 0
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
class UserTrialGetResponse(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.stageList: List[StageInfo] = []
def make(self) -> Dict:
dans = []
for x in self.stageList:
dans.append(x.make())
self.params = [dans]
return super().make()
# --user/trial/update--
class UserTrialUpdateRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
self.stageId = self.params[1]
self.stageLevel = self.params[2]
self.clearType = StageupClearType(self.params[3])
self.songScores = self.params[4]
self.numSongsCleared = self.params[5]
self.itemsObtained = self.params[6]
self.unk7: List = []
if len(self.params) == 8:
self.unk7 = self.params[7]
class UserTrialUpdateResponse(BaseResponse):
def __init__(self) -> None:
super().__init__()
def make(self) -> Dict:
return super().make()

View File

@ -0,0 +1,54 @@
from typing import Dict, List
from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import VipLoginBonus
# --user/vip/get--
class UserVipGetRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
class UserVipGetResponse(BaseResponse):
def __init__(self) -> None:
super().__init__()
self.vipDays: int = 0
self.unknown1: int = 1
self.unknown2: int = 1
self.presents: List[VipLoginBonus] = []
def make(self) -> Dict:
pres = []
for x in self.presents:
pres.append(x.make())
self.params = [
self.vipDays,
[
self.unknown1,
self.unknown2,
pres
]
]
return super().make()
# --user/vip/start--
class UserVipStartRequest(BaseRequest):
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.profileId = self.params[0]
self.cost = self.params[1]
self.days = self.params[2]
class UserVipStartResponse(BaseResponse):
def __init__(self, expires: int = 0) -> None:
super().__init__()
self.whenExpires: int = expires
self.presents = []
def make(self) -> Dict:
self.params = [
self.whenExpires,
self.presents
]
return super().make()

126
titles/wacca/index.py Normal file
View File

@ -0,0 +1,126 @@
import yaml
import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler
import logging
import json
from datetime import datetime
from hashlib import md5
from twisted.web.http import Request
from typing import Dict
from core.config import CoreConfig
from titles.wacca.config import WaccaConfig
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.reverse import WaccaReverse
from titles.wacca.lilyr import WaccaLilyR
from titles.wacca.lily import WaccaLily
from titles.wacca.s import WaccaS
from titles.wacca.base import WaccaBase
from titles.wacca.handlers.base import BaseResponse
class WaccaServlet():
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
self.core_cfg = core_cfg
self.game_cfg = WaccaConfig()
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/wacca.yaml")))
self.versions = [
WaccaBase(core_cfg, self.game_cfg),
WaccaS(core_cfg, self.game_cfg),
WaccaLily(core_cfg, self.game_cfg),
WaccaLilyR(core_cfg, self.game_cfg),
WaccaReverse(core_cfg, self.game_cfg),
]
self.logger = logging.getLogger("wacca")
log_fmt_str = "[%(asctime)s] Wacca | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
fileHandler = TimedRotatingFileHandler("{0}/{1}.log".format(self.core_cfg.server.log_dir, "wacca"), encoding='utf8',
when="d", backupCount=10)
fileHandler.setFormatter(log_fmt)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(log_fmt)
self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler)
self.logger.setLevel(self.game_cfg.server.loglevel)
coloredlogs.install(level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str)
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
def end(resp: Dict) -> bytes:
hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest()
request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode())
return json.dumps(resp).encode()
version_full = []
try:
req_json = json.loads(request.content.getvalue())
version_full = req_json["appVersion"].split(".")
except:
self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}")
resp = BaseResponse()
resp.status = 1
resp.message = "不正なリクエスト エラーです"
return end(resp.make())
url_split = url_path.split("/")
start_req_idx = url_split.index("api") + 1
func_to_find = "handle_"
for x in range(len(url_split) - start_req_idx):
func_to_find += f"{url_split[x + start_req_idx]}_"
func_to_find += "request"
ver_search = (int(version_full[0]) * 10000) + (int(version_full[1]) * 100) + int(version_full[2])
if ver_search < 15000:
internal_ver = WaccaConstants.VER_WACCA
elif ver_search >= 15000 and ver_search < 20000:
internal_ver = WaccaConstants.VER_WACCA_S
elif ver_search >= 20000 and ver_search < 25000:
internal_ver = WaccaConstants.VER_WACCA_LILY
elif ver_search >= 25000 and ver_search < 30000:
internal_ver = WaccaConstants.VER_WACCA_LILY_R
elif ver_search >= 30000:
internal_ver = WaccaConstants.VER_WACCA_REVERSE
else:
self.logger.warning(f"Unsupported version ({req_json['appVersion']}) request {url_path} - {req_json}")
resp = BaseResponse()
resp.status = 1
resp.message = "不正なアプリバージョンエラーです"
return end(resp.make())
self.logger.info(f"v{req_json['appVersion']} {url_path} request from {request.getClientAddress().host} with chipId {req_json['chipId']}")
self.logger.debug(req_json)
try:
handler = getattr(self.versions[internal_ver], func_to_find)
if handler is not None:
resp = handler(req_json)
else:
self.logger.warn(f"{req_json['appVersion']} has no handler for {func_to_find}")
resp = None
if resp is None:
resp = BaseResponse().make()
self.logger.debug(f"{req_json['appVersion']} response {resp}")
return end(resp)
except Exception as e:
self.logger.error(f"{req_json['appVersion']} Error handling method {url_path} -> {e}")
if self.game_cfg.server.loglevel <= logging.DEBUG:
raise
resp = BaseResponse().make()
return end(resp)

351
titles/wacca/lily.py Normal file
View File

@ -0,0 +1,351 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import json
from core.config import CoreConfig
from titles.wacca.s import WaccaS
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.handlers import *
class WaccaLily(WaccaS):
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = WaccaConstants.VER_WACCA_LILY
self.season = 2
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
self.allowed_stages = [
(2001, 1),
(2002, 2),
(2003, 3),
(2004, 4),
(2005, 5),
(2006, 6),
(2007, 7),
(2008, 8),
(2009, 9),
(2010, 10),
(2011, 11),
(2012, 12),
(2013, 13),
(2014, 14),
(210001, 0),
(210002, 0),
(210003, 0),
]
def handle_user_status_get_request(self, data: Dict) -> List[Any]:
req = UserStatusGetRequest(data)
resp = UserStatusGetV2Response()
ver_split = req.appVersion.split(".")
profile = 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:
profile_ver_split = ver_split
resp.lastGameVersion = req.appVersion
else:
profile_ver_split = profile["last_game_ver"].split(".")
resp.lastGameVersion = 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"]
resp.userStatus.loginDays = profile["login_count_days"]
resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"]
resp.userStatus.loginsToday = profile["login_count_today"]
resp.userStatus.rating = profile["rating"]
set_title_id = 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 = 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 profile["last_login_date"].timestamp() < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()):
resp.userStatus.loginsToday = 0
if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()):
resp.userStatus.loginConsecutiveDays = 0
if int(ver_split[0]) > int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[1]) > int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[1]) < int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[2]) > int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[2]) < int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
if profile["vip_expire_time"] is not None:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
if profile["always_vip"] or self.game_config.mods.always_vip:
resp.userStatus.vipExpireTime = int((datetime.now() + timedelta(days=30)).timestamp())
if self.game_config.mods.infinite_wp:
resp.userStatus.wp = 999999
return resp.make()
def handle_user_status_login_request(self, data: Dict) -> List[Any]:
req = UserStatusLoginRequest(data)
resp = UserStatusLoginResponseV2()
is_new_day = False
is_consec_day = False
is_consec_day = True
if req.userId == 0:
self.logger.info(f"Guest login on {req.chipId}")
resp.lastLoginDate = 0
else:
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(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
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < int(datetime.now().replace(hour=0,minute=0,second=0,microsecond=0).timestamp()):
is_new_day = True
is_consec_day = True
# If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak
elif last_login_time > int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) + timedelta(days=1)).timestamp()):
is_consec_day = False
# else, they are simply logging in again on the same day, and we don't need to do anything for that
self.data.profile.session_login(req.userId, is_new_day, is_consec_day)
resp.vipInfo.pageYear = datetime.now().year
resp.vipInfo.pageMonth = datetime.now().month
resp.vipInfo.pageDay = datetime.now().day
resp.vipInfo.numItem = 1
resp.firstLoginDaily = int(is_new_day)
return resp.make()
def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]:
req = UserStatusGetDetailRequest(data)
ver_split = req.appVersion.split(".")
if int(ver_split[1]) >= 53:
resp = UserStatusGetDetailResponseV3()
else:
resp = UserStatusGetDetailResponseV2()
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")
user_id = profile["user"]
profile_scores = self.data.score.get_best_scores(user_id)
profile_items = self.data.item.get_items(user_id)
profile_song_unlocks = self.data.item.get_song_unlocks(user_id)
profile_options = self.data.profile.get_options(user_id)
profile_favorites = self.data.profile.get_favorite_songs(user_id)
profile_gates = self.data.profile.get_gates(user_id)
profile_trophies = self.data.item.get_trophies(user_id)
profile_tickets = self.data.item.get_tickets(user_id)
if profile["vip_expire_time"] is None:
resp.userStatus.vipExpireTime = 0
else:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
if profile["always_vip"] or self.game_config.mods.always_vip:
resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp())
resp.songUpdateTime = int(profile["last_login_date"].timestamp())
resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"])
resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 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"]
resp.userStatus.loginDays = profile["login_count_days"]
resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"]
resp.userStatus.loginsToday = profile["login_count_today"]
resp.userStatus.rating = profile['rating']
if self.game_config.mods.infinite_wp:
resp.userStatus.wp = 999999
for fav in profile_favorites:
resp.favorites.append(fav["song_id"])
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 gate in self.game_config.gates.enabled_gates:
added_gate = False
for user_gate in profile_gates:
if user_gate["gate_id"] == gate:
if int(ver_split[1]) >= 53:
resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"],
user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"]))
else:
resp.gateInfo.append(GateDetailV1(user_gate["gate_id"],user_gate["page"],user_gate["progress"],
user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"]))
resp.seasonInfo.cumulativeGatePts += user_gate["total_points"]
added_gate = True
break
if not added_gate:
if int(ver_split[1]) >= 53:
resp.gateInfo.append(GateDetailV2(gate))
else:
resp.gateInfo.append(GateDetailV1(gate))
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())))
if x > 2:
resp.scores.append(BestScoreDetailV1(unlock["song_id"], x))
empty_scores = len(resp.scores)
for song in profile_scores:
resp.seasonInfo.cumulativeScore += song["score"]
empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores)
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"]
)
if empty_score_idx is not None:
resp.scores[empty_score_idx].clearCounts = clear_cts
resp.scores[empty_score_idx].clearCountsSeason = clear_cts
resp.scores[empty_score_idx].gradeCounts = grade_cts
resp.scores[empty_score_idx].score = song["score"]
resp.scores[empty_score_idx].bestCombo = song["best_combo"]
resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"]
resp.scores[empty_score_idx].rating = song["rating"]
else:
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"]
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())))
elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]:
resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"]))
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["user_plate"]:
resp.userItems.plates.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:
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)
resp.seasonInfo.platesObtained = len(resp.userItems.plates)
return resp.make()

54
titles/wacca/lilyr.py Normal file
View File

@ -0,0 +1,54 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import json
from core.config import CoreConfig
from titles.wacca.lily import WaccaLily
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.handlers import *
class WaccaLilyR(WaccaLily):
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = WaccaConstants.VER_WACCA_LILY_R
self.season = 2
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
self.allowed_stages = [
(2501, 1),
(2502, 2),
(2503, 3),
(2504, 4),
(2505, 5),
(2506, 6),
(2507, 7),
(2508, 8),
(2509, 9),
(2510, 10),
(2511, 11),
(2512, 12),
(2513, 13),
(2514, 14),
(210001, 0),
(210002, 0),
(210003, 0),
]
def handle_user_status_create_request(self, data: Dict) -> List[Any]:
req = UserStatusCreateRequest(data)
resp = super().handle_user_status_create_request(data)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210054) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210055) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210056) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210057) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210058) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210059) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210060) # Added lily r
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210061) # Added lily r
return resp
def handle_user_status_logout_request(self, data: Dict) -> List[Any]:
return BaseResponse().make()

80
titles/wacca/read.py Normal file
View File

@ -0,0 +1,80 @@
from typing import Optional
import wacky
import json
from os import walk, path
from read import BaseReader
from core.config import CoreConfig
from titles.wacca.database import WaccaData
from titles.wacca.const import WaccaConstants
class WaccaReader(BaseReader):
def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None:
super().__init__(config, version, bin_dir, opt_dir, extra)
self.data = WaccaData(config)
try:
self.logger.info(f"Start importer for {WaccaConstants.game_ver_to_string(version)}")
except IndexError:
self.logger.error(f"Invalid wacca version {version}")
exit(1)
def read(self) -> None:
if not (path.exists(f"{self.bin_dir}/Table") and path.exists(f"{self.bin_dir}/Message")):
self.logger.error("Could not find Table or Message folder, nothing to read")
return
self.read_music(f"{self.bin_dir}/Table", "MusicParameterTable")
def read_music(self, base_dir: str, table: str) -> None:
if not self.check_valid_pair(base_dir, table):
self.logger.warn(f"Cannot find {table} uasset/uexp pair at {base_dir}, music will not be read")
return
uasset=open(f"{base_dir}/{table}.uasset", "rb")
uexp=open(f"{base_dir}/{table}.uexp", "rb")
package = wacky.jsonify(uasset,uexp)
package_json = json.dumps(package, indent=4, sort_keys=True)
data=json.loads(package_json)
first_elem = data[0]
wacca_data = first_elem['rows']
for i, key in enumerate(wacca_data):
song_id = int(key)
title = wacca_data[str(key)]["MusicMessage"]
artist = wacca_data[str(key)]["ArtistMessage"]
bpm = wacca_data[str(key)]["Bpm"]
jacket_asset_name = wacca_data[str(key)]["JacketAssetName"]
diff = float(wacca_data[str(key)]["DifficultyNormalLv"])
designer = wacca_data[str(key)]["NotesDesignerNormal"]
if diff > 0:
self.data.static.put_music(self.version, song_id, 1, title, artist, bpm, diff, designer, jacket_asset_name)
self.logger.info(f"Read song {song_id} chart 1")
diff = float(wacca_data[str(key)]["DifficultyHardLv"])
designer = wacca_data[str(key)]["NotesDesignerHard"]
if diff > 0:
self.data.static.put_music(self.version, song_id, 2, title, artist, bpm, diff, designer, jacket_asset_name)
self.logger.info(f"Read song {song_id} chart 2")
diff = float(wacca_data[str(key)]["DifficultyExtremeLv"])
designer = wacca_data[str(key)]["NotesDesignerExpert"]
if diff > 0:
self.data.static.put_music(self.version, song_id, 3, title, artist, bpm, diff, designer, jacket_asset_name)
self.logger.info(f"Read song {song_id} chart 3")
diff = float(wacca_data[str(key)]["DifficultyInfernoLv"])
designer = wacca_data[str(key)]["NotesDesignerInferno"]
if diff > 0:
self.data.static.put_music(self.version, song_id, 4, title, artist, bpm, diff, designer, jacket_asset_name)
self.logger.info(f"Read song {song_id} chart 4")
def check_valid_pair(self, dir: str, file: str) -> bool:
return path.exists(f"{dir}/{file}.uasset") and path.exists(f"{dir}/{file}.uexp")

258
titles/wacca/reverse.py Normal file
View File

@ -0,0 +1,258 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import json
from core.config import CoreConfig
from titles.wacca.lilyr import WaccaLilyR
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.handlers import *
class WaccaReverse(WaccaLilyR):
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = WaccaConstants.VER_WACCA_REVERSE
self.season = 3
self.OPTIONS_DEFAULTS["set_nav_id"] = 310001
self.allowed_stages = [
(3001, 1),
(3002, 2),
(3003, 3),
(3004, 4),
(3005, 5),
(3006, 6),
(3007, 7),
(3008, 8),
(3009, 9),
(3010, 10),
(3011, 11),
(3012, 12),
(3013, 13),
(3014, 14),
# Touhou
(210001, 0),
(210002, 0),
(210003, 0),
# Final spurt
(310001, 0),
(310002, 0),
(310003, 0),
# boss remix
(310004, 0),
(310005, 0),
(310006, 0),
]
def handle_user_status_login_request(self, data: Dict) -> List[Any]:
resp = super().handle_user_status_login_request(data)
resp["params"].append([])
return resp
def handle_user_status_getDetail_request(self, data: Dict) -> List[Any]:
req = UserStatusGetDetailRequest(data)
resp = UserStatusGetDetailResponseV4()
profile = self.data.profile.get_profile(req.userId)
if profile is None:
self.logger.warn(f"Unknown profile {req.userId}")
return resp.make()
self.logger.info(f"Get detail for profile {req.userId}")
user_id = profile["user"]
profile_scores = self.data.score.get_best_scores(user_id)
profile_items = self.data.item.get_items(user_id)
profile_song_unlocks = self.data.item.get_song_unlocks(user_id)
profile_options = self.data.profile.get_options(user_id)
profile_favorites = self.data.profile.get_favorite_songs(user_id)
profile_gates = self.data.profile.get_gates(user_id)
profile_bingo = self.data.profile.get_bingo(user_id)
profile_trophies = self.data.item.get_trophies(user_id)
profile_tickets = self.data.item.get_tickets(user_id)
if profile["gate_tutorial_flags"] is not None:
for x in profile["gate_tutorial_flags"]:
resp.gateTutorialFlags.append(GateTutorialFlag(x[0], x[1]))
if profile["vip_expire_time"] is None:
resp.userStatus.vipExpireTime = 0
else:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
if profile["always_vip"] or self.game_config.mods.always_vip:
resp.userStatus.vipExpireTime = int((self.srvtime + timedelta(days=31)).timestamp())
resp.songUpdateTime = int(profile["last_login_date"].timestamp())
resp.lastSongInfo = LastSongDetail(profile["last_song_id"],profile["last_song_difficulty"],profile["last_folder_order"],profile["last_folder_id"],profile["last_song_order"])
resp.songPlayStatus = [resp.lastSongInfo.lastSongId, 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"]
resp.userStatus.loginDays = profile["login_count_days"]
resp.userStatus.loginConsecutiveDays = profile["login_count_days_consec"]
resp.userStatus.loginsToday = profile["login_count_today"]
resp.userStatus.rating = profile['rating']
if self.game_config.mods.infinite_wp:
resp.userStatus.wp = 999999
for fav in profile_favorites:
resp.favorites.append(fav["song_id"])
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"]))
if profile_bingo is not None:
resp.bingoStatus = BingoDetail(profile_bingo["page_number"])
for x in profile_bingo["page_progress"]:
resp.bingoStatus.pageStatus.append(BingoPageStatus(x[0], x[1], x[2]))
for gate in self.game_config.gates.enabled_gates:
added_gate = False
for user_gate in profile_gates:
if user_gate["gate_id"] == gate:
resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"],
user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"]))
resp.seasonInfo.cumulativeGatePts += user_gate["total_points"]
added_gate = True
break
if not added_gate:
resp.gateInfo.append(GateDetailV2(gate))
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())))
if x > 2:
resp.scores.append(BestScoreDetailV2(unlock["song_id"], x))
empty_scores = len(resp.scores)
for song in profile_scores:
resp.seasonInfo.cumulativeScore += song["score"]
empty_score_idx = resp.find_score_idx(song["song_id"], song["chart_id"], 0, empty_scores)
clear_cts = SongDetailClearCounts(
song["play_ct"],
song["clear_ct"],
song["missless_ct"],
song["fullcombo_ct"],
song["allmarv_ct"],
)
grade_cts = SongDetailGradeCountsV2(
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"], song["grade_sp_ct"], song["grade_ssp_ct"], song["grade_sssp_ct"]
)
if empty_score_idx is not None:
resp.scores[empty_score_idx].clearCounts = clear_cts
resp.scores[empty_score_idx].clearCountsSeason = clear_cts
resp.scores[empty_score_idx].gradeCounts = grade_cts
resp.scores[empty_score_idx].score = song["score"]
resp.scores[empty_score_idx].bestCombo = song["best_combo"]
resp.scores[empty_score_idx].lowestMissCtMaybe = song["lowest_miss_ct"]
resp.scores[empty_score_idx].rating = song["rating"]
else:
deets = BestScoreDetailV2(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())))
elif item["type"] == WaccaConstants.ITEM_TYPES["navigator"]:
resp.userItems.navigators.append(NavigatorItem(item["item_id"], 1, int(item["acquire_date"].timestamp()), item["use_count"], item["use_count"]))
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["user_plate"]:
resp.userItems.plates.append(itm_send)
elif item["type"] == WaccaConstants.ITEM_TYPES["touch_effect"]:
resp.userItems.touchEffect.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:
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)
resp.seasonInfo.platesObtained = len(resp.userItems.plates)
return resp.make()
def handle_user_status_create_request(self, data: Dict) -> List[Any]:
req = UserStatusCreateRequest(data)
resp = super().handle_user_status_create_request(data)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310001) # Added reverse
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 310002) # Added reverse
return resp

35
titles/wacca/s.py Normal file
View File

@ -0,0 +1,35 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import json
from core.config import CoreConfig
from titles.wacca.base import WaccaBase
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.handlers import *
class WaccaS(WaccaBase):
allowed_stages = [
(1501, 1),
(1502, 2),
(1503, 3),
(1504, 4),
(1505, 5),
(1506, 6),
(1507, 7),
(1508, 8),
(1509, 9),
(1510, 10),
(1511, 11),
(1512, 12),
(1513, 13),
]
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
super().__init__(cfg, game_cfg)
self.version = WaccaConstants.VER_WACCA_S
def handle_advertise_GetNews_request(self, data: Dict) -> List[Any]:
resp = GetNewsResponseV2()
return resp.make()

View File

@ -0,0 +1,6 @@
from titles.wacca.schema.profile import WaccaProfileData
from titles.wacca.schema.score import WaccaScoreData
from titles.wacca.schema.item import WaccaItemData
from titles.wacca.schema.static import WaccaStaticData
__all__ = ["WaccaProfileData", "WaccaScoreData", "WaccaItemData", "WaccaStaticData"]

177
titles/wacca/schema/item.py Normal file
View File

@ -0,0 +1,177 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
item = Table(
"wacca_item",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("item_id", Integer, nullable=False),
Column("type", Integer, nullable=False),
Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()),
Column("use_count", Integer, server_default="0"),
UniqueConstraint("user", "item_id", "type", name="wacca_item_uk"),
mysql_charset='utf8mb4'
)
ticket = Table(
"wacca_ticket",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("ticket_id", Integer, nullable=False),
Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()),
Column("expire_date", TIMESTAMP),
mysql_charset='utf8mb4'
)
song_unlock = Table(
"wacca_song_unlock",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("song_id", Integer, nullable=False),
Column("highest_difficulty", Integer, nullable=False),
Column("acquire_date", TIMESTAMP, nullable=False, server_default=func.now()),
UniqueConstraint("user", "song_id", name="wacca_song_unlock_uk"),
mysql_charset='utf8mb4'
)
trophy = Table(
"wacca_trophy",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("trophy_id", Integer, nullable=False),
Column("season", Integer, nullable=False),
Column("progress", Integer, nullable=False, server_default="0"),
Column("badge_type", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", "trophy_id", "season", name="wacca_trophy_uk"),
mysql_charset='utf8mb4'
)
class WaccaItemData(BaseData):
def get_song_unlocks(self, user_id: int) -> Optional[List[Row]]:
sql = song_unlock.select(song_unlock.c.user == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def unlock_song(self, user_id: int, song_id: int, difficulty: int) -> Optional[int]:
sql = insert(song_unlock).values(
user=user_id,
song_id=song_id,
highest_difficulty=difficulty
)
conflict = sql.on_duplicate_key_update(
highest_difficulty=case(
(song_unlock.c.highest_difficulty >= difficulty, song_unlock.c.highest_difficulty),
(song_unlock.c.highest_difficulty < difficulty, difficulty),
)
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to unlock song! user: {user_id}, song_id: {song_id}, difficulty: {difficulty}")
return None
return result.lastrowid
def put_item(self, user_id: int, item_type: int, item_id: int) -> Optional[int]:
sql = insert(item).values(
user = user_id,
item_id = item_id,
type = item_type,
)
conflict = sql.on_duplicate_key_update(
use_count = item.c.use_count + 1
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}, item_type: {item_type}")
return None
return result.lastrowid
def get_items(self, user_id: int, item_type: int = None, item_id: int = None) -> Optional[List[Row]]:
"""
A catch-all item lookup given a profile and option item type and ID specifiers
"""
sql = item.select(
and_(item.c.user == user_id,
item.c.type == item_type if item_type is not None else True,
item.c.item_id == item_id if item_id is not None else True)
)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def get_tickets(self, user_id: int) -> Optional[List[Row]]:
sql = select(ticket).where(ticket.c.user == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def add_ticket(self, user_id: int, ticket_id: int) -> None:
sql = insert(ticket).values(
user = user_id,
ticket_id = ticket_id
)
result = self.execute(sql)
if result is None:
self.logger.error(f"add_ticket: Failed to insert wacca ticket! user_id: {user_id} ticket_id {ticket_id}")
return None
return result.lastrowid
def spend_ticket(self, id: int) -> None:
sql = delete(ticket).where(ticket.c.id == id)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to delete ticket id {id}")
return None
def get_trophies(self, user_id: int, season: int = None) -> Optional[List[Row]]:
if season is None:
sql = select(trophy).where(trophy.c.user == user_id)
else:
sql = select(trophy).where(and_(trophy.c.user == user_id, trophy.c.season == season))
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def update_trophy(self, user_id: int, trophy_id: int, season: int, progress: int, badge_type: int) -> Optional[int]:
sql = insert(trophy).values(
user = user_id,
trophy_id = trophy_id,
season = season,
progress = progress,
badge_type = badge_type
)
conflict = sql.on_duplicate_key_update(
progress = progress
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"update_trophy: Failed to insert wacca trophy! user_id: {user_id} trophy_id: {trophy_id} progress {progress}")
return None
return result.lastrowid

View File

@ -0,0 +1,428 @@
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
profile = Table(
"wacca_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("version", Integer),
Column("username", String(8), nullable=False),
Column("xp", Integer, server_default="0"),
Column("wp", Integer, server_default="0"),
Column("wp_total", Integer, server_default="0"),
Column("wp_spent", Integer, server_default="0"),
Column("dan_type", Integer, server_default="0"),
Column("dan_level", Integer, server_default="0"),
Column("title_0", Integer, server_default="0"),
Column("title_1", Integer, server_default="0"),
Column("title_2", Integer, server_default="0"),
Column("rating", Integer, server_default="0"),
Column("vip_expire_time", TIMESTAMP),
Column("always_vip", Boolean, server_default="0"),
Column("login_count", Integer, server_default="0"),
Column("login_count_consec", Integer, server_default="0"),
Column("login_count_days", Integer, server_default="0"),
Column("login_count_days_consec", Integer, server_default="0"),
Column("login_count_today", Integer, server_default="0"),
Column("playcount_single", Integer, server_default="0"),
Column("playcount_multi_vs", Integer, server_default="0"),
Column("playcount_multi_coop", Integer, server_default="0"),
Column("playcount_stageup", Integer, server_default="0"),
Column("friend_view_1", Integer),
Column("friend_view_2", Integer),
Column("friend_view_3", Integer),
Column("last_game_ver", String(50)),
Column("last_song_id", Integer, server_default="0"),
Column("last_song_difficulty", Integer, server_default="0"),
Column("last_folder_order", Integer, server_default="0"),
Column("last_folder_id", Integer, server_default="0"),
Column("last_song_order", Integer, server_default="0"),
Column("last_login_date", TIMESTAMP, server_default=func.now()),
Column("gate_tutorial_flags", JSON),
UniqueConstraint("user", "version", name="wacca_profile_uk"),
mysql_charset='utf8mb4'
)
option = Table(
"wacca_option",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("opt_id", Integer, nullable=False),
Column("value", Integer, nullable=False),
UniqueConstraint("user", "opt_id", name="wacca_option_uk"),
)
bingo = Table(
"wacca_bingo",
metadata,
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), primary_key=True, nullable=False),
Column("page_number", Integer, nullable=False),
Column("page_progress", JSON, nullable=False),
UniqueConstraint("user", "page_number", name="wacca_bingo_uk"),
mysql_charset='utf8mb4'
)
friend = Table(
"wacca_friend",
metadata,
Column("profile_sender", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("profile_reciever", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("is_accepted", Boolean, server_default="0"),
PrimaryKeyConstraint('profile_sender', 'profile_reciever', name='arcade_owner_pk'),
mysql_charset='utf8mb4'
)
favorite = Table(
"wacca_favorite_song",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("song_id", Integer, nullable=False),
UniqueConstraint("user", "song_id", name="wacca_favorite_song_uk"),
mysql_charset='utf8mb4'
)
gate = Table(
"wacca_gate",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("gate_id", Integer, nullable=False),
Column("page", Integer, nullable=False, server_default="0"),
Column("progress", Integer, nullable=False, server_default="0"),
Column("loops", Integer, nullable=False, server_default="0"),
Column("last_used", TIMESTAMP, nullable=False, server_default=func.now()),
Column("mission_flag", Integer, nullable=False, server_default="0"),
Column("total_points", Integer, nullable=False, server_default="0"),
UniqueConstraint("user", "gate_id", name="wacca_gate_uk"),
)
class WaccaProfileData(BaseData):
def create_profile(self, aime_id: int, username: str, version: int) -> Optional[int]:
"""
Given a game version, aime id, and username, create a profile and return it's ID
"""
sql = insert(profile).values(
user=aime_id,
username=username,
version=version
)
conflict = sql.on_duplicate_key_update(
username = sql.inserted.username
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} Failed to insert wacca profile! aime id: {aime_id} username: {username}")
return None
return result.lastrowid
def update_profile_playtype(self, profile_id: int, play_type: int, game_version: str) -> None:
sql = profile.update(profile.c.id == profile_id).values(
playcount_single = profile.c.playcount_single + 1 if play_type == 1 else profile.c.playcount_single,
playcount_multi_vs = profile.c.playcount_multi_vs + 1 if play_type == 2 else profile.c.playcount_multi_vs,
playcount_multi_coop = profile.c.playcount_multi_coop + 1 if play_type == 3 else profile.c.playcount_multi_coop,
playcount_stageup = profile.c.playcount_stageup + 1 if play_type == 4 else profile.c.playcount_stageup,
last_game_ver = game_version,
)
result = self.execute(sql)
if result is None:
self.logger.error(f"update_profile: failed to update profile! profile: {profile_id}")
return None
def update_profile_lastplayed(self, profile_id: int, last_song_id: int, last_song_difficulty: int, last_folder_order: int,
last_folder_id: int, last_song_order: int) -> None:
sql = profile.update(profile.c.id == profile_id).values(
last_song_id = last_song_id,
last_song_difficulty = last_song_difficulty,
last_folder_order = last_folder_order,
last_folder_id = last_folder_id,
last_song_order = last_song_order
)
result = self.execute(sql)
if result is None:
self.logger.error(f"update_profile_lastplayed: failed to update profile! profile: {profile_id}")
return None
def update_profile_dan(self, profile_id: int, dan_level: int, dan_type: int) -> Optional[int]:
sql = profile.update(profile.c.id == profile_id).values(
dan_level = dan_level,
dan_type = dan_type
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"update_profile_dan: Failed to update! profile {profile_id}")
return None
return result.lastrowid
def get_profile(self, profile_id: int = 0, aime_id: int = None) -> Optional[Row]:
"""
Given a game version and either a profile or aime id, return the profile
"""
if aime_id is not None:
sql = profile.select(profile.c.user == aime_id)
elif profile_id > 0:
sql = profile.select(profile.c.id == profile_id)
else:
self.logger.error(f"get_profile: Bad arguments!! profile_id {profile_id} aime_id {aime_id}")
return None
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def get_options(self, user_id: int, option_id: int = None) -> Optional[List[Row]]:
"""
Get a specific user option for a profile, or all of them if none specified
"""
sql = option.select(
and_(option.c.user == user_id,
option.c.opt_id == option_id if option_id is not None else True)
)
result = self.execute(sql)
if result is None: return None
if option_id is not None:
return result.fetchone()
else:
return result.fetchall()
def update_option(self, user_id: int, option_id: int, value: int) -> Optional[int]:
sql = insert(option).values(
user = user_id,
opt_id = option_id,
value = value
)
conflict = sql.on_duplicate_key_update(
value = sql.inserted.value
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to insert option! profile: {user_id}, option: {option_id}, value: {value}")
return None
return result.lastrowid
def add_favorite_song(self, user_id: int, song_id: int) -> Optional[int]:
sql = favorite.insert().values(
user=user_id,
song_id=song_id
)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} failed to insert favorite! profile: {user_id}, song_id: {song_id}")
return None
return result.lastrowid
def remove_favorite_song(self, user_id: int, song_id: int) -> None:
sql = favorite.delete(and_(favorite.c.user == user_id, favorite.c.song_id == song_id))
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} failed to remove favorite! profile: {user_id}, song_id: {song_id}")
return None
def get_favorite_songs(self, user_id: int) -> Optional[List[Row]]:
sql = favorite.select(favorite.c.user == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def get_gates(self, user_id: int) -> Optional[List[Row]]:
sql = select(gate).where(gate.c.user == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def update_gate(self, user_id: int, gate_id: int, page: int, progress: int, loop: int, mission_flag: int,
total_points: int) -> Optional[int]:
sql = insert(gate).values(
user=user_id,
gate_id=gate_id,
page=page,
progress=progress,
loops=loop,
mission_flag=mission_flag,
total_points=total_points
)
conflict = sql.on_duplicate_key_update(
page=sql.inserted.page,
progress=sql.inserted.progress,
loops=sql.inserted.loops,
mission_flag=sql.inserted.mission_flag,
total_points=sql.inserted.total_points,
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__} failed to update gate! user: {user_id}, gate_id: {gate_id}")
return None
return result.lastrowid
def get_friends(self, user_id: int) -> Optional[List[Row]]:
sql = friend.select(friend.c.profile_sender == user_id)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def profile_to_aime_user(self, profile_id: int) -> Optional[int]:
sql = select(profile.c.user).where(profile.c.id == profile_id)
result = self.execute(sql)
if result is None:
self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}")
return None
this_profile = result.fetchone()
if this_profile is None:
self.logger.info(f"profile_to_aime_user: No user found for profile {profile_id}")
return None
return this_profile['user']
def session_login(self, profile_id: int, is_new_day: bool, is_consec_day: bool) -> None:
# TODO: Reset consec days counter
sql = profile.update(profile.c.id == profile_id).values(
login_count = profile.c.login_count + 1,
login_count_consec = profile.c.login_count_consec + 1,
login_count_days = profile.c.login_count_days + 1 if is_new_day else profile.c.login_count_days,
login_count_days_consec = profile.c.login_count_days_consec + 1 if is_new_day and is_consec_day else profile.c.login_count_days_consec,
login_count_today = 1 if is_new_day else profile.c.login_count_today + 1,
last_login_date = func.now()
)
result = self.execute(sql)
if result is None:
self.logger.error(f"session_login: failed to update profile! profile: {profile_id}")
return None
def session_logout(self, profile_id: int) -> None:
sql = profile.update(profile.c.id == id).values(
login_count_consec = 0
)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} failed to update profile! profile: {profile_id}")
return None
def add_xp(self, profile_id: int, xp: int) -> None:
sql = profile.update(profile.c.id == profile_id).values(
xp = profile.c.xp + xp
)
result = self.execute(sql)
if result is None:
self.logger.error(f"add_xp: Failed to update profile! profile_id {profile_id} xp {xp}")
return None
def add_wp(self, profile_id: int, wp: int) -> None:
sql = profile.update(profile.c.id == profile_id).values(
wp = profile.c.wp + wp,
wp_total = profile.c.wp_total + wp,
)
result = self.execute(sql)
if result is None:
self.logger.error(f"add_wp: Failed to update profile! profile_id {profile_id} wp {wp}")
return None
def spend_wp(self, profile_id: int, wp: int) -> None:
sql = profile.update(profile.c.id == profile_id).values(
wp = profile.c.wp - wp,
wp_spent = profile.c.wp_spent + wp,
)
result = self.execute(sql)
if result is None:
self.logger.error(f"spend_wp: Failed to update profile! profile_id {profile_id} wp {wp}")
return None
def activate_vip(self, profile_id: int, expire_time) -> None:
sql = profile.update(profile.c.id == profile_id).values(
vip_expire_time = expire_time
)
result = self.execute(sql)
if result is None:
self.logger.error(f"activate_vip: Failed to update profile! profile_id {profile_id} expire_time {expire_time}")
return None
def update_user_rating(self, profile_id: int, new_rating: int) -> None:
sql = profile.update(profile.c.id == profile_id).values(
rating = new_rating
)
result = self.execute(sql)
if result is None:
self.logger.error(f"update_user_rating: Failed to update profile! profile_id {profile_id} new_rating {new_rating}")
return None
def update_bingo(self, aime_id: int, page: int, progress: int) -> Optional[int]:
sql = insert(bingo).values(
user=aime_id,
page_number=page,
page_progress=progress
)
conflict = sql.on_duplicate_key_update(
page_number=page,
page_progress=progress
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"put_bingo: failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def get_bingo(self, aime_id: int) -> Optional[List[Row]]:
sql = select(bingo).where(bingo.c.user==aime_id)
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def get_bingo_page(self, aime_id: int, page: Dict) -> Optional[List[Row]]:
sql = select(bingo).where(and_(bingo.c.user==aime_id, bingo.c.page_number==page))
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def update_vip_time(self, profile_id: int, time_left) -> None:
sql = profile.update(profile.c.id == profile_id).values(vip_expire_time = time_left)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to update VIP time for profile {profile_id}")
def update_tutorial_flags(self, profile_id: int, flags: Dict) -> None:
sql = profile.update(profile.c.id == profile_id).values(gate_tutorial_flags = flags)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to update tutorial flags for profile {profile_id}")

View File

@ -0,0 +1,260 @@
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
best_score = Table(
"wacca_score_best",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("song_id", Integer),
Column("chart_id", Integer),
Column("score", Integer),
Column("play_ct", Integer),
Column("clear_ct", Integer),
Column("missless_ct", Integer),
Column("fullcombo_ct", Integer),
Column("allmarv_ct", Integer),
Column("grade_d_ct", Integer),
Column("grade_c_ct", Integer),
Column("grade_b_ct", Integer),
Column("grade_a_ct", Integer),
Column("grade_aa_ct", Integer),
Column("grade_aaa_ct", Integer),
Column("grade_s_ct", Integer),
Column("grade_ss_ct", Integer),
Column("grade_sss_ct", Integer),
Column("grade_master_ct", Integer),
Column("grade_sp_ct", Integer),
Column("grade_ssp_ct", Integer),
Column("grade_sssp_ct", Integer),
Column("best_combo", Integer),
Column("lowest_miss_ct", Integer),
Column("rating", Integer),
UniqueConstraint("user", "song_id", "chart_id", name="wacca_score_uk"),
mysql_charset='utf8mb4'
)
playlog = Table(
"wacca_score_playlog",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("song_id", Integer),
Column("chart_id", Integer),
Column("score", Integer),
Column("clear", Integer),
Column("grade", Integer),
Column("max_combo", Integer),
Column("marv_ct", Integer),
Column("great_ct", Integer),
Column("good_ct", Integer),
Column("miss_ct", Integer),
Column("fast_ct", Integer),
Column("late_ct", Integer),
Column("season", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset='utf8mb4'
)
stageup = Table(
"wacca_score_stageup",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("version", Integer),
Column("stage_id", Integer),
Column("clear_status", Integer),
Column("clear_song_ct", Integer),
Column("song1_score", Integer),
Column("song2_score", Integer),
Column("song3_score", Integer),
Column("play_ct", Integer, server_default="1"),
UniqueConstraint("user", "stage_id", name="wacca_score_stageup_uk"),
mysql_charset='utf8mb4'
)
class WaccaScoreData(BaseData):
def put_best_score(self, user_id: int, song_id: int, chart_id: int, score: int, clear: List[int],
grade: List[int], best_combo: int, lowest_miss_ct: int) -> Optional[int]:
"""
Update the user's best score for a chart
"""
while len(grade) < 13:
grade.append(0)
sql = insert(best_score).values(
user=user_id,
song_id=song_id,
chart_id=chart_id,
score=score,
play_ct=clear[0],
clear_ct=clear[1],
missless_ct=clear[2],
fullcombo_ct=clear[3],
allmarv_ct=clear[4],
grade_d_ct=grade[0],
grade_c_ct=grade[1],
grade_b_ct=grade[2],
grade_a_ct=grade[3],
grade_aa_ct=grade[4],
grade_aaa_ct=grade[5],
grade_s_ct=grade[6],
grade_ss_ct=grade[7],
grade_sss_ct=grade[8],
grade_master_ct=grade[9],
grade_sp_ct=grade[10],
grade_ssp_ct=grade[11],
grade_sssp_ct=grade[12],
best_combo=best_combo,
lowest_miss_ct=lowest_miss_ct,
rating=0
)
conflict = sql.on_duplicate_key_update(
score=score,
play_ct=clear[0],
clear_ct=clear[1],
missless_ct=clear[2],
fullcombo_ct=clear[3],
allmarv_ct=clear[4],
grade_d_ct=grade[0],
grade_c_ct=grade[1],
grade_b_ct=grade[2],
grade_a_ct=grade[3],
grade_aa_ct=grade[4],
grade_aaa_ct=grade[5],
grade_s_ct=grade[6],
grade_ss_ct=grade[7],
grade_sss_ct=grade[8],
grade_master_ct=grade[9],
grade_sp_ct=grade[10],
grade_ssp_ct=grade[11],
grade_sssp_ct=grade[12],
best_combo=best_combo,
lowest_miss_ct=lowest_miss_ct,
)
result = self.execute(conflict)
if result is None:
self.logger.error(f"{__name__}: failed to insert best score! profile: {user_id}, song: {song_id}, chart: {chart_id}")
return None
return result.lastrowid
def put_playlog(self, user_id: int, song_id: int, chart_id: int, this_score: int, clear: int, grade: int, max_combo: int,
marv_ct: int, great_ct: int, good_ct: int, miss_ct: int, fast_ct: int, late_ct: int, season: int) -> Optional[int]:
"""
Add an entry to the user's play log
"""
sql = playlog.insert().values(
user=user_id,
song_id=song_id,
chart_id=chart_id,
score=this_score,
clear=clear,
grade=grade,
max_combo=max_combo,
marv_ct=marv_ct,
great_ct=great_ct,
good_ct=good_ct,
miss_ct=miss_ct,
fast_ct=fast_ct,
late_ct=late_ct,
season=season
)
result = self.execute(sql)
if result is None:
self.logger.error(f"{__name__} failed to insert playlog! profile: {user_id}, song: {song_id}, chart: {chart_id}")
return None
return result.lastrowid
def get_best_score(self, user_id: int, song_id: int, chart_id: int) -> Optional[Row]:
sql = best_score.select(
and_(best_score.c.user == user_id, best_score.c.song_id == song_id, best_score.c.chart_id == chart_id)
)
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def get_best_scores(self, user_id: int) -> Optional[List[Row]]:
sql = best_score.select(
best_score.c.user == user_id
)
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def update_song_rating(self, user_id: int, song_id: int, chart_id: int, new_rating: int) -> None:
sql = best_score.update(
and_(
best_score.c.user == user_id,
best_score.c.song_id == song_id,
best_score.c.chart_id == chart_id
)).values(
rating = new_rating
)
result = self.execute(sql)
if result is None:
self.logger.error(f"update_song_rating: failed to update rating! user_id: {user_id} song_id: {song_id} chart_id {chart_id} new_rating {new_rating}")
return None
def put_stageup(self, user_id: int, version: int, stage_id: int, clear_status: int, clear_song_ct: int, score1: int,
score2: int, score3: int) -> Optional[int]:
sql = insert(stageup).values(
user = user_id,
version = version,
stage_id = stage_id,
clear_status = clear_status,
clear_song_ct = clear_song_ct,
song1_score = score1,
song2_score = score2,
song3_score = score3,
)
conflict = sql.on_duplicate_key_update(
clear_status = clear_status,
clear_song_ct = clear_song_ct,
song1_score = score1,
song2_score = score2,
song3_score = score3,
play_ct = stageup.c.play_ct + 1
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_stageup: failed to update! user_id: {user_id} version: {version} stage_id: {stage_id}")
return None
return result.lastrowid
def get_stageup(self, user_id: int, version: int) -> Optional[List[Row]]:
sql = select(stageup).where(and_(stageup.c.user==user_id, stageup.c.version==version))
result = self.execute(sql)
if result is None: return None
return result.fetchall()
def get_stageup_stage(self, user_id: int, version: int, stage_id: int) -> Optional[Row]:
sql = select(stageup).where(
and_(
stageup.c.user == user_id,
stageup.c.version == version,
stageup.c.stage_id == stage_id,
)
)
result = self.execute(sql)
if result is None: return None
return result.fetchone()

View File

@ -0,0 +1,68 @@
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, Boolean, Float
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from typing import Optional, List, Dict, Any
from core.data.schema import BaseData, metadata
from core.data import cached
music = Table(
"wacca_static_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("songId", Integer),
Column("chartId", Integer),
Column("title", String(255)),
Column("artist", String(255)),
Column("bpm", String(255)),
Column("difficulty", Float),
Column("chartDesigner", String(255)),
Column("jacketFile", String(255)),
UniqueConstraint("version", "songId", "chartId", name="wacca_static_music_uk"),
mysql_charset='utf8mb4'
)
class WaccaStaticData(BaseData):
def put_music(self, version: int, song_id: int, chart_id: int, title: str, artist: str, bpm: str,
difficulty: float, chart_designer: str, jacket: str) -> Optional[int]:
sql = insert(music).values(
version = version,
songId = song_id,
chartId = chart_id,
title = title,
artist = artist,
bpm = bpm,
difficulty = difficulty,
chartDesigner = chart_designer,
jacketFile = jacket
)
conflict = sql.on_duplicate_key_update(
title = title,
artist = artist,
bpm = bpm,
difficulty = difficulty,
chartDesigner = chart_designer,
jacketFile = jacket
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert music {song_id} chart {chart_id}")
return None
return result.lastrowid
def get_music_chart(self, version: int, song_id: int, chart_id: int) -> Optional[List[Row]]:
sql = select(music).where(and_(
music.c.version == version,
music.c.songId == song_id,
music.c.chartId == chart_id
))
result = self.execute(sql)
if result is None: return None
return result.fetchone()