diff --git a/titles/sao/base.py b/titles/sao/base.py index 1e1b02b..685d330 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -73,7 +73,12 @@ class SaoBase: user_id = -1 self.logger.error("Failed to register card!") + # Create profile with 3 basic heroes profile_id = self.game_data.profile.create_profile(user_id) + self.game_data.item.put_hero_log(user_id, 101000010, 1, 0, 101000016, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 102000010, 1, 0, 103000006, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_log(user_id, 103000010, 1, 0, 112000009, 0, 30086, 1001, 1002, 1003, 1005) + self.game_data.item.put_hero_party(user_id, 0, 101000010, 102000010, 103000010) self.logger.info(f"User Authenticated: { access_code } | { user_id }") @@ -121,9 +126,19 @@ class SaoBase: def handle_c600(self, request: Any) -> bytes: #have_object/get_hero_log_user_data_list - heroIdsData = self.game_data.static.get_hero_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 + + hero_data = self.game_data.item.get_hero_logs(user_id) - resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, heroIdsData) + resp = SaoGetHeroLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero_data) return resp.make() def handle_c602(self, request: Any) -> bytes: @@ -164,7 +179,23 @@ class SaoBase: def handle_c804(self, request: Any) -> bytes: #custom/get_party_data_list - resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + + 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 + + hero_party = self.game_data.item.get_hero_party(user_id, 0) + hero1_data = self.game_data.item.get_hero_log(user_id, hero_party[3]) + hero2_data = self.game_data.item.get_hero_log(user_id, hero_party[4]) + hero3_data = self.game_data.item.get_hero_log(user_id, hero_party[5]) + + resp = SaoGetPartyDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, hero1_data, hero2_data, hero3_data) return resp.make() def handle_c902(self, request: Any) -> bytes: # for whatever reason, having all entries empty or filled changes nothing @@ -197,7 +228,7 @@ class SaoBase: #home/check_profile_card_used_reward resp = SaoCheckProfileCardUsedRewardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() - + def handle_c806(self, request: Any) -> bytes: #custom/change_party req = bytes.fromhex(request)[24:] @@ -228,18 +259,40 @@ class SaoBase: "sub_equipment_user_equipment_id_size" / Rebuild(Int32ub, len_(this.sub_equipment_user_equipment_id) * 2), # calculates the length of the sub_equipment_user_equipment_id "sub_equipment_user_equipment_id" / PaddedString(this.sub_equipment_user_equipment_id_size, "utf_16_le"), # sub_equipment_user_equipment_id is a (zero) padded string "skill_slot1_skill_id" / Int32ub, # skill_slot1_skill_id is a int, - "skill_slot2_skill_id" / Int32ub, # skill_slot2_skill_id is a int, - "skill_slot3_skill_id" / Int32ub, # skill_slot3_skill_id is a int, - "skill_slot4_skill_id" / Int32ub, # skill_slot4_skill_id is a int, - "skill_slot5_skill_id" / Int32ub, # skill_slot5_skill_id is a int, + "skill_slot2_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot3_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot4_skill_id" / Int32ub, # skill_slot1_skill_id is a int, + "skill_slot5_skill_id" / Int32ub, # skill_slot1_skill_id is a int, )), )), ) req_data = req_struct.parse(req) + user_id = req_data.user_id - #self.logger.info(f"User Team Data: { req_data }") + for party_team in req_data.party_data_list[0].party_team_data_list: + hero_data = self.game_data.item.get_hero_log(user_id, party_team["user_hero_log_id"]) + hero_level = 1 + hero_exp = 0 + + if hero_data: + hero_level = hero_data["log_level"] + hero_exp = hero_data["log_exp"] + + self.game_data.item.put_hero_log( + user_id, + party_team["user_hero_log_id"], + hero_level, + hero_exp, + party_team["main_weapon_user_equipment_id"], + party_team["sub_equipment_user_equipment_id"], + party_team["skill_slot1_skill_id"], + party_team["skill_slot2_skill_id"], + party_team["skill_slot3_skill_id"], + party_team["skill_slot4_skill_id"], + party_team["skill_slot5_skill_id"] + ) resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -371,12 +424,6 @@ class SaoBase: req_data = req_struct.parse(req) - #self.logger.info(f"User Get Col Data: { req_data.get_col }") - #self.logger.info(f"User Hero Log Exp Data: { req_data.get_hero_log_exp }") - #self.logger.info(f"User Score Data: { req_data.score_data[0] }") - #self.logger.info(f"User Discovery Enemy Data: { req_data.discovery_enemy_data_list }") - #self.logger.info(f"User Mission Data: { req_data.mission_data_list }") - resp = SaoEpisodePlayEndResponse(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 9048517..3bd6d9c 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -478,28 +478,49 @@ class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, heroIdsData) -> None: + def __init__(self, cmd, hero_data) -> None: super().__init__(cmd) self.result = 1 - #print(heroIdsData) - #print(list(map(str,heroIdsData))) + self.user_hero_log_id = [] + self.log_level = [] + self.max_log_level_extended_num = [] + self.log_exp = [] + self.last_set_skill_slot1_skill_id = [] + self.last_set_skill_slot2_skill_id = [] + self.last_set_skill_slot3_skill_id = [] + self.last_set_skill_slot4_skill_id = [] + self.last_set_skill_slot5_skill_id = [] + + for i in range(len(hero_data)): + self.user_hero_log_id.append(hero_data[i][2]) + self.log_level.append(hero_data[i][3]) + self.max_log_level_extended_num.append(hero_data[i][3]) + self.log_exp.append(hero_data[i][4]) + self.last_set_skill_slot1_skill_id.append(hero_data[i][7]) + self.last_set_skill_slot2_skill_id.append(hero_data[i][8]) + self.last_set_skill_slot3_skill_id.append(hero_data[i][9]) + self.last_set_skill_slot4_skill_id.append(hero_data[i][10]) + self.last_set_skill_slot5_skill_id.append(hero_data[i][11]) + + #print(self.user_hero_log_id) + #print(list(map(str,self.user_hero_log_id))) # hero_log_user_data_list - self.user_hero_log_id = list(map(str,heroIdsData)) #str - self.hero_log_id = heroIdsData #int - self.log_level = 10 #short - self.max_log_level_extended_num = 10 #short - self.log_exp = 1000 #int + self.user_hero_log_id = list(map(str,self.user_hero_log_id)) #str + self.hero_log_id = list(map(int,self.user_hero_log_id)) #int + self.log_level = list(map(int,self.log_level)) #short + self.max_log_level_extended_num = list(map(int,self.log_level)) #short + self.log_exp = list(map(int,self.log_level)) #int self.possible_awakening_flag = 0 #byte self.awakening_stage = 0 #short self.awakening_exp = 0 #int self.skill_slot_correction_value = 0 #byte - self.last_set_skill_slot1_skill_id = 0 #short - self.last_set_skill_slot2_skill_id = 0 #short - self.last_set_skill_slot3_skill_id = 0 #short - self.last_set_skill_slot4_skill_id = 0 #short - self.last_set_skill_slot5_skill_id = 0 #short + self.last_set_skill_slot1_skill_id = list(map(int,self.last_set_skill_slot1_skill_id)) #short + self.last_set_skill_slot2_skill_id = list(map(int,self.last_set_skill_slot2_skill_id)) #short + self.last_set_skill_slot3_skill_id = list(map(int,self.last_set_skill_slot3_skill_id)) #short + self.last_set_skill_slot4_skill_id = list(map(int,self.last_set_skill_slot4_skill_id)) #short + self.last_set_skill_slot5_skill_id = list(map(int,self.last_set_skill_slot5_skill_id)) #short self.property1_property_id = 0 #int self.property1_value1 = 0 #int self.property1_value2 = 0 #int @@ -573,18 +594,18 @@ class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): user_hero_log_id_size=len(self.user_hero_log_id[i]) * 2, user_hero_log_id=[ord(x) for x in self.user_hero_log_id[i]], hero_log_id=self.hero_log_id[i], - log_level=self.log_level, - max_log_level_extended_num=self.max_log_level_extended_num, - log_exp=self.log_exp, + log_level=self.log_level[i], + max_log_level_extended_num=self.max_log_level_extended_num[i], + log_exp=self.log_exp[i], 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, + last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id[i], + last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id[i], + last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id[i], + last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id[i], + last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id[i], property1_property_id=self.property1_property_id, property1_value1=self.property1_value1, property1_value2=self.property1_value2, @@ -926,10 +947,10 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): def make(self) -> bytes: episode_data_struct = Struct( - "user_episode_append_id_size" / Int32ub, # big endian - "user_episode_append_id" / Int16ul[5], #forced to match the user_episode_append_id_list index which is always 5 chars for the episode ids - "user_id_size" / Int32ub, # big endian - "user_id" / Int16ul[6], # has to be exactly 6 chars in the user field... MANDATORY + "user_episode_append_id_size" / Rebuild(Int32ub, len_(this.user_episode_append_id) * 2), # calculates the length of the user_episode_append_id + "user_episode_append_id" / PaddedString(this.user_episode_append_id_size, "utf_16_le"), # user_episode_append_id is a (zero) padded string + "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 "episode_append_id" / Int32ub, "own_num" / Int32ub, ) @@ -955,10 +976,8 @@ class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): for i in range(len(self.user_id_list)): # add the episode_data_struct to the resp_struct.episode_append_data_list resp_data.episode_append_data_list.append(dict( - user_episode_append_id_size=len(self.user_episode_append_id_list[i]) * 2, - user_episode_append_id=[ord(x) for x in self.user_episode_append_id_list[i]], - user_id_size=len(self.user_id_list[i]) * 2, - user_id=[ord(x) for x in self.user_id_list[i]], + user_episode_append_id=self.user_episode_append_id_list[i], + user_id=self.user_id_list[i], episode_append_id=self.episode_append_id_list[i], own_num=self.own_num_list[i], )) @@ -974,8 +993,9 @@ class SaoGetPartyDataListRequest(SaoBaseRequest): super().__init__(data) class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party - def __init__(self, cmd) -> None: + def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: super().__init__(cmd) + self.result = 1 self.party_data_list_size = 1 # Number of arrays @@ -985,36 +1005,36 @@ class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party self.user_party_team_id_1 = "0" self.arrangement_num_1 = 0 - self.user_hero_log_id_1 = "101000010" - self.main_weapon_user_equipment_id_1 = "101000016" - self.sub_equipment_user_equipment_id_1 = "0" - self.skill_slot1_skill_id_1 = 30086 - self.skill_slot2_skill_id_1 = 1001 - self.skill_slot3_skill_id_1 = 1002 - self.skill_slot4_skill_id_1 = 1003 - self.skill_slot5_skill_id_1 = 1005 + self.user_hero_log_id_1 = str(hero1_data[2]) + self.main_weapon_user_equipment_id_1 = str(hero1_data[5]) + self.sub_equipment_user_equipment_id_1 = str(hero1_data[6]) + self.skill_slot1_skill_id_1 = hero1_data[7] + self.skill_slot2_skill_id_1 = hero1_data[8] + self.skill_slot3_skill_id_1 = hero1_data[9] + self.skill_slot4_skill_id_1 = hero1_data[10] + self.skill_slot5_skill_id_1 = hero1_data[11] self.user_party_team_id_2 = "0" self.arrangement_num_2 = 0 - self.user_hero_log_id_2 = "102000010" - self.main_weapon_user_equipment_id_2 = "103000006" - self.sub_equipment_user_equipment_id_2 = "0" - self.skill_slot1_skill_id_2 = 30086 - self.skill_slot2_skill_id_2 = 1001 - self.skill_slot3_skill_id_2 = 1002 - self.skill_slot4_skill_id_2 = 1003 - self.skill_slot5_skill_id_2 = 1005 + self.user_hero_log_id_2 = str(hero2_data[2]) + self.main_weapon_user_equipment_id_2 = str(hero2_data[5]) + self.sub_equipment_user_equipment_id_2 = str(hero2_data[6]) + self.skill_slot1_skill_id_2 = hero2_data[7] + self.skill_slot2_skill_id_2 = hero2_data[8] + self.skill_slot3_skill_id_2 = hero2_data[9] + self.skill_slot4_skill_id_2 = hero2_data[10] + self.skill_slot5_skill_id_2 = hero2_data[11] self.user_party_team_id_3 = "0" self.arrangement_num_3 = 0 - self.user_hero_log_id_3 = "103000010" - self.main_weapon_user_equipment_id_3 = "112000009" - self.sub_equipment_user_equipment_id_3 = "0" - self.skill_slot1_skill_id_3 = 30086 - self.skill_slot2_skill_id_3 = 1001 - self.skill_slot3_skill_id_3 = 1002 - self.skill_slot4_skill_id_3 = 1003 - self.skill_slot5_skill_id_3 = 1005 + self.user_hero_log_id_3 = str(hero3_data[2]) + self.main_weapon_user_equipment_id_3 = str(hero3_data[5]) + self.sub_equipment_user_equipment_id_3 = str(hero3_data[6]) + self.skill_slot1_skill_id_3 = hero3_data[7] + self.skill_slot2_skill_id_3 = hero3_data[8] + self.skill_slot3_skill_id_3 = hero3_data[9] + self.skill_slot4_skill_id_3 = hero3_data[10] + self.skill_slot5_skill_id_3 = hero3_data[11] def make(self) -> bytes: # create a resp struct diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py index b4fede2..3e75fc0 100644 --- a/titles/sao/schema/__init__.py +++ b/titles/sao/schema/__init__.py @@ -1,2 +1,3 @@ from .profile import SaoProfileData -from .static import SaoStaticData \ No newline at end of file +from .static import SaoStaticData +from .item import SaoItemData \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py new file mode 100644 index 0000000..a0d5123 --- /dev/null +++ b/titles/sao/schema/item.py @@ -0,0 +1,161 @@ +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 + +hero_log_data = Table( + "sao_hero_log_data", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_hero_log_id", Integer, nullable=False), + Column("log_level", Integer, nullable=False), + Column("log_exp", Integer, nullable=False), + Column("main_weapon", Integer, nullable=False), + Column("sub_equipment", Integer, nullable=False), + Column("skill_slot1_skill_id", Integer, nullable=False), + Column("skill_slot2_skill_id", Integer, nullable=False), + Column("skill_slot3_skill_id", Integer, nullable=False), + Column("skill_slot4_skill_id", Integer, nullable=False), + Column("skill_slot5_skill_id", Integer, nullable=False), + Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_hero_log_id", name="sao_hero_log_data_uk"), + mysql_charset="utf8mb4", +) + +hero_party = Table( + "sao_hero_party", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("user_party_team_id", Integer, nullable=False), + Column("user_hero_log_id_1", Integer, nullable=False), + Column("user_hero_log_id_2", Integer, nullable=False), + Column("user_hero_log_id_3", Integer, nullable=False), + UniqueConstraint("user", "user_party_team_id", name="sao_hero_party_uk"), + mysql_charset="utf8mb4", +) + +class SaoItemData(BaseData): + def put_hero_log(self, user_id: int, user_hero_log_id: int, log_level: int, log_exp: int, main_weapon: int, sub_equipment: int, skill_slot1_skill_id: int, skill_slot2_skill_id: int, skill_slot3_skill_id: int, skill_slot4_skill_id: int, skill_slot5_skill_id: int) -> Optional[int]: + sql = insert(hero_log_data).values( + user=user_id, + user_hero_log_id=user_hero_log_id, + log_level=log_level, + log_exp=log_exp, + main_weapon=main_weapon, + sub_equipment=sub_equipment, + skill_slot1_skill_id=skill_slot1_skill_id, + skill_slot2_skill_id=skill_slot2_skill_id, + skill_slot3_skill_id=skill_slot3_skill_id, + skill_slot4_skill_id=skill_slot4_skill_id, + skill_slot5_skill_id=skill_slot5_skill_id, + ) + + conflict = sql.on_duplicate_key_update( + log_level=hero_log_data.c.log_level, + log_exp=hero_log_data.c.log_exp, + main_weapon=hero_log_data.c.main_weapon, + sub_equipment=hero_log_data.c.sub_equipment, + skill_slot1_skill_id=hero_log_data.c.skill_slot1_skill_id, + skill_slot2_skill_id=hero_log_data.c.skill_slot2_skill_id, + skill_slot3_skill_id=hero_log_data.c.skill_slot3_skill_id, + skill_slot4_skill_id=hero_log_data.c.skill_slot4_skill_id, + skill_slot5_skill_id=hero_log_data.c.skill_slot5_skill_id, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero! user: {user_id}, user_hero_log_id: {user_hero_log_id}" + ) + return None + + return result.lastrowid + + def put_hero_party(self, user_id: int, user_party_team_id: int, user_hero_log_id_1: int, user_hero_log_id_2: int, user_hero_log_id_3: int) -> Optional[int]: + sql = insert(hero_party).values( + user=user_id, + user_party_team_id=user_party_team_id, + user_hero_log_id_1=user_hero_log_id_1, + user_hero_log_id_2=user_hero_log_id_2, + user_hero_log_id_3=user_hero_log_id_3, + ) + + conflict = sql.on_duplicate_key_update( + user_hero_log_id_1=hero_party.c.user_hero_log_id_1, + user_hero_log_id_2=hero_party.c.user_hero_log_id_2, + user_hero_log_id_3=hero_party.c.user_hero_log_id_3, + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert hero party! user: {user_id}, user_party_team_id: {user_party_team_id}" + ) + return None + + return result.lastrowid + + def get_hero_log( + self, user_id: int, user_hero_log_id: int = None + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + hero_log_data.c.user_hero_log_id == user_hero_log_id if user_hero_log_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_hero_logs( + self, user_id: int + ) -> Optional[List[Row]]: + """ + A catch-all hero lookup given a profile and user_party_team_id and ID specifiers + """ + sql = hero_log_data.select( + and_( + hero_log_data.c.user == user_id, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_hero_party( + self, user_id: int, user_party_team_id: int = None + ) -> Optional[List[Row]]: + sql = hero_party.select( + and_( + hero_party.c.user == user_id, + hero_party.c.user_party_team_id == user_party_team_id if user_party_team_id is not None else True, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file