From 3bd03c592edbf22fed8cc3f1f75075bbb0649878 Mon Sep 17 00:00:00 2001 From: Midorica Date: Thu, 1 Jun 2023 13:19:48 -0400 Subject: [PATCH] Item progression and synthesize of hero and equipment done --- docs/game_specific_info.md | 6 ++ titles/sao/base.py | 107 +++++++++++++++++++++-- titles/sao/handlers/base.py | 166 +++++++++++++++++++++++++++++++++++- titles/sao/schema/item.py | 63 ++++++++++++++ 4 files changed, 334 insertions(+), 8 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index aa0a39f..15efe7c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -436,6 +436,12 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core python dbutils.py --game SDEW upgrade ``` +### Notes +- Stages are currently force unlocked, no progression +- Co-Op (matching) is not supported +- Shop is not functionnal +- Player title is currently static and cannot be changed in-game + ### Credits for SAO support: - Midorica - Limited Network Support diff --git a/titles/sao/base.py b/titles/sao/base.py index ac1b69c..982e256 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -165,9 +165,21 @@ class SaoBase: def handle_c604(self, request: Any) -> bytes: #have_object/get_item_user_data_list - itemIdsData = self.game_data.static.get_item_ids(0, True) - - resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, itemIdsData) + #itemIdsData = self.game_data.static.get_item_ids(0, True) + req = bytes.fromhex(request)[24:] + req_struct = Struct( + Padding(16), + "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 + + ) + req_data = req_struct.parse(req) + user_id = req_data.user_id + + item_data = self.game_data.item.get_user_items(user_id) + + resp = SaoGetItemUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, item_data) + #resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() def handle_c606(self, request: Any) -> bytes: @@ -244,7 +256,89 @@ class SaoBase: resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - def handle_c816(self, request: Any) -> bytes: # not fully done yet + def handle_c814(self, request: Any) -> bytes: + #custom/synthesize_enhancement_hero_log + 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 + "origin_user_hero_log_id_size" / Rebuild(Int32ub, len_(this.origin_user_hero_log_id) * 2), # calculates the length of the origin_user_hero_log_id + "origin_user_hero_log_id" / PaddedString(this.origin_user_hero_log_id_size, "utf_16_le"), # origin_user_hero_log_id is a (zero) padded string + Padding(3), + "material_common_reward_user_data_list_length" / Rebuild(Int8ub, len_(this.material_common_reward_user_data_list)), # material_common_reward_user_data_list is a byte, + "material_common_reward_user_data_list" / Array(this.material_common_reward_user_data_list_length, Struct( + "common_reward_type" / Int16ub, # team_no is a byte + "user_common_reward_id_size" / Rebuild(Int32ub, len_(this.user_common_reward_id) * 2), # calculates the length of the user_common_reward_id + "user_common_reward_id" / PaddedString(this.user_common_reward_id_size, "utf_16_le"), # user_common_reward_id is a (zero) padded string + )), + ) + + req_data = req_struct.parse(req) + user_id = req_data.user_id + synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + + for i in range(0,req_data.material_common_reward_user_data_list_length): + + itemList = self.game_data.static.get_item_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + heroList = self.game_data.static.get_hero_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + equipmentList = self.game_data.static.get_equipment_id(req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if itemList: + hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if equipmentList: + equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + if heroList: + hero_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) + self.game_data.item.remove_hero_log(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) + + self.game_data.item.put_hero_log( + user_id, + int(req_data.origin_user_hero_log_id), + synthesize_hero_log_data["log_level"], + hero_exp, + synthesize_hero_log_data["main_weapon"], + synthesize_hero_log_data["sub_equipment"], + synthesize_hero_log_data["skill_slot1_skill_id"], + synthesize_hero_log_data["skill_slot2_skill_id"], + synthesize_hero_log_data["skill_slot3_skill_id"], + synthesize_hero_log_data["skill_slot4_skill_id"], + synthesize_hero_log_data["skill_slot5_skill_id"] + ) + + profile = self.game_data.profile.get_profile(req_data.user_id) + new_col = int(profile["own_col"]) - 100 + + # Update profile + + self.game_data.profile.put_profile( + req_data.user_id, + profile["user_type"], + profile["nick_name"], + profile["rank_num"], + profile["rank_exp"], + new_col, + profile["own_vp"], + profile["own_yui_medal"], + profile["setting_title_id"] + ) + + # Load the item again to push to the response handler + synthesize_hero_log_data = self.game_data.item.get_hero_log(req_data.user_id, req_data.origin_user_hero_log_id) + + resp = SaoSynthesizeEnhancementHeroLogResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, synthesize_hero_log_data) + return resp.make() + + def handle_c816(self, request: Any) -> bytes: #custom/synthesize_enhancement_equipment req = bytes.fromhex(request)[24:] @@ -278,7 +372,7 @@ class SaoBase: if itemList: equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) - # Then delete the used item, function for items progression is not done yet... + self.game_data.item.remove_item(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) if equipmentList: equipment_data = self.game_data.item.get_user_equipment(req_data.user_id, req_data.material_common_reward_user_data_list[i].user_common_reward_id) @@ -595,10 +689,13 @@ class SaoBase: heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) equipmentList = self.game_data.static.get_equipment_id(randomized_unanalyzed_id['CommonRewardId']) + itemList = self.game_data.static.get_item_id(randomized_unanalyzed_id['CommonRewardId']) if heroList: self.game_data.item.put_hero_log(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) if equipmentList: self.game_data.item.put_equipment_data(req_data.user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) + if itemList: + self.game_data.item.put_item(req_data.user_id, randomized_unanalyzed_id['CommonRewardId']) # Send response diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 4949143..91b30fe 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -816,13 +816,18 @@ class SaoGetItemUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetItemUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, itemIdsData) -> None: + def __init__(self, cmd, item_data) -> None: super().__init__(cmd) self.result = 1 + self.user_item_id = [] + + for i in range(len(item_data)): + self.user_item_id.append(item_data[i][2]) + # item_user_data_list - self.user_item_id = list(map(str,itemIdsData)) #str - self.item_id = itemIdsData #int + self.user_item_id = list(map(str,self.user_item_id)) #str + self.item_id = list(map(int,self.user_item_id)) #int self.protect_flag = 0 #byte self.get_date = "20230101120000" #str @@ -1776,6 +1781,161 @@ class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data +class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): + def __init__(self, cmd, hero_data) -> None: + super().__init__(cmd) + self.result = 1 + + # Calculate level based off experience and the CSV list + with open(r'titles/sao/data/HeroLogLevel.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) + + exp = hero_data[4] + + for e in range(0,len(data)): + if exp>=int(data[e][1]) and exp bytes: + #new stuff + + hero_log_user_data_list_struct = Struct( + "user_hero_log_id_size" / Int32ub, # big endian + "user_hero_log_id" / Int16ul[9], #string + "hero_log_id" / Int32ub, #int + "log_level" / Int16ub, #short + "max_log_level_extended_num" / Int16ub, #short + "log_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 + "last_set_skill_slot1_skill_id" / Int16ub, #short + "last_set_skill_slot2_skill_id" / Int16ub, #short + "last_set_skill_slot3_skill_id" / Int16ub, #short + "last_set_skill_slot4_skill_id" / Int16ub, #short + "last_set_skill_slot5_skill_id" / Int16ub, #short + "property1_property_id" / Int32ub, + "property1_value1" / Int32ub, + "property1_value2" / Int32ub, + "property2_property_id" / Int32ub, + "property2_value1" / Int32ub, + "property2_value2" / Int32ub, + "property3_property_id" / Int32ub, + "property3_value1" / Int32ub, + "property3_value2" / Int32ub, + "property4_property_id" / Int32ub, + "property4_value1" / Int32ub, + "property4_value2" / Int32ub, + "converted_card_num" / Int16ub, + "shop_purchase_flag" / Int8ul, # result is either 0 or 1 + "protect_flag" / Int8ul, # result is either 0 or 1 + "get_date_size" / Int32ub, # big endian + "get_date" / Int16ul[len(self.get_date)], + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian + "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + hero_log_user_data_list_size=0, + hero_log_user_data_list=[], + ))) + + hero_data = dict( + user_hero_log_id_size=len(self.user_hero_log_id) * 2, + user_hero_log_id=[ord(x) for x in self.user_hero_log_id], + hero_log_id=self.hero_log_id, + log_level=self.log_level, + max_log_level_extended_num=self.max_log_level_extended_num, + log_exp=self.log_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + skill_slot_correction_value=self.skill_slot_correction_value, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, + property1_property_id=self.property1_property_id, + property1_value1=self.property1_value1, + property1_value2=self.property1_value2, + property2_property_id=self.property2_property_id, + property2_value1=self.property2_value1, + property2_value2=self.property2_value2, + property3_property_id=self.property3_property_id, + property3_value1=self.property3_value1, + property3_value2=self.property3_value2, + property4_property_id=self.property4_property_id, + property4_value1=self.property4_value1, + property4_value2=self.property4_value2, + converted_card_num=self.converted_card_num, + shop_purchase_flag=self.shop_purchase_flag, + protect_flag=self.protect_flag, + get_date_size=len(self.get_date) * 2, + get_date=[ord(x) for x in self.get_date], + + ) + + resp_data.hero_log_user_data_list.append(hero_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + class SaoSynthesizeEnhancementEquipment(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 0e3d86c..858267e 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -28,6 +28,21 @@ equipment_data = Table( mysql_charset="utf8mb4", ) +item_data = Table( + "sao_item_data", + 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("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "item_id", name="sao_item_data_uk"), + mysql_charset="utf8mb4", +) + hero_log_data = Table( "sao_hero_log_data", metadata, @@ -104,6 +119,25 @@ class SaoItemData(BaseData): self.logger.error(f"Failed to create SAO session for user {user_id}!") return None return result.lastrowid + + def put_item(self, user_id: int, item_id: int) -> Optional[int]: + sql = insert(item_data).values( + user=user_id, + item_id=item_id, + ) + + conflict = sql.on_duplicate_key_update( + item_id=item_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}" + ) + return None + + return result.lastrowid def put_equipment_data(self, user_id: int, equipment_id: int, enhancement_value: int, enhancement_exp: int, awakening_exp: int, awakening_stage: int, possible_awakening_flag: int) -> Optional[int]: sql = insert(equipment_data).values( @@ -218,6 +252,23 @@ class SaoItemData(BaseData): return None return result.fetchall() + def get_user_items( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all items lookup given a profile + """ + sql = item_data.select( + and_( + item_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_hero_log( self, user_id: int, user_hero_log_id: int = None ) -> Optional[List[Row]]: @@ -309,4 +360,16 @@ class SaoItemData(BaseData): self.logger.error( f"{__name__} failed to remove equipment! profile: {user_id}, equipment_id: {equipment_id}" ) + return None + + def remove_item(self, user_id: int, item_id: int) -> None: + sql = item_data.delete( + and_(item_data.c.user == user_id, item_data.c.item_id == item_id) + ) + + result = self.execute(sql) + if result is None: + self.logger.error( + f"{__name__} failed to remove item! profile: {user_id}, item_id: {item_id}" + ) return None \ No newline at end of file