diff --git a/titles/sao/base.py b/titles/sao/base.py index ad03e97..74aa85a 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -815,6 +815,8 @@ class SaoBase: ) # Grab the rare loot from the table, match it with the right item and then push to the player profile + json_data = {"data": []} + for r in range(0,req_data.get_rare_drop_data_list_length): rewardList = self.game_data.static.get_rare_drop_id(int(req_data.get_rare_drop_data_list[r].quest_rare_drop_id)) commonRewardId = rewardList["commonRewardId"] @@ -850,9 +852,13 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) + + json_data["data"].append(randomized_unanalyzed_id['CommonRewardId']) # Send response + self.game_data.item.create_end_session(user_id, episode_id, quest_clear_flag, json_data["data"]) + resp = SaoEpisodePlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -1117,6 +1123,8 @@ class SaoBase: hero_data["skill_slot4_skill_id"], hero_data["skill_slot5_skill_id"] ) + + json_data = {"data": []} # Grab the rare loot from the table, match it with the right item and then push to the player profile for r in range(0,req_data.get_rare_drop_data_list_length): @@ -1154,9 +1162,13 @@ class SaoBase: self.game_data.item.put_equipment_data(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 200, 0, 0, 0) if itemList: self.game_data.item.put_item(user_id, randomized_unanalyzed_id['CommonRewardId']) + + json_data["data"].append(randomized_unanalyzed_id['CommonRewardId']) # Send response + self.game_data.item.create_end_session(user_id, trial_tower_id, quest_clear_flag, json_data["data"]) + resp = SaoTrialTowerPlayEndResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) return resp.make() @@ -1176,32 +1188,9 @@ class SaoBase: req_data = req_struct.parse(req) user_id = req_data.user_id - with open('titles/sao/data/RewardTable.csv', 'r') as f: - keys_unanalyzed = next(f).strip().split(',') - data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + end_session_data = self.game_data.item.get_end_session(user_id) - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - i = 0 - - # Create a loop to check if the id is a hero or else try 15 times before closing the loop and sending a dummy hero - while not heroList: - if i == 15: - # Return the dummy hero but not save it - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, 102000070) - return resp.make() - - i += 1 - randomized_unanalyzed_id = choice(data_unanalyzed) - heroList = self.game_data.static.get_hero_id(randomized_unanalyzed_id['CommonRewardId']) - - hero_data = self.game_data.item.get_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId']) - - # Avoid having a duplicated card and cause an overwrite - if not hero_data: - self.game_data.item.put_hero_log(user_id, randomized_unanalyzed_id['CommonRewardId'], 1, 0, 101000016, 0, 30086, 1001, 1002, 0, 0) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, randomized_unanalyzed_id['CommonRewardId']) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, end_session_data[4]) return resp.make() def handle_c91a(self, request: Any) -> bytes: # handler is identical to the episode diff --git a/titles/sao/data/RewardTable.csv b/titles/sao/data/RewardTable.csv index acce5bd..929ff82 100644 Binary files a/titles/sao/data/RewardTable.csv and b/titles/sao/data/RewardTable.csv differ diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index f5c114f..93d0589 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -3,6 +3,7 @@ from datetime import datetime from construct import * import sys import csv +from csv import * class SaoBaseRequest: def __init__(self, data: bytes) -> None: @@ -1661,43 +1662,77 @@ class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): super().__init__(data) class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): - def __init__(self, cmd, randomized_unanalyzed_id) -> None: + def __init__(self, cmd, end_session_data) -> None: super().__init__(cmd) self.result = 1 - self.play_end_unanalyzed_log_reward_data_list_size = 1 # Number of arrays - self.unanalyzed_log_grade_id = 3 # RewardTable.csv - self.common_reward_data_size = 1 + self.unanalyzed_log_grade_id = [] - self.common_reward_type_1 = 1 - self.common_reward_id_1 = int(randomized_unanalyzed_id) - self.common_reward_num_1 = 1 + self.common_reward_type = [] + self.common_reward_id = [] + self.common_reward_num = 1 + + for x in range(len(end_session_data)): + self.common_reward_id.append(end_session_data[x]) + + with open('titles/sao/data/RewardTable.csv', 'r') as f: + keys_unanalyzed = next(f).strip().split(',') + data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) + + for i in range(len(data_unanalyzed)): + if int(data_unanalyzed[i]["CommonRewardId"]) == int(end_session_data[x]): + self.unanalyzed_log_grade_id.append(int(data_unanalyzed[i]["UnanalyzedLogGradeId"])) + self.common_reward_type.append(int(data_unanalyzed[i]["CommonRewardType"])) + break + + self.unanalyzed_log_grade_id = list(map(int,self.unanalyzed_log_grade_id)) #int + self.common_reward_type = list(map(int,self.common_reward_type)) #int + self.common_reward_id = list(map(int,self.common_reward_id)) #int def make(self) -> bytes: + #new stuff + common_reward_data_struct = Struct( + "common_reward_type" / Int16ub, + "common_reward_id" / Int32ub, + "common_reward_num" / Int32ub, + ) + + play_end_unanalyzed_log_reward_data_list_struct = Struct( + "unanalyzed_log_grade_id" / Int32ub, + "common_reward_data_size" / Rebuild(Int32ub, len_(this.common_reward_data)), # big endian + "common_reward_data" / Array(this.common_reward_data_size, common_reward_data_struct), + ) + # create a resp struct resp_struct = Struct( "result" / Int8ul, # result is either 0 or 1 - "play_end_unanalyzed_log_reward_data_list_size" / Int32ub, # big endian - - "unanalyzed_log_grade_id" / Int32ub, - "common_reward_data_size" / Int32ub, - - "common_reward_type_1" / Int16ub, - "common_reward_id_1" / Int32ub, - "common_reward_num_1" / Int32ub, + "play_end_unanalyzed_log_reward_data_list_size" / Rebuild(Int32ub, len_(this.play_end_unanalyzed_log_reward_data_list)), # big endian + "play_end_unanalyzed_log_reward_data_list" / Array(this.play_end_unanalyzed_log_reward_data_list_size, play_end_unanalyzed_log_reward_data_list_struct), ) - resp_data = resp_struct.build(dict( + resp_data = resp_struct.parse(resp_struct.build(dict( result=self.result, - play_end_unanalyzed_log_reward_data_list_size=self.play_end_unanalyzed_log_reward_data_list_size, - - unanalyzed_log_grade_id=self.unanalyzed_log_grade_id, - common_reward_data_size=self.common_reward_data_size, + play_end_unanalyzed_log_reward_data_list_size=0, + play_end_unanalyzed_log_reward_data_list=[], + ))) - common_reward_type_1=self.common_reward_type_1, - common_reward_id_1=self.common_reward_id_1, - common_reward_num_1=self.common_reward_num_1, - )) + for i in range(len(self.common_reward_id)): + reward_resp_data = dict( + unanalyzed_log_grade_id=self.unanalyzed_log_grade_id[i], + common_reward_data_size=0, + common_reward_data=[], + ) + + reward_resp_data["common_reward_data"].append(dict( + common_reward_type=self.common_reward_type[i], + common_reward_id=self.common_reward_id[i], + common_reward_num=self.common_reward_num, + )) + + resp_data.play_end_unanalyzed_log_reward_data_list.append(reward_resp_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) self.length = len(resp_data) return super().make() + resp_data @@ -1733,8 +1768,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): self.concurrent_destroying_num.append(quest_data[i][7]) # quest_scene_ex_bonus_user_data_list - self.achievement_flag = [[1, 1, 1],[1, 1, 1]] - self.ex_bonus_table_id = [[1, 2, 3],[4, 5, 6]] + self.achievement_flag = [1,1,1] + self.ex_bonus_table_id = [1,2,3] self.quest_type = list(map(int,self.quest_type)) #int @@ -1748,8 +1783,8 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): def make(self) -> bytes: #new stuff quest_scene_ex_bonus_user_data_list_struct = Struct( - "achievement_flag" / Int32ub, # big endian "ex_bonus_table_id" / Int32ub, # big endian + "achievement_flag" / Int8ul, # result is either 0 or 1 ) quest_scene_best_score_user_data_struct = Struct( @@ -1802,7 +1837,6 @@ class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): total_damage=[ord(x) for x in self.total_damage[i]], concurrent_destroying_num=self.concurrent_destroying_num[i], )) - resp_data.quest_scene_user_data_list.append(quest_resp_data) @@ -2462,7 +2496,7 @@ class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): self.length = len(resp_data) return super().make() + resp_data - + class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) diff --git a/titles/sao/index.py b/titles/sao/index.py index 69a3db5..1fef504 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -112,5 +112,4 @@ class SaoServlet(resource.Resource): self.logger.info(f"Handler {req_url} - {sao_request[:4]} request") self.logger.debug(f"Request: {request.content.getvalue().hex()}") - self.logger.debug(f"Response: {handler(sao_request).hex()}") return handler(sao_request) \ No newline at end of file diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 8b46723..11adf27 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -1,6 +1,6 @@ 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.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select, update, delete from sqlalchemy.engine import Row @@ -122,6 +122,22 @@ sessions = Table( mysql_charset="utf8mb4", ) +end_sessions = Table( + "sao_end_sessions", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("quest_id", Integer, nullable=False), + Column("play_result_flag", Boolean, nullable=False), + Column("reward_data", JSON, nullable=True), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + mysql_charset="utf8mb4", +) + class SaoItemData(BaseData): def create_session(self, user_id: int, user_party_team_id: int, episode_id: int, play_mode: int, quest_drop_boost_apply_flag: int) -> Optional[int]: sql = insert(sessions).values( @@ -140,6 +156,22 @@ class SaoItemData(BaseData): return None return result.lastrowid + def create_end_session(self, user_id: int, quest_id: int, play_result_flag: bool, reward_data: JSON) -> Optional[int]: + sql = insert(end_sessions).values( + user=user_id, + quest_id=quest_id, + play_result_flag=play_result_flag, + reward_data=reward_data, + ) + + conflict = sql.on_duplicate_key_update(user=user_id) + + result = self.execute(conflict) + if result is None: + self.logger.error(f"Failed to create SAO end 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, @@ -418,6 +450,22 @@ class SaoItemData(BaseData): return None return result.fetchone() + def get_end_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = end_sessions.select( + and_( + end_sessions.c.user == user_id, + ) + ).order_by( + end_sessions.c.play_date.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: sql = hero_log_data.delete( and_(