From 84fc002cdbb882a547d17fb4b91c3e48f54332a2 Mon Sep 17 00:00:00 2001 From: Midorica Date: Fri, 2 Jun 2023 13:53:49 -0400 Subject: [PATCH] Adding trial tower support for SAO --- titles/sao/base.py | 186 ++++++++++++++++++++++++++++++++++++ titles/sao/handlers/base.py | 126 ++++++++++++++++++++++++ 2 files changed, 312 insertions(+) diff --git a/titles/sao/base.py b/titles/sao/base.py index 982e256..ef3879a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -711,7 +711,193 @@ class SaoBase: resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) return resp.make() + def handle_c918(self, request: Any) -> bytes: + #quest/trial_tower_play_end + req = bytes.fromhex(request)[24:] + + req_struct = Struct( + Padding(20), + "ticket_id" / Bytes(1), # needs to be parsed as an int + Padding(1), + "user_id_size" / Rebuild(Int32ub, len_(this.user_id) * 2), # calculates the length of the user_id + "user_id" / PaddedString(this.user_id_size, "utf_16_le"), # user_id is a (zero) padded string + Padding(2), + "trial_tower_id" / Int16ub, # trial_tower_id is a short, + Padding(3), + "play_end_request_data" / Int8ub, # play_end_request_data is a byte + Padding(1), + "play_result_flag" / Int8ub, # play_result_flag is a byte + Padding(2), + "base_get_data_length" / Rebuild(Int8ub, len_(this.base_get_data)), # base_get_data_length is a byte, + "base_get_data" / Array(this.base_get_data_length, Struct( + "get_hero_log_exp" / Int32ub, # get_hero_log_exp is an int + "get_col" / Int32ub, # get_num is a short + )), + Padding(3), + "get_player_trace_data_list_length" / Rebuild(Int8ub, len_(this.get_player_trace_data_list)), # get_player_trace_data_list_length is a byte + "get_player_trace_data_list" / Array(this.get_player_trace_data_list_length, Struct( + "user_quest_scene_player_trace_id" / Int32ub, # user_quest_scene_player_trace_id is an int + )), + Padding(3), + "get_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_rare_drop_data_list)), # get_rare_drop_data_list_length is a byte + "get_rare_drop_data_list" / Array(this.get_rare_drop_data_list_length, Struct( + "quest_rare_drop_id" / Int32ub, # quest_rare_drop_id is an int + )), + Padding(3), + "get_special_rare_drop_data_list_length" / Rebuild(Int8ub, len_(this.get_special_rare_drop_data_list)), # get_special_rare_drop_data_list_length is a byte + "get_special_rare_drop_data_list" / Array(this.get_special_rare_drop_data_list_length, Struct( + "quest_special_rare_drop_id" / Int32ub, # quest_special_rare_drop_id is an int + )), + Padding(3), + "get_unanalyzed_log_tmp_reward_data_list_length" / Rebuild(Int8ub, len_(this.get_unanalyzed_log_tmp_reward_data_list)), # get_unanalyzed_log_tmp_reward_data_list_length is a byte + "get_unanalyzed_log_tmp_reward_data_list" / Array(this.get_unanalyzed_log_tmp_reward_data_list_length, Struct( + "unanalyzed_log_grade_id" / Int32ub, # unanalyzed_log_grade_id is an int, + )), + Padding(3), + "get_event_item_data_list_length" / Rebuild(Int8ub, len_(this.get_event_item_data_list)), # get_event_item_data_list_length is a byte, + "get_event_item_data_list" / Array(this.get_event_item_data_list_length, Struct( + "event_item_id" / Int32ub, # event_item_id is an int + "get_num" / Int16ub, # get_num is a short + )), + Padding(3), + "discovery_enemy_data_list_length" / Rebuild(Int8ub, len_(this.discovery_enemy_data_list)), # discovery_enemy_data_list_length is a byte + "discovery_enemy_data_list" / Array(this.discovery_enemy_data_list_length, Struct( + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "destroy_boss_data_list_length" / Rebuild(Int8ub, len_(this.destroy_boss_data_list)), # destroy_boss_data_list_length is a byte + "destroy_boss_data_list" / Array(this.destroy_boss_data_list_length, Struct( + "boss_type" / Int8ub, # boss_type is a byte + "enemy_kind_id" / Int32ub, # enemy_kind_id is an int + "destroy_num" / Int16ub, # destroy_num is a short + )), + Padding(3), + "mission_data_list_length" / Rebuild(Int8ub, len_(this.mission_data_list)), # mission_data_list_length is a byte + "mission_data_list" / Array(this.mission_data_list_length, Struct( + "mission_id" / Int32ub, # enemy_kind_id is an int + "clear_flag" / Int8ub, # boss_type is a byte + "mission_difficulty_id" / Int16ub, # destroy_num is a short + )), + Padding(3), + "score_data_length" / Rebuild(Int8ub, len_(this.score_data)), # score_data_length is a byte + "score_data" / Array(this.score_data_length, Struct( + "clear_time" / Int32ub, # clear_time is an int + "combo_num" / Int32ub, # boss_type is a int + "total_damage_size" / Rebuild(Int32ub, len_(this.total_damage) * 2), # calculates the length of the total_damage + "total_damage" / PaddedString(this.total_damage_size, "utf_16_le"), # total_damage is a (zero) padded string + "concurrent_destroying_num" / Int16ub, # concurrent_destroying_num is a short + "reaching_skill_level" / Int16ub, # reaching_skill_level is a short + "ko_chara_num" / Int8ub, # ko_chara_num is a byte + "acceleration_invocation_num" / Int16ub, # acceleration_invocation_num is a short + "boss_destroying_num" / Int16ub, # boss_destroying_num is a short + "synchro_skill_used_flag" / Int8ub, # synchro_skill_used_flag is a byte + "used_friend_skill_id" / Int32ub, # used_friend_skill_id is an int + "friend_skill_used_flag" / Int8ub, # friend_skill_used_flag is a byte + "continue_cnt" / Int16ub, # continue_cnt is a short + "total_loss_num" / Int16ub, # total_loss_num is a short + )), + + ) + + req_data = req_struct.parse(req) + + # Update the profile + profile = self.game_data.profile.get_profile(req_data.user_id) + + exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason + col = int(profile["own_col"]) + int(req_data.base_get_data[0].get_col) + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/PlayerRank.csv') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + data = [] + rowf = False + for row in csv_reader: + if rowf==False: + rowf=True + else: + data.append(row) + + for i in range(0,len(data)): + if exp>=int(data[i][1]) and exp bytes: #should be tweaked for proper item unlock #quest/episode_play_end_unanalyzed_log_fixed resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() + + def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode + #quest/trial_tower_play_end_unanalyzed_log_fixed + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 91b30fe..a9d336a 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1530,6 +1530,132 @@ class SaoEpisodePlayEndResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoTrialTowerPlayEndRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoTrialTowerPlayEndResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.play_end_response_data_size = 1 # Number of arrays + self.multi_play_end_response_data_size = 1 # Unused on solo play + self.trial_tower_play_end_updated_notification_data_size = 1 # Number of arrays + self.treasure_hunt_play_end_response_data_size = 1 # Number of arrays + + self.dummy_1 = 0 + self.dummy_2 = 0 + self.dummy_3 = 0 + + self.rarity_up_occurrence_flag = 0 + self.adventure_ex_area_occurrences_flag = 0 + self.ex_bonus_data_list_size = 1 # Number of arrays + self.play_end_player_trace_reward_data_list_size = 0 # Number of arrays + + self.ex_bonus_table_id = 0 # ExBonusTable.csv values, dont care for now + self.achievement_status = 1 + + self.common_reward_data_size = 1 # Number of arrays + + self.common_reward_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv + self.common_reward_id = 0 + self.common_reward_num = 0 + + self.store_best_score_clear_time_flag = 0 + self.store_best_score_combo_num_flag = 0 + self.store_best_score_total_damage_flag = 0 + self.store_best_score_concurrent_destroying_num_flag = 0 + self.store_reaching_trial_tower_rank = 0 + + self.get_event_point = 0 + self.total_event_point = 0 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_end_response_data_size" / Int32ub, # big endian + + "rarity_up_occurrence_flag" / Int8ul, # result is either 0 or 1 + "adventure_ex_area_occurrences_flag" / Int8ul, # result is either 0 or 1 + "ex_bonus_data_list_size" / Int32ub, # big endian + "play_end_player_trace_reward_data_list_size" / Int32ub, # big endian + + # ex_bonus_data_list + "ex_bonus_table_id" / Int32ub, + "achievement_status" / Int8ul, # result is either 0 or 1 + + # play_end_player_trace_reward_data_list + "common_reward_data_size" / Int32ub, + + # common_reward_data + "common_reward_type" / Int16ub, # short + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + + "multi_play_end_response_data_size" / Int32ub, # big endian + + # multi_play_end_response_data + "dummy_1" / Int8ul, # result is either 0 or 1 + "dummy_2" / Int8ul, # result is either 0 or 1 + "dummy_3" / Int8ul, # result is either 0 or 1 + + "trial_tower_play_end_updated_notification_data_size" / Int32ub, # big endian + + #trial_tower_play_end_updated_notification_data + "store_best_score_clear_time_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_combo_num_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_total_damage_flag" / Int8ul, # result is either 0 or 1 + "store_best_score_concurrent_destroying_num_flag" / Int8ul, # result is either 0 or 1 + "store_reaching_trial_tower_rank" / Int32ub, + + "treasure_hunt_play_end_response_data_size" / Int32ub, # big endian + + #treasure_hunt_play_end_response_data + "get_event_point" / Int32ub, + "total_event_point" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_end_response_data_size=self.play_end_response_data_size, + + rarity_up_occurrence_flag=self.rarity_up_occurrence_flag, + adventure_ex_area_occurrences_flag=self.adventure_ex_area_occurrences_flag, + ex_bonus_data_list_size=self.ex_bonus_data_list_size, + play_end_player_trace_reward_data_list_size=self.play_end_player_trace_reward_data_list_size, + + ex_bonus_table_id=self.ex_bonus_table_id, + achievement_status=self.achievement_status, + + common_reward_data_size=self.common_reward_data_size, + + common_reward_type=self.common_reward_type, + common_reward_id=self.common_reward_id, + common_reward_num=self.common_reward_num, + + multi_play_end_response_data_size=self.multi_play_end_response_data_size, + + dummy_1=self.dummy_1, + dummy_2=self.dummy_2, + dummy_3=self.dummy_3, + + trial_tower_play_end_updated_notification_data_size=self.trial_tower_play_end_updated_notification_data_size, + store_best_score_clear_time_flag=self.store_best_score_clear_time_flag, + store_best_score_combo_num_flag=self.store_best_score_combo_num_flag, + store_best_score_total_damage_flag=self.store_best_score_total_damage_flag, + store_best_score_concurrent_destroying_num_flag=self.store_best_score_concurrent_destroying_num_flag, + store_reaching_trial_tower_rank=self.store_reaching_trial_tower_rank, + + treasure_hunt_play_end_response_data_size=self.treasure_hunt_play_end_response_data_size, + + get_event_point=self.get_event_point, + total_event_point=self.total_event_point, + )) + + self.length = len(resp_data) + return super().make() + resp_data + class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data)