diff --git a/core/allnet.py b/core/allnet.py index ab435e7..32ae177 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -112,6 +112,8 @@ class AllnetServlet: ) resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.host = f"{self.config.title.hostname}:{self.config.title.port}" + + self.logger.debug(f"Allnet response: {vars(resp)}") return self.dict_to_http_form_string([vars(resp)]) resp.uri, resp.host = self.uri_registry[req.game_id] @@ -204,12 +206,12 @@ class AllnetServlet: else: # TODO: Keychip check if path.exists( - f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" ): resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" if path.exists( - f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" + f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" ): resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" @@ -410,8 +412,8 @@ class AllnetPowerOnResponse3: self.uri = "" self.host = "" self.place_id = "123" - self.name = "" - self.nickname = "" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" self.region0 = "1" self.region_name0 = "W" self.region_name1 = "" @@ -434,8 +436,8 @@ class AllnetPowerOnResponse2: self.uri = "" self.host = "" self.place_id = "123" - self.name = "Test" - self.nickname = "Test123" + self.name = "ARTEMiS" + self.nickname = "ARTEMiS" self.region0 = "1" self.region_name0 = "W" self.region_name1 = "X" diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 98663d1..6a95005 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -79,6 +79,9 @@ class UserData(BaseData): if usr["password"] is None: return False + + if passwd is None or not passwd: + return False return bcrypt.checkpw(passwd, usr["password"].encode()) diff --git a/core/data/schema/versions/SDBT_3_rollback.sql b/core/data/schema/versions/SDBT_3_rollback.sql new file mode 100644 index 0000000..ff78c54 --- /dev/null +++ b/core/data/schema/versions/SDBT_3_rollback.sql @@ -0,0 +1,30 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + DROP COLUMN regionId, + DROP COLUMN machineType; + +ALTER TABLE chuni_static_events + DROP COLUMN startDate; + +ALTER TABLE chuni_profile_data + DROP COLUMN rankUpChallengeResults; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN presetId id INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + ADD PRIMARY KEY(id); +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId) + REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/data/schema/versions/SDBT_4_upgrade.sql b/core/data/schema/versions/SDBT_4_upgrade.sql new file mode 100644 index 0000000..984447e --- /dev/null +++ b/core/data/schema/versions/SDBT_4_upgrade.sql @@ -0,0 +1,29 @@ +SET FOREIGN_KEY_CHECKS = 0; + +ALTER TABLE chuni_score_playlog + ADD COLUMN regionId INT, + ADD COLUMN machineType INT; + +ALTER TABLE chuni_static_events + ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE chuni_profile_data + ADD COLUMN rankUpChallengeResults JSON; + +ALTER TABLE chuni_static_login_bonus + DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1; + +ALTER TABLE chuni_static_login_bonus_preset + CHANGE COLUMN id presetId INT NOT NULL; +ALTER TABLE chuni_static_login_bonus_preset + DROP PRIMARY KEY; +ALTER TABLE chuni_static_login_bonus_preset + DROP INDEX chuni_static_login_bonus_preset_uk; +ALTER TABLE chuni_static_login_bonus_preset + ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version); + +ALTER TABLE chuni_static_login_bonus + ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version) + REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/core/frontend.py b/core/frontend.py index c992e76..9eb30e6 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -182,7 +182,7 @@ class FE_Gate(FE_Base): access_code: str = request.args[b"access_code"][0].decode() username: str = request.args[b"username"][0] email: str = request.args[b"email"][0].decode() - passwd: str = request.args[b"passwd"][0] + passwd: bytes = request.args[b"passwd"][0] uid = self.data.card.get_user_id_from_card(access_code) if uid is None: @@ -197,7 +197,7 @@ class FE_Gate(FE_Base): if result is None: return redirectTo(b"/gate?e=3", request) - if not self.data.user.check_password(uid, passwd.encode()): + if not self.data.user.check_password(uid, passwd): return redirectTo(b"/gate", request) return redirectTo(b"/user", request) @@ -227,9 +227,22 @@ class FE_User(FE_Base): usr_sesh = IUserSession(sesh) if usr_sesh.userId == 0: return redirectTo(b"/gate", request) + + cards = self.data.card.get_user_cards(usr_sesh.userId) + user = self.data.user.get_user(usr_sesh.userId) + card_data = [] + for c in cards: + if c['is_locked']: + status = 'Locked' + elif c['is_banned']: + status = 'Banned' + else: + status = 'Active' + + card_data.append({'access_code': c['access_code'], 'status': status}) return template.render( - title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh) + title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username'] ).encode("utf-16") diff --git a/core/frontend/user/index.jinja b/core/frontend/user/index.jinja index eabdd18..2911e67 100644 --- a/core/frontend/user/index.jinja +++ b/core/frontend/user/index.jinja @@ -1,4 +1,31 @@ {% extends "core/frontend/index.jinja" %} {% block content %} -

testing

+

Management for {{ username }}

+

Cards

+ + + {% endblock content %} \ No newline at end of file diff --git a/core/frontend/widgets/topbar.jinja b/core/frontend/widgets/topbar.jinja index d196361..fb63ebe 100644 --- a/core/frontend/widgets/topbar.jinja +++ b/core/frontend/widgets/topbar.jinja @@ -4,7 +4,7 @@
  {% for game in game_list %} -   +   {% endfor %}
diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index f1b334e..5361ce9 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -9,42 +9,44 @@ using the megaime database. Clean installations always create the latest databas # Table of content - [Supported Games](#supported-games) - - [Chunithm](#chunithm) + - [CHUNITHM](#chunithm) - [crossbeats REV.](#crossbeats-rev) - [maimai DX](#maimai-dx) - [O.N.G.E.K.I.](#o-n-g-e-k-i) - [Card Maker](#card-maker) - [WACCA](#wacca) + - [Sword Art Online Arcade](#sao) # Supported Games Games listed below have been tested and confirmed working. -## Chunithm +## CHUNITHM ### SDBT -| Version ID | Version Name | -|------------|--------------------| -| 0 | Chunithm | -| 1 | Chunithm+ | -| 2 | Chunithm Air | -| 3 | Chunithm Air + | -| 4 | Chunithm Star | -| 5 | Chunithm Star + | -| 6 | Chunithm Amazon | -| 7 | Chunithm Amazon + | -| 8 | Chunithm Crystal | -| 9 | Chunithm Crystal + | -| 10 | Chunithm Paradise | +| Version ID | Version Name | +|------------|-----------------------| +| 0 | CHUNITHM | +| 1 | CHUNITHM PLUS | +| 2 | CHUNITHM AIR | +| 3 | CHUNITHM AIR PLUS | +| 4 | CHUNITHM STAR | +| 5 | CHUNITHM STAR PLUS | +| 6 | CHUNITHM AMAZON | +| 7 | CHUNITHM AMAZON PLUS | +| 8 | CHUNITHM CRYSTAL | +| 9 | CHUNITHM CRYSTAL PLUS | +| 10 | CHUNITHM PARADISE | ### SDHD/SDBT -| Version ID | Version Name | -|------------|-----------------| -| 11 | Chunithm New!! | -| 12 | Chunithm New!!+ | +| Version ID | Version Name | +|------------|---------------------| +| 11 | CHUNITHM NEW!! | +| 12 | CHUNITHM NEW PLUS!! | +| 13 | CHUNITHM SUN | ### Importer @@ -60,13 +62,33 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac ### Database upgrade Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see -which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to +which version is the latest, f.e. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to perform all previous updates as well: ```shell python dbutils.py --game SDBT upgrade ``` +### Online Battle + +**Only matchmaking (with your imaginary friends) is supported! Online Battle does not (yet?) work!** + +The first person to start the Online Battle (now called host) will create a "matching room" with a given `roomId`, after that max 3 other people can join the created room. +Non used slots during the matchmaking will be filled with CPUs after the timer runs out. +As soon as a new member will join the room the timer will jump back to 60 secs again. +Sending those 4 messages to all other users is also working properly. +In order to use the Online Battle every user needs the same ICF, same rom version and same data version! +If a room is full a new room will be created if another user starts an Online Battle. +After a failed Online Battle the room will be deleted. The host is used for the timer countdown, so if the connection failes to the host the timer will stop and could create a "frozen" state. + +#### Information/Problems: + +- Online Battle uses UDP hole punching and opens port 50201? +- `reflectorUri` seems related to that? +- Timer countdown should be handled globally and not by one user +- Game can freeze or can crash if someone (especially the host) leaves the matchmaking + + ## crossbeats REV. ### SDCA @@ -111,9 +133,9 @@ Config file is located in `config/cxb.yaml`. | 1 | maimai DX PLUS | | 2 | maimai DX Splash | | 3 | maimai DX Splash PLUS | -| 4 | maimai DX Universe | -| 5 | maimai DX Universe PLUS | -| 6 | maimai DX Festival | +| 4 | maimai DX UNiVERSE | +| 5 | maimai DX UNiVERSE PLUS | +| 6 | maimai DX FESTiVAL | ### Importer @@ -238,13 +260,13 @@ python dbutils.py --game SDDT upgrade ### Support status * Card Maker 1.34: - * Chunithm New!!: Yes - * maimai DX Universe: Yes + * CHUNITHM NEW!!: Yes + * maimai DX UNiVERSE: Yes * O.N.G.E.K.I. Bright: Yes * Card Maker 1.35: - * Chunithm New!!+: Yes - * maimai DX Universe PLUS: Yes + * CHUNITHM SUN: Yes (NEW PLUS!! up to A032) + * maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031) * O.N.G.E.K.I. Bright Memory: Yes @@ -344,3 +366,47 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core ```shell python dbutils.py --game SDFE upgrade ``` + +## SAO + +### SDEW + +| Version ID | Version Name | +|------------|---------------| +| 0 | SAO | + + +### Importer + +In order to use the importer locate your game installation folder and execute: + +```shell +python read.py --series SDEW --version --binfolder /path/to/game/extractedassets +``` + +The importer for SAO will import all items, heroes, support skills and titles data. + +### Config + +Config file is located in `config/sao.yaml`. + +| Option | Info | +|--------------------|-----------------------------------------------------------------------------| +| `hostname` | Changes the server listening address for Mucha | +| `port` | Changes the listing port | +| `auto_register` | Allows the game to handle the automatic registration of new cards | + + +### Database upgrade + +Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well: + +```shell +python dbutils.py --game SDEW upgrade +``` + +### Credits for SAO support: + +- Midorica - Limited Network Support +- Dniel97 - Helping with network base +- tungnotpunk - Source \ No newline at end of file diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index bbac976..59db51e 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -15,6 +15,9 @@ version: 12: rom: 2.05.00 data: 2.05.00 + 13: + rom: 2.10.00 + data: 2.10.00 crypto: encrypted_only: False \ No newline at end of file diff --git a/example_config/sao.yaml b/example_config/sao.yaml new file mode 100644 index 0000000..7e3ecf2 --- /dev/null +++ b/example_config/sao.yaml @@ -0,0 +1,6 @@ +server: + hostname: "localhost" + enable: True + loglevel: "info" + port: 9000 + auto_register: True \ No newline at end of file diff --git a/readme.md b/readme.md index ec25191..ee407cd 100644 --- a/readme.md +++ b/readme.md @@ -3,30 +3,31 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar # Supported games Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. -+ Chunithm - + All versions up to New!! Plus -+ Crossbeats Rev ++ CHUNITHM + + All versions up to SUN + ++ crossbeats REV. + All versions + omnimix + maimai DX - + All versions up to Festival + + All versions up to FESTiVAL -+ Hatsune Miku Arcade ++ Hatsune Miku: Project DIVA Arcade + All versions + Card Maker - + 1.34.xx - + 1.35.xx + + 1.34 + + 1.35 -+ Ongeki ++ O.N.G.E.K.I. + All versions up to Bright Memory -+ Wacca ++ WACCA + Lily R + Reverse -+ Pokken ++ POKKÉN TOURNAMENT + Final Online ## Requirements diff --git a/titles/chuni/__init__.py b/titles/chuni/__init__.py index 89cd4f5..0c3cc79 100644 --- a/titles/chuni/__init__.py +++ b/titles/chuni/__init__.py @@ -7,4 +7,4 @@ index = ChuniServlet database = ChuniData reader = ChuniReader game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] -current_schema_version = 3 +current_schema_version = 4 diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 0eaabff..689c2fe 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -44,13 +44,15 @@ class ChuniBase: # check if a user already has some pogress and if not add the # login bonus entry user_login_bonus = self.data.item.get_login_bonus( - user_id, self.version, preset["id"] + user_id, self.version, preset["presetId"] ) if user_login_bonus is None: - self.data.item.put_login_bonus(user_id, self.version, preset["id"]) + self.data.item.put_login_bonus( + user_id, self.version, preset["presetId"] + ) # yeah i'm lazy user_login_bonus = self.data.item.get_login_bonus( - user_id, self.version, preset["id"] + user_id, self.version, preset["presetId"] ) # skip the login bonus entirely if its already finished @@ -66,13 +68,13 @@ class ChuniBase: last_update_date = datetime.now() all_login_boni = self.data.static.get_login_bonus( - self.version, preset["id"] + self.version, preset["presetId"] ) # skip the current bonus preset if no boni were found if all_login_boni is None or len(all_login_boni) < 1: self.logger.warn( - f"No bonus entries found for bonus preset {preset['id']}" + f"No bonus entries found for bonus preset {preset['presetId']}" ) continue @@ -83,14 +85,14 @@ class ChuniBase: if bonus_count > max_needed_days: # assume that all login preset ids under 3000 needs to be # looped, like 30 and 40 are looped, 40 does not work? - if preset["id"] < 3000: + if preset["presetId"] < 3000: bonus_count = 1 else: is_finished = True # grab the item for the corresponding day login_item = self.data.static.get_login_bonus_by_required_days( - self.version, preset["id"], bonus_count + self.version, preset["presetId"], bonus_count ) if login_item is not None: # now add the present to the database so the @@ -108,7 +110,7 @@ class ChuniBase: self.data.item.put_login_bonus( user_id, self.version, - preset["id"], + preset["presetId"], bonusCount=bonus_count, lastUpdateDate=last_update_date, isWatched=False, @@ -156,12 +158,18 @@ class ChuniBase: event_list = [] for evt_row in game_events: - tmp = {} - tmp["id"] = evt_row["eventId"] - tmp["type"] = evt_row["type"] - tmp["startDate"] = "2017-12-05 07:00:00.0" - tmp["endDate"] = "2099-12-31 00:00:00.0" - event_list.append(tmp) + event_list.append( + { + "id": evt_row["eventId"], + "type": evt_row["type"], + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + evt_row["startDate"], "%Y-%m-%d %H:%M:%S" + ), + "endDate": "2099-12-31 00:00:00", + } + ) return { "type": data["type"], @@ -228,29 +236,36 @@ class ChuniBase: def handle_get_user_character_api_request(self, data: Dict) -> Dict: characters = self.data.item.get_characters(data["userId"]) if characters is None: - return {} - next_idx = -1 + return { + "userId": data["userId"], + "length": 0, + "nextIndex": -1, + "userCharacterList": [], + } - characterList = [] - for x in range(int(data["nextIndex"]), len(characters)): + character_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(characters)): tmp = characters[x]._asdict() tmp.pop("user") tmp.pop("id") - characterList.append(tmp) + character_list.append(tmp) - if len(characterList) >= int(data["maxCount"]): + if len(character_list) >= max_ct: break - if len(characterList) >= int(data["maxCount"]) and len(characters) > int( - data["maxCount"] - ) + int(data["nextIndex"]): - next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1 + if len(characters) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = -1 return { "userId": data["userId"], - "length": len(characterList), + "length": len(character_list), "nextIndex": next_idx, - "userCharacterList": characterList, + "userCharacterList": character_list, } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: @@ -292,8 +307,8 @@ class ChuniBase: if len(user_course_list) >= max_ct: break - if len(user_course_list) >= max_ct: - next_idx = next_idx + max_ct + if len(user_course_list) >= next_idx + max_ct: + next_idx += max_ct else: next_idx = -1 @@ -347,12 +362,23 @@ class ChuniBase: } def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_fav_item_list = [] + + # still needs to be implemented on WebUI + # 1: Music, 3: Character + fav_list = self.data.item.get_all_favorites( + data["userId"], self.version, fav_kind=int(data["kind"]) + ) + if fav_list is not None: + for fav in fav_list: + user_fav_item_list.append({"id": fav["favId"]}) + return { "userId": data["userId"], - "length": 0, + "length": len(user_fav_item_list), "kind": data["kind"], "nextIndex": -1, - "userFavoriteItemList": [], + "userFavoriteItemList": user_fav_item_list, } def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: @@ -387,13 +413,13 @@ class ChuniBase: xout = kind * 10000000000 + next_idx + len(items) if len(items) < int(data["maxCount"]): - nextIndex = 0 + next_idx = 0 else: - nextIndex = xout + next_idx = xout return { "userId": data["userId"], - "nextIndex": nextIndex, + "nextIndex": next_idx, "itemKind": kind, "length": len(items), "userItemList": items, @@ -452,6 +478,7 @@ class ChuniBase: "nextIndex": -1, "userMusicList": [], # 240 } + song_list = [] next_idx = int(data["nextIndex"]) max_ct = int(data["maxCount"]) @@ -474,10 +501,10 @@ class ChuniBase: if len(song_list) >= max_ct: break - if len(song_list) >= max_ct: + if len(song_list) >= next_idx + max_ct: next_idx += max_ct else: - next_idx = 0 + next_idx = -1 return { "userId": data["userId"], @@ -623,12 +650,15 @@ class ChuniBase: self.data.profile.put_profile_data( user_id, self.version, upsert["userData"][0] ) + if "userDataEx" in upsert: self.data.profile.put_profile_data_ex( user_id, self.version, upsert["userDataEx"][0] ) + if "userGameOption" in upsert: self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) + if "userGameOptionEx" in upsert: self.data.profile.put_profile_option_ex( user_id, upsert["userGameOptionEx"][0] @@ -672,6 +702,10 @@ class ChuniBase: if "userPlaylogList" in upsert: for playlog in upsert["userPlaylogList"]: + # convert the player names to utf-8 + playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"]) + playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"]) + playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"]) self.data.score.put_playlog(user_id, playlog) if "userTeamPoint" in upsert: diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 6ab3cc3..b3a4cb5 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -17,21 +17,23 @@ class ChuniConstants: VER_CHUNITHM_PARADISE = 10 VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW_PLUS = 12 + VER_CHUNITHM_SUN = 13 VERSION_NAMES = [ - "Chunithm", - "Chunithm+", - "Chunithm Air", - "Chunithm Air+", - "Chunithm Star", - "Chunithm Star+", - "Chunithm Amazon", - "Chunithm Amazon+", - "Chunithm Crystal", - "Chunithm Crystal+", - "Chunithm Paradise", - "Chunithm New!!", - "Chunithm New!!+", + "CHUNITHM", + "CHUNITHM PLUS", + "CHUNITHM AIR", + "CHUNITHM AIR PLUS", + "CHUNITHM STAR", + "CHUNITHM STAR PLUS", + "CHUNITHM AMAZON", + "CHUNITHM AMAZON PLUS", + "CHUNITHM CRYSTAL", + "CHUNITHM CRYSTAL PLUS", + "CHUNITHM PARADISE", + "CHUNITHM NEW!!", + "CHUNITHM NEW PLUS!!", + "CHUNITHM SUN" ] @classmethod diff --git a/titles/chuni/index.py b/titles/chuni/index.py index a7545ba..5d185e9 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -29,6 +29,7 @@ from titles.chuni.crystalplus import ChuniCrystalPlus from titles.chuni.paradise import ChuniParadise from titles.chuni.new import ChuniNew from titles.chuni.newplus import ChuniNewPlus +from titles.chuni.sun import ChuniSun class ChuniServlet: @@ -55,6 +56,7 @@ class ChuniServlet: ChuniParadise, ChuniNew, ChuniNewPlus, + ChuniSun, ] self.logger = logging.getLogger("chuni") @@ -96,15 +98,18 @@ class ChuniServlet: ] for method in method_list: method_fixed = inflection.camelize(method)[6:-7] + # number of iterations was changed to 70 in SUN + iter_count = 70 if version >= ChuniConstants.VER_CHUNITHM_SUN else 44 hash = PBKDF2( method_fixed, bytes.fromhex(keys[2]), 128, - count=44, + count=iter_count, hmac_hash_module=SHA1, ) - self.hash_table[version][hash.hex()] = method_fixed + hashed_name = hash.hex()[:32] # truncate unused bytes like the game does + self.hash_table[version][hashed_name] = method_fixed self.logger.debug( f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" @@ -145,30 +150,32 @@ class ChuniServlet: if version < 105: # 1.0 internal_ver = ChuniConstants.VER_CHUNITHM - elif version >= 105 and version < 110: # Plus + elif version >= 105 and version < 110: # PLUS internal_ver = ChuniConstants.VER_CHUNITHM_PLUS - elif version >= 110 and version < 115: # Air + elif version >= 110 and version < 115: # AIR internal_ver = ChuniConstants.VER_CHUNITHM_AIR - elif version >= 115 and version < 120: # Air Plus + elif version >= 115 and version < 120: # AIR PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS - elif version >= 120 and version < 125: # Star + elif version >= 120 and version < 125: # STAR internal_ver = ChuniConstants.VER_CHUNITHM_STAR - elif version >= 125 and version < 130: # Star Plus + elif version >= 125 and version < 130: # STAR PLUS internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS - elif version >= 130 and version < 135: # Amazon + elif version >= 130 and version < 135: # AMAZON internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON - elif version >= 135 and version < 140: # Amazon Plus + elif version >= 135 and version < 140: # AMAZON PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS - elif version >= 140 and version < 145: # Crystal + elif version >= 140 and version < 145: # CRYSTAL internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL - elif version >= 145 and version < 150: # Crystal Plus + elif version >= 145 and version < 150: # CRYSTAL PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS - elif version >= 150 and version < 200: # Paradise + elif version >= 150 and version < 200: # PARADISE internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE - elif version >= 200 and version < 205: # New + elif version >= 200 and version < 205: # NEW!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW - elif version >= 205 and version < 210: # New Plus + elif version >= 205 and version < 210: # NEW PLUS!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS + elif version >= 210: # SUN + internal_ver = ChuniConstants.VER_CHUNITHM_SUN if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 67b6fcc..40dee9b 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -23,41 +23,44 @@ class ChuniNew(ChuniBase): self.version = ChuniConstants.VER_CHUNITHM_NEW def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + # use UTC time and convert it to JST time by adding +9 + # matching therefore starts one hour before and lasts for 8 hours match_start = datetime.strftime( - datetime.now() - timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=8), self.date_time_format ) match_end = datetime.strftime( - datetime.now() + timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=16), self.date_time_format ) reboot_start = datetime.strftime( - datetime.now() - timedelta(hours=11), self.date_time_format + datetime.utcnow() + timedelta(hours=6), self.date_time_format ) reboot_end = datetime.strftime( - datetime.now() - timedelta(hours=10), self.date_time_format + datetime.utcnow() + timedelta(hours=7), self.date_time_format ) return { "gameSetting": { - "isMaintenance": "false", + "isMaintenance": False, "requestInterval": 10, "rebootStartTime": reboot_start, "rebootEndTime": reboot_end, - "isBackgroundDistribute": "false", + "isBackgroundDistribute": False, "maxCountCharacter": 300, "maxCountItem": 300, "maxCountMusic": 300, "matchStartTime": match_start, "matchEndTime": match_end, - "matchTimeLimit": 99, + "matchTimeLimit": 60, "matchErrorLimit": 9999, "romVersion": self.game_cfg.version.version(self.version)["rom"], "dataVersion": self.game_cfg.version.version(self.version)["data"], "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", + # might be really important for online battle to connect the cabs via UDP port 50201 "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", }, - "isDumpUpload": "false", - "isAou": "false", + "isDumpUpload": False, + "isAou": False, } def handle_remove_token_api_request(self, data: Dict) -> Dict: @@ -468,3 +471,162 @@ class ChuniNew(ChuniBase): self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} + + def handle_ping_request(self, data: Dict) -> Dict: + # matchmaking ping request + return {"returnCode": "1"} + + def handle_begin_matching_api_request(self, data: Dict) -> Dict: + room_id = 1 + # check if there is a free matching room + matching_room = self.data.item.get_oldest_free_matching(self.version) + + if matching_room is None: + # grab the latest roomId and add 1 for the new room + newest_matching = self.data.item.get_newest_matching(self.version) + if newest_matching is not None: + room_id = newest_matching["roomId"] + 1 + + # fix userName WTF8 + new_member = data["matchingMemberInfo"] + new_member["userName"] = self.read_wtf8(new_member["userName"]) + + # create the new room with room_id and the current user id (host) + # user id is required for the countdown later on + self.data.item.put_matching( + self.version, room_id, [new_member], user_id=new_member["userId"] + ) + + # get the newly created matching room + matching_room = self.data.item.get_matching(self.version, room_id) + else: + # a room already exists, so just add the new member to it + matching_member_list = matching_room["matchingMemberInfoList"] + # fix userName WTF8 + new_member = data["matchingMemberInfo"] + new_member["userName"] = self.read_wtf8(new_member["userName"]) + matching_member_list.append(new_member) + + # add the updated room to the database, make sure to set isFull correctly! + self.data.item.put_matching( + self.version, + matching_room["roomId"], + matching_member_list, + user_id=matching_room["user"], + is_full=True if len(matching_member_list) >= 4 else False, + ) + + matching_wait = { + "isFinish": False, + "restMSec": matching_room["restMSec"], # in sec + "pollingInterval": 1, # in sec + "matchingMemberInfoList": matching_room["matchingMemberInfoList"], + } + + return {"roomId": 1, "matchingWaitState": matching_wait} + + def handle_end_matching_api_request(self, data: Dict) -> Dict: + matching_room = self.data.item.get_matching(self.version, data["roomId"]) + members = matching_room["matchingMemberInfoList"] + + # only set the host user to role 1 every other to 0? + role_list = [ + {"role": 1} if m["userId"] == matching_room["user"] else {"role": 0} + for m in members + ] + + self.data.item.put_matching( + self.version, + matching_room["roomId"], + members, + user_id=matching_room["user"], + rest_sec=0, # make sure to always set 0 + is_full=True, # and full, so no one can join + ) + + return { + "matchingResult": 1, # needs to be 1 for successful matching + "matchingMemberInfoList": members, + # no idea, maybe to differentiate between CPUs and real players? + "matchingMemberRoleList": role_list, + # TCP/UDP connection? + "reflectorUri": f"{self.core_cfg.title.hostname}", + } + + def handle_remove_matching_member_api_request(self, data: Dict) -> Dict: + # get all matching rooms, because Chuni only returns the userId + # not the actual roomId + matching_rooms = self.data.item.get_all_matchings(self.version) + if matching_rooms is None: + return {"returnCode": "1"} + + for room in matching_rooms: + old_members = room["matchingMemberInfoList"] + new_members = [m for m in old_members if m["userId"] != data["userId"]] + + # if nothing changed go to the next room + if len(old_members) == len(new_members): + continue + + # if the last user got removed, delete the matching room + if len(new_members) <= 0: + self.data.item.delete_matching(self.version, room["roomId"]) + else: + # remove the user from the room + self.data.item.put_matching( + self.version, + room["roomId"], + new_members, + user_id=room["user"], + rest_sec=room["restMSec"], + ) + + return {"returnCode": "1"} + + def handle_get_matching_state_api_request(self, data: Dict) -> Dict: + polling_interval = 1 + # get the current active room + matching_room = self.data.item.get_matching(self.version, data["roomId"]) + members = matching_room["matchingMemberInfoList"] + rest_sec = matching_room["restMSec"] + + # grab the current member + current_member = data["matchingMemberInfo"] + + # only the host user can decrease the countdown + if matching_room["user"] == int(current_member["userId"]): + # cap the restMSec to 0 + if rest_sec > 0: + rest_sec -= polling_interval + else: + rest_sec = 0 + + # update the members in order to recieve messages + for i, member in enumerate(members): + if member["userId"] == current_member["userId"]: + # replace the old user data with the current user data, + # also parse WTF-8 everytime + current_member["userName"] = self.read_wtf8(current_member["userName"]) + members[i] = current_member + + self.data.item.put_matching( + self.version, + data["roomId"], + members, + rest_sec=rest_sec, + user_id=matching_room["user"], + ) + + # only add the other members to the list + diff_members = [m for m in members if m["userId"] != current_member["userId"]] + + matching_wait = { + # makes no difference? Always use False? + "isFinish": True if rest_sec == 0 else False, + "restMSec": rest_sec, + "pollingInterval": polling_interval, + # the current user needs to be the first one? + "matchingMemberInfoList": [current_member] + diff_members, + } + + return {"matchingWaitState": matching_wait} diff --git a/titles/chuni/newplus.py b/titles/chuni/newplus.py index 4faf47a..bbe1419 100644 --- a/titles/chuni/newplus.py +++ b/titles/chuni/newplus.py @@ -36,6 +36,6 @@ class ChuniNewPlus(ChuniNew): def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: user_data = super().handle_cm_get_user_preview_api_request(data) - # hardcode lastDataVersion for CardMaker 1.35 + # hardcode lastDataVersion for CardMaker 1.35 A028 user_data["lastDataVersion"] = "2.05.00" return user_data diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 4ffcf93..94c4fd8 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -1,5 +1,12 @@ from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy import ( + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, + delete, +) from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.engine.base import Connection from sqlalchemy.schema import ForeignKey @@ -203,8 +210,141 @@ login_bonus = Table( mysql_charset="utf8mb4", ) +favorite = Table( + "chuni_item_favorite", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("favId", Integer, nullable=False), + Column("favKind", Integer, nullable=False, server_default="1"), + UniqueConstraint("version", "user", "favId", name="chuni_item_favorite_uk"), + mysql_charset="utf8mb4", +) + +matching = Table( + "chuni_item_matching", + metadata, + Column("roomId", Integer, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("restMSec", Integer, nullable=False, server_default="60"), + Column("isFull", Boolean, nullable=False, server_default="0"), + PrimaryKeyConstraint("roomId", "version", name="chuni_item_matching_pk"), + Column("matchingMemberInfoList", JSON, nullable=False), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): + def get_oldest_free_matching(self, version: int) -> Optional[Row]: + sql = matching.select( + and_( + matching.c.version == version, + matching.c.isFull == False + ) + ).order_by(matching.c.roomId.asc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_newest_matching(self, version: int) -> Optional[Row]: + sql = matching.select( + and_( + matching.c.version == version + ) + ).order_by(matching.c.roomId.desc()) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def get_all_matchings(self, version: int) -> Optional[List[Row]]: + sql = matching.select( + and_( + matching.c.version == version + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + + def get_matching(self, version: int, room_id: int) -> Optional[Row]: + sql = matching.select( + and_(matching.c.version == version, matching.c.roomId == room_id) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() + + def put_matching( + self, + version: int, + room_id: int, + matching_member_info_list: list, + user_id: int = None, + rest_sec: int = 60, + is_full: bool = False + ) -> Optional[int]: + sql = insert(matching).values( + roomId=room_id, + version=version, + restMSec=rest_sec, + user=user_id, + isFull=is_full, + matchingMemberInfoList=matching_member_info_list, + ) + + conflict = sql.on_duplicate_key_update( + restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def delete_matching(self, version: int, room_id: int): + sql = delete(matching).where( + and_(matching.c.roomId == room_id, matching.c.version == version) + ) + + result = self.execute(sql) + if result is None: + return None + return result.lastrowid + + def get_all_favorites( + self, user_id: int, version: int, fav_kind: int = 1 + ) -> Optional[List[Row]]: + sql = favorite.select( + and_( + favorite.c.version == version, + favorite.c.user == user_id, + favorite.c.favKind == fav_kind, + ) + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def put_login_bonus( self, user_id: int, version: int, preset_id: int, **login_bonus_data ) -> Optional[int]: diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index e35769c..f8edc33 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -89,8 +89,6 @@ profile = Table( Integer, ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"), ), - Column("avatarBack", Integer, server_default="0"), - Column("avatarFace", Integer, server_default="0"), Column("eliteRankPoint", Integer, server_default="0"), Column("stockedGridCount", Integer, server_default="0"), Column("netBattleLoseCount", Integer, server_default="0"), @@ -98,10 +96,8 @@ profile = Table( Column("netBattle4thCount", Integer, server_default="0"), Column("overPowerRate", Integer, server_default="0"), Column("battleRewardStatus", Integer, server_default="0"), - Column("avatarPoint", Integer, server_default="0"), Column("netBattle1stCount", Integer, server_default="0"), Column("charaIllustId", Integer, server_default="0"), - Column("avatarItem", Integer, server_default="0"), Column("userNameEx", String(8), server_default=""), Column("netBattleWinCount", Integer, server_default="0"), Column("netBattleCorrection", Integer, server_default="0"), @@ -112,7 +108,6 @@ profile = Table( Column("netBattle3rdCount", Integer, server_default="0"), Column("netBattleConsecutiveWinCount", Integer, server_default="0"), Column("overPowerLowerRank", Integer, server_default="0"), - Column("avatarWear", Integer, server_default="0"), Column("classEmblemBase", Integer, server_default="0"), Column("battleRankPoint", Integer, server_default="0"), Column("netBattle2ndCount", Integer, server_default="0"), @@ -120,13 +115,19 @@ profile = Table( Column("skillId", Integer, server_default="0"), Column("lastCountryCode", String(5), server_default="JPN"), Column("isNetBattleHost", Boolean, server_default="0"), - Column("avatarFront", Integer, server_default="0"), - Column("avatarSkin", Integer, server_default="0"), Column("battleRewardCount", Integer, server_default="0"), Column("battleRewardIndex", Integer, server_default="0"), Column("netBattlePlayCount", Integer, server_default="0"), Column("exMapLoopCount", Integer, server_default="0"), Column("netBattleEndState", Integer, server_default="0"), + Column("rankUpChallengeResults", JSON), + Column("avatarBack", Integer, server_default="0"), + Column("avatarFace", Integer, server_default="0"), + Column("avatarPoint", Integer, server_default="0"), + Column("avatarItem", Integer, server_default="0"), + Column("avatarWear", Integer, server_default="0"), + Column("avatarFront", Integer, server_default="0"), + Column("avatarSkin", Integer, server_default="0"), Column("avatarHead", Integer, server_default="0"), UniqueConstraint("user", "version", name="chuni_profile_profile_uk"), mysql_charset="utf8mb4", @@ -417,8 +418,8 @@ class ChuniProfileData(BaseData): sql = ( select([profile, option]) .join(option, profile.c.user == option.c.user) - .filter(and_(profile.c.user == aime_id, profile.c.version == version)) - ) + .filter(and_(profile.c.user == aime_id, profile.c.version <= version)) + ).order_by(profile.c.version.desc()) result = self.execute(sql) if result is None: @@ -429,9 +430,9 @@ class ChuniProfileData(BaseData): sql = select(profile).where( and_( profile.c.user == aime_id, - profile.c.version == version, + profile.c.version <= version, ) - ) + ).order_by(profile.c.version.desc()) result = self.execute(sql) if result is None: @@ -461,9 +462,9 @@ class ChuniProfileData(BaseData): sql = select(profile_ex).where( and_( profile_ex.c.user == aime_id, - profile_ex.c.version == version, + profile_ex.c.version <= version, ) - ) + ).order_by(profile_ex.c.version.desc()) result = self.execute(sql) if result is None: diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index 6a94813..203aa11 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -134,7 +134,9 @@ playlog = Table( Column("charaIllustId", Integer), Column("romVersion", String(255)), Column("judgeHeaven", Integer), - mysql_charset="utf8mb4", + Column("regionId", Integer), + Column("machineType", Integer), + mysql_charset="utf8mb4" ) diff --git a/titles/chuni/schema/static.py b/titles/chuni/schema/static.py index 4537518..85d0397 100644 --- a/titles/chuni/schema/static.py +++ b/titles/chuni/schema/static.py @@ -1,11 +1,19 @@ from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy import ( + ForeignKeyConstraint, + Table, + Column, + UniqueConstraint, + PrimaryKeyConstraint, + and_, +) from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.engine.base import Connection from sqlalchemy.engine import Row from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert +from datetime import datetime from core.data.schema import BaseData, metadata @@ -17,6 +25,7 @@ events = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), mysql_charset="utf8mb4", @@ -125,11 +134,13 @@ gacha_cards = Table( login_bonus_preset = Table( "chuni_static_login_bonus_preset", metadata, - Column("id", Integer, primary_key=True, nullable=False), + Column("presetId", Integer, nullable=False), Column("version", Integer, nullable=False), Column("presetName", String(255), nullable=False), Column("isEnabled", Boolean, server_default="1"), - UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), + PrimaryKeyConstraint( + "presetId", "version", name="chuni_static_login_bonus_preset_pk" + ), mysql_charset="utf8mb4", ) @@ -138,15 +149,7 @@ login_bonus = Table( metadata, Column("id", Integer, primary_key=True, nullable=False), Column("version", Integer, nullable=False), - Column( - "presetId", - ForeignKey( - "chuni_static_login_bonus_preset.id", - ondelete="cascade", - onupdate="cascade", - ), - nullable=False, - ), + Column("presetId", Integer, nullable=False), Column("loginBonusId", Integer, nullable=False), Column("loginBonusName", String(255), nullable=False), Column("presentId", Integer, nullable=False), @@ -157,6 +160,16 @@ login_bonus = Table( UniqueConstraint( "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" ), + ForeignKeyConstraint( + ["presetId", "version"], + [ + "chuni_static_login_bonus_preset.presetId", + "chuni_static_login_bonus_preset.version", + ], + onupdate="CASCADE", + ondelete="CASCADE", + name="chuni_static_login_bonus_ibfk_1", + ), mysql_charset="utf8mb4", ) @@ -236,7 +249,7 @@ class ChuniStaticData(BaseData): self, version: int, preset_id: int, preset_name: str, is_enabled: bool ) -> Optional[int]: sql = insert(login_bonus_preset).values( - id=preset_id, + presetId=preset_id, version=version, presetName=preset_name, isEnabled=is_enabled, @@ -416,6 +429,14 @@ class ChuniStaticData(BaseData): return None return result.fetchall() + def get_music(self, version: int) -> Optional[List[Row]]: + sql = music.select(music.c.version <= version) + + result = self.execute(sql) + if result is None: + return None + return result.fetchall() + def get_music_chart( self, version: int, song_id: int, chart_id: int ) -> Optional[List[Row]]: diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py new file mode 100644 index 0000000..b56fa29 --- /dev/null +++ b/titles/chuni/sun.py @@ -0,0 +1,37 @@ +from typing import Dict, Any + +from core.config import CoreConfig +from titles.chuni.newplus import ChuniNewPlus +from titles.chuni.const import ChuniConstants +from titles.chuni.config import ChuniConfig + + +class ChuniSun(ChuniNewPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_SUN + + def handle_get_game_setting_api_request(self, data: Dict) -> Dict: + ret = super().handle_get_game_setting_api_request(data) + ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"] + ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"] + ret["gameSetting"][ + "matchingUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "matchingUriX" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "udpHolePunchUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + ret["gameSetting"][ + "reflectorUri" + ] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/" + return ret + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.35 A032 + user_data["lastDataVersion"] = "2.10.00" + return user_data diff --git a/titles/idz/userdb.py b/titles/idz/userdb.py index 2f70ba4..2ac765e 100644 --- a/titles/idz/userdb.py +++ b/titles/idz/userdb.py @@ -83,7 +83,13 @@ class IDZUserDBProtocol(Protocol): def dataReceived(self, data: bytes) -> None: self.logger.debug(f"Receive data {data.hex()}") crypt = AES.new(self.static_key, AES.MODE_ECB) - data_dec = crypt.decrypt(data) + + try: + data_dec = crypt.decrypt(data) + + except Exception as e: + self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}") + self.logger.debug(f"Decrypt data {data_dec.hex()}") magic = struct.unpack_from(" None: + self.core_cfg = core_cfg + self.game_cfg = game_cfg + self.core_data = Data(core_cfg) + self.game_data = SaoData(core_cfg) + self.version = 0 + self.logger = logging.getLogger("sao") + + def handle_noop(self, request: Any) -> bytes: + sao_request = request + + sao_id = int(sao_request[:4],16) + 1 + + ret = struct.pack("!HHIIIIIIb", sao_id, 0, 0, 5, 1, 1, 5, 0x01000000, 0).hex() + return bytes.fromhex(ret) + + def handle_c122(self, request: Any) -> bytes: + #common/get_maintenance_info + resp = SaoGetMaintResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c12e(self, request: Any) -> bytes: + #common/ac_cabinet_boot_notification + resp = SaoCommonAcCabinetBootNotificationResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c100(self, request: Any) -> bytes: + #common/get_app_versions + resp = SaoCommonGetAppVersionsRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c102(self, request: Any) -> bytes: + #common/master_data_version_check + resp = SaoMasterDataVersionCheckResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c10a(self, request: Any) -> bytes: + #common/paying_play_start + resp = SaoCommonPayingPlayStartRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_ca02(self, request: Any) -> bytes: + #quest_multi_play_room/get_quest_scene_multi_play_photon_server + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c11e(self, request: Any) -> bytes: + #common/get_auth_card_data + + #Check authentication + access_code = bytes.fromhex(request[188:268]).decode("utf-16le") + user_id = self.core_data.card.get_user_id_from_card( access_code ) + + if not user_id: + user_id = self.core_data.user.create_user() #works + card_id = self.core_data.card.create_card(user_id, access_code) + + if card_id is None: + 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 }") + + #Grab values from profile + profile_data = self.game_data.profile.get_profile(user_id) + + if user_id and not profile_data: + profile_id = self.game_data.profile.create_profile(user_id) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetAuthCardDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c40c(self, request: Any) -> bytes: + #home/check_ac_login_bonus + resp = SaoHomeCheckAcLoginBonusResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c104(self, request: Any) -> bytes: + #common/login + access_code = bytes.fromhex(request[228:308]).decode("utf-16le") + user_id = self.core_data.card.get_user_id_from_card( access_code ) + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoCommonLoginResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c404(self, request: Any) -> bytes: + #home/check_comeback_event + resp = SaoCheckComebackEventRequest(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c000(self, request: Any) -> bytes: + #ticket/ticket + resp = SaoTicketResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c500(self, request: Any) -> bytes: + #user_info/get_user_basic_data + user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetUserBasicDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c600(self, request: Any) -> bytes: + #have_object/get_hero_log_user_data_list + 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, hero_data) + return resp.make() + + def handle_c602(self, request: Any) -> bytes: + #have_object/get_equipment_user_data_list + equipmentIdsData = self.game_data.static.get_equipment_ids(0, True) + + resp = SaoGetEquipmentUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, equipmentIdsData) + return resp.make() + + 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) + return resp.make() + + def handle_c606(self, request: Any) -> bytes: + #have_object/get_support_log_user_data_list + supportIdsData = self.game_data.static.get_support_log_ids(0, True) + + resp = SaoGetSupportLogUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, supportIdsData) + return resp.make() + + def handle_c800(self, request: Any) -> bytes: + #custom/get_title_user_data_list + titleIdsData = self.game_data.static.get_title_ids(0, True) + + resp = SaoGetTitleUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, titleIdsData) + return resp.make() + + def handle_c608(self, request: Any) -> bytes: + #have_object/get_episode_append_data_list + user_id = bytes.fromhex(request[88:112]).decode("utf-16le") + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoGetEpisodeAppendDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c804(self, request: Any) -> bytes: + #custom/get_party_data_list + + 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 + #quest/get_quest_scene_prev_scan_profile_card + resp = SaoGetQuestScenePrevScanProfileCardResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c124(self, request: Any) -> bytes: + #common/get_resource_path_info + resp = SaoGetResourcePathInfoResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c900(self, request: Any) -> bytes: + #quest/get_quest_scene_user_data_list // QuestScene.csv + questIdsData = self.game_data.static.get_quests_ids(0, True) + resp = SaoGetQuestSceneUserDataListResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, questIdsData) + return resp.make() + + def handle_c400(self, request: Any) -> bytes: + #home/check_yui_medal_get_condition + resp = SaoCheckYuiMedalGetConditionResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c402(self, request: Any) -> bytes: + #home/get_yui_medal_bonus_user_data + resp = SaoGetYuiMedalBonusUserDataResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1) + return resp.make() + + def handle_c40a(self, request: Any) -> bytes: + #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:] + + 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 + "act_type" / Int8ub, # play_mode is a byte + Padding(3), + "party_data_list_length" / Rebuild(Int8ub, len_(this.party_data_list)), # party_data_list is a byte, + "party_data_list" / Array(this.party_data_list_length, Struct( + "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id + "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string + "team_no" / Int8ub, # team_no is a byte + Padding(3), + "party_team_data_list_length" / Rebuild(Int8ub, len_(this.party_team_data_list)), # party_team_data_list is a byte + "party_team_data_list" / Array(this.party_team_data_list_length, Struct( + "user_party_team_id_size" / Rebuild(Int32ub, len_(this.user_party_team_id) * 2), # calculates the length of the user_party_team_id + "user_party_team_id" / PaddedString(this.user_party_team_id_size, "utf_16_le"), # user_party_team_id is a (zero) padded string + "arrangement_num" / Int8ub, # arrangement_num is a byte + "user_hero_log_id_size" / Rebuild(Int32ub, len_(this.user_hero_log_id) * 2), # calculates the length of the user_hero_log_id + "user_hero_log_id" / PaddedString(this.user_hero_log_id_size, "utf_16_le"), # user_hero_log_id is a (zero) padded string + "main_weapon_user_equipment_id_size" / Rebuild(Int32ub, len_(this.main_weapon_user_equipment_id) * 2), # calculates the length of the main_weapon_user_equipment_id + "main_weapon_user_equipment_id" / PaddedString(this.main_weapon_user_equipment_id_size, "utf_16_le"), # main_weapon_user_equipment_id is a (zero) padded string + "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_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 + + 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() + + def handle_c904(self, request: Any) -> bytes: + #quest/episode_play_start + + 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 + "episode_id" / Int32ub, # episode_id is a int, + "play_mode" / Int8ub, # play_mode is a byte + Padding(3), + "play_start_request_data_length" / Rebuild(Int8ub, len_(this.play_start_request_data)), # play_start_request_data_length is a byte, + "play_start_request_data" / Array(this.play_start_request_data_length, Struct( + "user_party_id_size" / Rebuild(Int32ub, len_(this.user_party_id) * 2), # calculates the length of the user_party_id + "user_party_id" / PaddedString(this.user_party_id_size, "utf_16_le"), # user_party_id is a (zero) padded string + "appoint_leader_resource_card_code_size" / Rebuild(Int32ub, len_(this.appoint_leader_resource_card_code) * 2), # calculates the length of the total_damage + "appoint_leader_resource_card_code" / PaddedString(this.appoint_leader_resource_card_code_size, "utf_16_le"), # total_damage is a (zero) padded string + "use_profile_card_code_size" / Rebuild(Int32ub, len_(this.use_profile_card_code) * 2), # calculates the length of the total_damage + "use_profile_card_code" / PaddedString(this.use_profile_card_code_size, "utf_16_le"), # use_profile_card_code is a (zero) padded string + "quest_drop_boost_apply_flag" / Int8ub, # quest_drop_boost_apply_flag is a byte + )), + + ) + + req_data = req_struct.parse(req) + + user_id = req_data.user_id + profile_data = self.game_data.profile.get_profile(user_id) + + self.game_data.item.create_session( + user_id, + int(req_data.play_start_request_data[0].user_party_id), + req_data.episode_id, + req_data.play_mode, + req_data.play_start_request_data[0].quest_drop_boost_apply_flag + ) + + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c908(self, request: Any) -> bytes: # Level calculation missing for the profile and heroes + #quest/episode_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), + "episode_id" / Int16ub, # episode_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: + #quest/trial_tower_play_start + user_id = bytes.fromhex(request[100:124]).decode("utf-16le") + floor_id = int(request[130:132], 16) # not required but nice to know + profile_data = self.game_data.profile.get_profile(user_id) + + resp = SaoEpisodePlayStartResponse(int.from_bytes(bytes.fromhex(request[:4]), "big")+1, profile_data) + return resp.make() + + def handle_c90a(self, request: Any) -> 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() diff --git a/titles/sao/config.py b/titles/sao/config.py new file mode 100644 index 0000000..7b9a2d5 --- /dev/null +++ b/titles/sao/config.py @@ -0,0 +1,47 @@ +from core.config import CoreConfig + + +class SaoServerConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def hostname(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "hostname", default="localhost" + ) + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "sao", "server", "loglevel", default="info" + ) + ) + + @property + def port(self) -> int: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "port", default=9000 + ) + + @property + def auto_register(self) -> bool: + """ + Automatically register users in `aime_user` on first carding in with sao + if they don't exist already. Set to false to display an error instead. + """ + return CoreConfig.get_config_field( + self.__config, "sao", "server", "auto_register", default=True + ) + + +class SaoConfig(dict): + def __init__(self) -> None: + self.server = SaoServerConfig(self) diff --git a/titles/sao/const.py b/titles/sao/const.py new file mode 100644 index 0000000..8bdea0f --- /dev/null +++ b/titles/sao/const.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class SaoConstants: + GAME_CODE = "SDEW" + + CONFIG_NAME = "sao.yaml" + + VER_SAO = 0 + + VERSION_NAMES = ("Sword Art Online Arcade") + + @classmethod + def game_ver_to_string(cls, ver: int): + return cls.VERSION_NAMES[ver] diff --git a/titles/sao/data/HeroLogLevel.csv b/titles/sao/data/HeroLogLevel.csv new file mode 100644 index 0000000..36a53b9 --- /dev/null +++ b/titles/sao/data/HeroLogLevel.csv @@ -0,0 +1,121 @@ +HeroLogLevelId,RequireExp +1,0, +2,1000, +3,1200, +4,1400, +5,1600, +6,1900, +7,2200, +8,2500, +9,2800, +10,3100, +11,3500, +12,3900, +13,4300, +14,4700, +15,5100, +16,5550, +17,6000, +18,6450, +19,6900, +20,7350, +21,7850, +22,8350, +23,8850, +24,9350, +25,9850, +26,10450, +27,11050, +28,11650, +29,12250, +30,12850, +31,13550, +32,14250, +33,14950, +34,15650, +35,16350, +36,17150, +37,17950, +38,18750, +39,19550, +40,20350, +41,21250, +42,22150, +43,23050, +44,23950, +45,24850, +46,25850, +47,26850, +48,27850, +49,28850, +50,29850, +51,30950, +52,32050, +53,33150, +54,34250, +55,35350, +56,36550, +57,37750, +58,38950, +59,40150, +60,41350, +61,42650, +62,43950, +63,45250, +64,46550, +65,47850, +66,49250, +67,50650, +68,52050, +69,53450, +70,54850, +71,56350, +72,57850, +73,59350, +74,60850, +75,62350, +76,63950, +77,65550, +78,67150, +79,68750, +80,70350, +81,72050, +82,73750, +83,75450, +84,77150, +85,78850, +86,80650, +87,82450, +88,84250, +89,86050, +90,87850, +91,89750, +92,91650, +93,93550, +94,95450, +95,97350, +96,99350, +97,101350, +98,103350, +99,105350, +100,107350, +101,200000, +102,300000, +103,450000, +104,600000, +105,800000, +106,1000000, +107,1250000, +108,1500000, +109,1800000, +110,2100000, +111,2100000, +112,2100000, +113,2100000, +114,2100000, +115,2100000, +116,2100000, +117,2100000, +118,2100000, +119,2100000, +120,2100000, diff --git a/titles/sao/data/PlayerRank.csv b/titles/sao/data/PlayerRank.csv new file mode 100644 index 0000000..9031661 --- /dev/null +++ b/titles/sao/data/PlayerRank.csv @@ -0,0 +1,301 @@ +PlayerRankId,TotalExp, +1,0, +2,100, +3,300, +4,500, +5,800, +6,1100, +7,1400, +8,1800, +9,2200, +10,2600,, +11,3000,, +12,3500,, +13,4000,, +14,4500,, +15,5000,, +16,5500,, +17,6100,, +18,6700,, +19,7300,, +20,7900,, +21,8500,, +22,9100,, +23,9800,, +24,10500, +25,11200, +26,11900, +27,12600, +28,13300, +29,14000, +30,14800, +31,15600, +32,16400, +33,17200, +34,18000, +35,18800, +36,19600, +37,20400, +38,21300, +39,22200, +40,23100, +41,24000, +42,24900, +43,25800, +44,26700, +45,27600, +46,28500, +47,29500, +48,30600, +49,31800, +50,33100, +51,34500, +52,36000, +53,37600, +54,39300, +55,41100, +56,43000, +57,45000, +58,47100, +59,49300, +60,51600, +61,54000, +62,56500, +63,59100, +64,61800, +65,64600, +66,67500, +67,70500, +68,73600, +69,76800, +70,80100, +71,83500, +72,87000, +73,90600, +74,94300, +75,98100, +76,102000, +77,106000, +78,110100, +79,114300, +80,118600, +81,123000, +82,127500, +83,132100, +84,136800, +85,141600, +86,146500, +87,151500, +88,156600, +89,161800, +90,167100, +91,172500, +92,178000, +93,183600, +94,189300, +95,195100, +96,201000, +97,207000, +98,213100, +99,219300, +100,225600, +101,232000, +102,238500, +103,245100, +104,251800, +105,258600, +106,265500, +107,272500, +108,279600, +109,286800, +110,294100, +111,301500, +112,309000, +113,316600, +114,324300, +115,332100, +116,340000, +117,348000, +118,356100, +119,364300, +120,372600, +121,381000, +122,389500, +123,398100, +124,406800, +125,415600, +126,424500, +127,433500, +128,442600, +129,451800, +130,461100, +131,470500, +132,480000, +133,489600, +134,499300, +135,509100, +136,519000, +137,529000, +138,539100, +139,549300, +140,559600, +141,570000, +142,580500, +143,591100, +144,601800, +145,612600, +146,623500, +147,634500, +148,645600, +149,656800, +150,668100, +151,679500, +152,691000, +153,702600, +154,714300, +155,726100, +156,738000, +157,750000, +158,762100, +159,774300, +160,786600, +161,799000, +162,811500, +163,824100, +164,836800, +165,849600, +166,862500, +167,875500, +168,888600, +169,901800, +170,915100, +171,928500, +172,942000, +173,955600, +174,969300, +175,983100, +176,997000, +177,1011000, +178,1025100, +179,1039300, +180,1053600, +181,1068000, +182,1082500, +183,1097100, +184,1111800, +185,1126600, +186,1141500, +187,1156500, +188,1171600, +189,1186800, +190,1202100, +191,1217500, +192,1233000, +193,1248600, +194,1264300, +195,1280100, +196,1296000, +197,1312000, +198,1328100, +199,1344300, +200,1360600, +201,1377000, +202,1393500, +203,1410100, +204,1426800, +205,1443600, +206,1460500, +207,1477500, +208,1494600, +209,1511800, +210,1529100, +211,1546500, +212,1564000, +213,1581600, +214,1599300, +215,1617100, +216,1635000, +217,1653000, +218,1671100, +219,1689300, +220,1707600, +221,1726000, +222,1744500, +223,1763100, +224,1781800, +225,1800600, +226,1819500, +227,1838500, +228,1857600, +229,1876800, +230,1896100, +231,1915500, +232,1935000, +233,1954600, +234,1974300, +235,1994100, +236,2014000, +237,2034000, +238,2054100, +239,2074300, +240,2094600, +241,2115000, +242,2135500, +243,2156100, +244,2176800, +245,2197600, +246,2218500, +247,2239500, +248,2260600, +249,2281800, +250,2303100, +251,2324500, +252,2346000, +253,2367600, +254,2389300, +255,2411100, +256,2433000, +257,2455000, +258,2477100, +259,2499300, +260,2521600, +261,2544000, +262,2566500, +263,2589100, +264,2611800, +265,2634600, +266,2657500, +267,2680500, +268,2703600, +269,2726800, +270,2750100, +271,2773500, +272,2797000, +273,2820600, +274,2844300, +275,2868100, +276,2892000, +277,2916000, +278,2940100, +279,2964300, +280,2988600, +281,3013000, +282,3037500, +283,3062100, +284,3086800, +285,3111600, +286,3136500, +287,3161500, +288,3186600, +289,3211800, +290,3237100, +291,3262500, +292,3288000, +293,3313600, +294,3339300, +295,3365100, +296,3391000, +297,3417000, +298,3443100, +299,3469300, +300,3495600, diff --git a/titles/sao/database.py b/titles/sao/database.py new file mode 100644 index 0000000..463440d --- /dev/null +++ b/titles/sao/database.py @@ -0,0 +1,12 @@ +from core.data import Data +from core.config import CoreConfig + +from .schema import * + + +class SaoData(Data): + def __init__(self, cfg: CoreConfig) -> None: + super().__init__(cfg) + + self.profile = SaoProfileData(cfg, self.session) + self.static = SaoStaticData(cfg, self.session) \ No newline at end of file diff --git a/titles/sao/handlers/__init__.py b/titles/sao/handlers/__init__.py new file mode 100644 index 0000000..90a6b4e --- /dev/null +++ b/titles/sao/handlers/__init__.py @@ -0,0 +1 @@ +from titles.sao.handlers.base import * \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py new file mode 100644 index 0000000..0c74182 --- /dev/null +++ b/titles/sao/handlers/base.py @@ -0,0 +1,1739 @@ +import struct +from datetime import datetime +from construct import * +import sys +import csv + +class SaoBaseRequest: + def __init__(self, data: bytes) -> None: + self.cmd = struct.unpack_from("!H", bytes)[0] + # TODO: The rest of the request header + +class SaoBaseResponse: + def __init__(self, cmd_id: int) -> None: + self.cmd = cmd_id + self.err_status = 0 + self.error_type = 0 + self.vendor_id = 5 + self.game_id = 1 + self.version_id = 1 + self.length = 1 + + def make(self) -> bytes: + return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) + +class SaoNoopResponse(SaoBaseResponse): + def __init__(self, cmd: int) -> None: + super().__init__(cmd) + self.result = 1 + self.length = 5 + + def make(self) -> bytes: + return super().make() + struct.pack("!bI", self.result, 0) + +class SaoGetMaintRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + # TODO: The rest of the mait info request + +class SaoGetMaintResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.maint_begin = datetime.fromtimestamp(0) + self.maint_begin_int_ct = 6 + self.maint_end = datetime.fromtimestamp(0) + self.maint_end_int_ct = 6 + self.dt_format = "%Y%m%d%H%M%S" + + def make(self) -> bytes: + maint_begin_list = [x for x in datetime.strftime(self.maint_begin, self.dt_format)] + maint_end_list = [x for x in datetime.strftime(self.maint_end, self.dt_format)] + self.maint_begin_int_ct = len(maint_begin_list) * 2 + self.maint_end_int_ct = len(maint_end_list) * 2 + + maint_begin_bytes = b"" + maint_end_bytes = b"" + + for x in maint_begin_list: + maint_begin_bytes += struct.pack(" None: + super().__init__(data) + +class SaoCommonAcCabinetBootNotificationResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoMasterDataVersionCheckRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoMasterDataVersionCheckResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.update_flag = 0 + self.data_version = 100 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "update_flag" / Int8ul, # result is either 0 or 1 + "data_version" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + update_flag=self.update_flag, + data_version=self.data_version, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonGetAppVersionsRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonGetAppVersionsRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_list_size = 1 # Number of arrays + + self.version_app_id = 1 + self.applying_start_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_list_size" / Int32ub, + + "version_app_id" / Int32ub, + "applying_start_date_size" / Int32ub, # big endian + "applying_start_date" / Int16ul[len(self.applying_start_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_list_size=self.data_list_size, + + version_app_id=self.version_app_id, + applying_start_date_size=len(self.applying_start_date) * 2, + applying_start_date=[ord(x) for x in self.applying_start_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonPayingPlayStartRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonPayingPlayStartRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.paying_session_id = "1" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "paying_session_id_size" / Int32ub, # big endian + "paying_session_id" / Int16ul[len(self.paying_session_id)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + paying_session_id_size=len(self.paying_session_id) * 2, + paying_session_id=[ord(x) for x in self.paying_session_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetAuthCardDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetAuthCardDataResponse(SaoBaseResponse): #GssSite.dll / GssSiteSystem / GameConnectProt / public class get_auth_card_data_R : GameConnect.GssProtocolBase + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + + self.result = 1 + self.unused_card_flag = "" + self.first_play_flag = 0 + self.tutorial_complete_flag = 1 + self.nick_name = profile_data['nick_name'] # nick_name field #4 + self.personal_id = str(profile_data['user']) + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "unused_card_flag_size" / Int32ub, # big endian + "unused_card_flag" / Int16ul[len(self.unused_card_flag)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "tutorial_complete_flag" / Int8ul, # result is either 0 or 1 + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "personal_id_size" / Int32ub, # big endian + "personal_id" / Int16ul[len(self.personal_id)] + ) + + resp_data = resp_struct.build(dict( + result=self.result, + unused_card_flag_size=len(self.unused_card_flag) * 2, + unused_card_flag=[ord(x) for x in self.unused_card_flag], + first_play_flag=self.first_play_flag, + tutorial_complete_flag=self.tutorial_complete_flag, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + personal_id_size=len(self.personal_id) * 2, + personal_id=[ord(x) for x in self.personal_id] + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoHomeCheckAcLoginBonusRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoHomeCheckAcLoginBonusResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.reward_get_flag = 1 + self.get_ac_login_bonus_id_list_size = 2 # Array + + self.get_ac_login_bonus_id_1 = 1 # "2020年7月9日~(アニメ&リコリス記念)" + self.get_ac_login_bonus_id_2 = 2 # "2020年10月6日~(秋のデビュー&カムバックCP)" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "reward_get_flag" / Int8ul, # result is either 0 or 1 + "get_ac_login_bonus_id_list_size" / Int32ub, + + "get_ac_login_bonus_id_1" / Int32ub, + "get_ac_login_bonus_id_2" / Int32ub, + ) + + resp_data = resp_struct.build(dict( + result=self.result, + reward_get_flag=self.reward_get_flag, + get_ac_login_bonus_id_list_size=self.get_ac_login_bonus_id_list_size, + + get_ac_login_bonus_id_1=self.get_ac_login_bonus_id_1, + get_ac_login_bonus_id_2=self.get_ac_login_bonus_id_2, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestSceneMultiPlayPhotonServerRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.application_id = "7df3a2f6-d69d-4073-aafe-810ee61e1cea" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "application_id_size" / Int32ub, # big endian + "application_id" / Int16ul[len(self.application_id)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + application_id_size=len(self.application_id) * 2, + application_id=[ord(x) for x in self.application_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoTicketRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoTicketResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = "1" + self.ticket_id = "9" #up to 18 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result_size" / Int32ub, # big endian + "result" / Int16ul[len(self.result)], + "ticket_id_size" / Int32ub, # big endian + "ticket_id" / Int16ul[len(self.result)], + ) + + resp_data = resp_struct.build(dict( + result_size=len(self.result) * 2, + result=[ord(x) for x in self.result], + ticket_id_size=len(self.ticket_id) * 2, + ticket_id=[ord(x) for x in self.ticket_id], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCommonLoginRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCommonLoginResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.user_id = str(profile_data['user']) + self.first_play_flag = 0 + self.grantable_free_ticket_flag = 1 + self.login_reward_vp = 99 + self.today_paying_flag = 1 + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_id_size" / Int32ub, # big endian + "user_id" / Int16ul[len(self.user_id)], + "first_play_flag" / Int8ul, # result is either 0 or 1 + "grantable_free_ticket_flag" / Int8ul, # result is either 0 or 1 + "login_reward_vp" / Int16ub, + "today_paying_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_id_size=len(self.user_id) * 2, + user_id=[ord(x) for x in self.user_id], + first_play_flag=self.first_play_flag, + grantable_free_ticket_flag=self.grantable_free_ticket_flag, + login_reward_vp=self.login_reward_vp, + today_paying_flag=self.today_paying_flag, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckComebackEventRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckComebackEventRequest(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag_ = 1 + self.get_comeback_event_id_list = "" # Array of events apparently + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag_" / Int8ul, # result is either 0 or 1 + "get_comeback_event_id_list_size" / Int32ub, # big endian + "get_comeback_event_id_list" / Int16ul[len(self.get_comeback_event_id_list)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag_=self.get_flag_, + get_comeback_event_id_list_size=len(self.get_comeback_event_id_list) * 2, + get_comeback_event_id_list=[ord(x) for x in self.get_comeback_event_id_list], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetUserBasicDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetUserBasicDataResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.user_basic_data_size = 1 # Number of arrays + self.user_type = profile_data['user_type'] + self.nick_name = profile_data['nick_name'] + self.rank_num = profile_data['rank_num'] + self.rank_exp = profile_data['rank_exp'] + self.own_col = profile_data['own_col'] + self.own_vp = profile_data['own_vp'] + self.own_yui_medal = profile_data['own_yui_medal'] + self.setting_title_id = profile_data['setting_title_id'] + self.favorite_user_hero_log_id = "" + self.favorite_user_support_log_id = "" + self.my_store_id = "1" + self.my_store_name = "ARTEMiS" + self.user_reg_date = "20230101120000" + + def make(self) -> bytes: + # create a resp struct + ''' + bool = Int8ul + short = Int16ub + int = Int32ub + ''' + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "user_basic_data_size" / Int32ub, + + "user_type" / Int16ub, + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, + "rank_exp" / Int32ub, + "own_col" / Int32ub, + "own_vp" / Int32ub, + "own_yui_medal" / Int32ub, + "setting_title_id" / Int32ub, + "favorite_user_hero_log_id_size" / Int32ub, # big endian + "favorite_user_hero_log_id" / Int16ul[len(str(self.favorite_user_hero_log_id))], + "favorite_user_support_log_id_size" / Int32ub, # big endian + "favorite_user_support_log_id" / Int16ul[len(str(self.favorite_user_support_log_id))], + "my_store_id_size" / Int32ub, # big endian + "my_store_id" / Int16ul[len(str(self.my_store_id))], + "my_store_name_size" / Int32ub, # big endian + "my_store_name" / Int16ul[len(str(self.my_store_name))], + "user_reg_date_size" / Int32ub, # big endian + "user_reg_date" / Int16ul[len(self.user_reg_date)] + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + user_basic_data_size=self.user_basic_data_size, + + user_type=self.user_type, + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=self.rank_num, + rank_exp=self.rank_exp, + own_col=self.own_col, + own_vp=self.own_vp, + own_yui_medal=self.own_yui_medal, + setting_title_id=self.setting_title_id, + favorite_user_hero_log_id_size=len(self.favorite_user_hero_log_id) * 2, + favorite_user_hero_log_id=[ord(x) for x in str(self.favorite_user_hero_log_id)], + favorite_user_support_log_id_size=len(self.favorite_user_support_log_id) * 2, + favorite_user_support_log_id=[ord(x) for x in str(self.favorite_user_support_log_id)], + my_store_id_size=len(self.my_store_id) * 2, + my_store_id=[ord(x) for x in str(self.my_store_id)], + my_store_name_size=len(self.my_store_name) * 2, + my_store_name=[ord(x) for x in str(self.my_store_name)], + user_reg_date_size=len(self.user_reg_date) * 2, + user_reg_date=[ord(x) for x in self.user_reg_date], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetHeroLogUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, hero_data) -> None: + super().__init__(cmd) + self.result = 1 + + 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)): + + # 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[i][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=[], + ))) + + for i in range(len(self.hero_log_id)): + hero_data = dict( + 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[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[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, + 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 SaoGetEquipmentUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, equipmentIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # equipment_user_data_list + self.user_equipment_id = list(map(str,equipmentIdsData)) #str + self.equipment_id = equipmentIdsData #int + self.enhancement_value = 10 #short + self.max_enhancement_value_extended_num = 10 #short + self.enhancement_exp = 1000 #int + self.possible_awakening_flag = 0 #byte + self.awakening_stage = 0 #short + self.awakening_exp = 0 #int + self.property1_property_id = 0 #int + self.property1_value1 = 0 #int + self.property1_value2 = 0 #int + self.property2_property_id = 0 #int + self.property2_value1 = 0 #int + self.property2_value2 = 0 #int + self.property3_property_id = 0 #int + self.property3_value1 = 0 #int + self.property3_value2 = 0 #int + self.property4_property_id = 0 #int + self.property4_value1 = 0 #int + self.property4_value2 = 0 #int + self.converted_card_num = 1 #short + self.shop_purchase_flag = 1 #byte + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + + equipment_user_data_list_struct = Struct( + "user_equipment_id_size" / Int32ub, # big endian + "user_equipment_id" / Int16ul[9], #string + "equipment_id" / Int32ub, #int + "enhancement_value" / Int16ub, #short + "max_enhancement_value_extended_num" / Int16ub, #short + "enhancement_exp" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, #int + "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 + "equipment_user_data_list_size" / Rebuild(Int32ub, len_(this.equipment_user_data_list)), # big endian + "equipment_user_data_list" / Array(this.equipment_user_data_list_size, equipment_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + equipment_user_data_list_size=0, + equipment_user_data_list=[], + ))) + + for i in range(len(self.equipment_id)): + equipment_data = dict( + user_equipment_id_size=len(self.user_equipment_id[i]) * 2, + user_equipment_id=[ord(x) for x in self.user_equipment_id[i]], + equipment_id=self.equipment_id[i], + enhancement_value=self.enhancement_value, + max_enhancement_value_extended_num=self.max_enhancement_value_extended_num, + enhancement_exp=self.enhancement_exp, + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + 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.equipment_user_data_list.append(equipment_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetItemUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetItemUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, itemIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # item_user_data_list + self.user_item_id = list(map(str,itemIdsData)) #str + self.item_id = itemIdsData #int + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + #new stuff + + item_user_data_list_struct = Struct( + "user_item_id_size" / Int32ub, # big endian + "user_item_id" / Int16ul[6], #string but this will not work with 10000 IDs... only with 6 digits + "item_id" / Int32ub, #int + "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 + "item_user_data_list_size" / Rebuild(Int32ub, len_(this.item_user_data_list)), # big endian + "item_user_data_list" / Array(this.item_user_data_list_size, item_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + item_user_data_list_size=0, + item_user_data_list=[], + ))) + + for i in range(len(self.item_id)): + item_data = dict( + user_item_id_size=len(self.user_item_id[i]) * 2, + user_item_id=[ord(x) for x in self.user_item_id[i]], + item_id=self.item_id[i], + 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.item_user_data_list.append(item_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetSupportLogUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, supportIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # support_log_user_data_list + self.user_support_log_id = list(map(str,supportIdsData)) #str + self.support_log_id = supportIdsData #int + self.possible_awakening_flag = 0 + self.awakening_stage = 0 + self.awakening_exp = 0 + self.converted_card_num = 0 + self.shop_purchase_flag = 0 + self.protect_flag = 0 #byte + self.get_date = "20230101120000" #str + + def make(self) -> bytes: + support_log_user_data_list_struct = Struct( + "user_support_log_id_size" / Int32ub, # big endian + "user_support_log_id" / Int16ul[9], + "support_log_id" / Int32ub, #int + "possible_awakening_flag" / Int8ul, # result is either 0 or 1 + "awakening_stage" / Int16ub, #short + "awakening_exp" / Int32ub, # int + "converted_card_num" / Int16ub, #short + "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 + "support_log_user_data_list_size" / Rebuild(Int32ub, len_(this.support_log_user_data_list)), # big endian + "support_log_user_data_list" / Array(this.support_log_user_data_list_size, support_log_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + support_log_user_data_list_size=0, + support_log_user_data_list=[], + ))) + + for i in range(len(self.support_log_id)): + support_data = dict( + user_support_log_id_size=len(self.user_support_log_id[i]) * 2, + user_support_log_id=[ord(x) for x in self.user_support_log_id[i]], + support_log_id=self.support_log_id[i], + possible_awakening_flag=self.possible_awakening_flag, + awakening_stage=self.awakening_stage, + awakening_exp=self.awakening_exp, + 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.support_log_user_data_list.append(support_data) + + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetTitleUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetTitleUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, titleIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # title_user_data_list + self.user_title_id = list(map(str,titleIdsData)) #str + self.title_id = titleIdsData #int + + def make(self) -> bytes: + title_user_data_list_struct = Struct( + "user_title_id_size" / Int32ub, # big endian + "user_title_id" / Int16ul[6], #string + "title_id" / Int32ub, #int + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "title_user_data_list_size" / Rebuild(Int32ub, len_(this.title_user_data_list)), # big endian + "title_user_data_list" / Array(this.title_user_data_list_size, title_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + title_user_data_list_size=0, + title_user_data_list=[], + ))) + + for i in range(len(self.title_id)): + title_data = dict( + user_title_id_size=len(self.user_title_id[i]) * 2, + user_title_id=[ord(x) for x in self.user_title_id[i]], + title_id=self.title_id[i], + ) + + resp_data.title_user_data_list.append(title_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetEpisodeAppendDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.length = None + self.result = 1 + + self.user_episode_append_id_list = ["10001", "10002", "10003", "10004", "10005"] + self.user_id_list = [str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"]), str(profile_data["user"])] + self.episode_append_id_list = [10001, 10002, 10003, 10004, 10005] + self.own_num_list = [3, 3, 3, 3 ,3] + + def make(self) -> bytes: + episode_data_struct = Struct( + "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, + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "episode_append_data_list_size" / Rebuild(Int32ub, len_(this.episode_append_data_list)), # big endian + "episode_append_data_list" / Array(this.episode_append_data_list_size, episode_data_struct), + ) + + # really dump to parse the build resp, but that creates a new object + # and is nicer to twork with + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + episode_append_data_list_size=0, + episode_append_data_list=[], + ))) + + if len(self.user_episode_append_id_list) != len(self.user_id_list) != len(self.episode_append_id_list) != len(self.own_num_list): + raise ValueError("all lists must be of the same length") + + 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=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], + )) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetPartyDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party + 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 + + self.user_party_id = "0" + self.team_no = 0 + self.party_team_data_list_size = 3 # Number of arrays + + self.user_party_team_id_1 = "0" + self.arrangement_num_1 = 0 + 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 = 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 = 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 + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "party_data_list_size" / Int32ub, # big endian + + "user_party_id_size" / Int32ub, # big endian + "user_party_id" / Int16ul[len(self.user_party_id)], + "team_no" / Int8ul, # result is either 0 or 1 + "party_team_data_list_size" / Int32ub, # big endian + + "user_party_team_id_1_size" / Int32ub, # big endian + "user_party_team_id_1" / Int16ul[len(self.user_party_team_id_1)], + "arrangement_num_1" / Int8ul, # big endian + "user_hero_log_id_1_size" / Int32ub, # big endian + "user_hero_log_id_1" / Int16ul[len(self.user_hero_log_id_1)], + "main_weapon_user_equipment_id_1_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_1" / Int16ul[len(self.main_weapon_user_equipment_id_1)], + "sub_equipment_user_equipment_id_1_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_1" / Int16ul[len(self.sub_equipment_user_equipment_id_1)], + "skill_slot1_skill_id_1" / Int32ub, + "skill_slot2_skill_id_1" / Int32ub, + "skill_slot3_skill_id_1" / Int32ub, + "skill_slot4_skill_id_1" / Int32ub, + "skill_slot5_skill_id_1" / Int32ub, + + "user_party_team_id_2_size" / Int32ub, # big endian + "user_party_team_id_2" / Int16ul[len(self.user_party_team_id_2)], + "arrangement_num_2" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_2_size" / Int32ub, # big endian + "user_hero_log_id_2" / Int16ul[len(self.user_hero_log_id_2)], + "main_weapon_user_equipment_id_2_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_2" / Int16ul[len(self.main_weapon_user_equipment_id_2)], + "sub_equipment_user_equipment_id_2_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_2" / Int16ul[len(self.sub_equipment_user_equipment_id_2)], + "skill_slot1_skill_id_2" / Int32ub, + "skill_slot2_skill_id_2" / Int32ub, + "skill_slot3_skill_id_2" / Int32ub, + "skill_slot4_skill_id_2" / Int32ub, + "skill_slot5_skill_id_2" / Int32ub, + + "user_party_team_id_3_size" / Int32ub, # big endian + "user_party_team_id_3" / Int16ul[len(self.user_party_team_id_3)], + "arrangement_num_3" / Int8ul, # result is either 0 or 1 + "user_hero_log_id_3_size" / Int32ub, # big endian + "user_hero_log_id_3" / Int16ul[len(self.user_hero_log_id_3)], + "main_weapon_user_equipment_id_3_size" / Int32ub, # big endian + "main_weapon_user_equipment_id_3" / Int16ul[len(self.main_weapon_user_equipment_id_3)], + "sub_equipment_user_equipment_id_3_size" / Int32ub, # big endian + "sub_equipment_user_equipment_id_3" / Int16ul[len(self.sub_equipment_user_equipment_id_3)], + "skill_slot1_skill_id_3" / Int32ub, + "skill_slot2_skill_id_3" / Int32ub, + "skill_slot3_skill_id_3" / Int32ub, + "skill_slot4_skill_id_3" / Int32ub, + "skill_slot5_skill_id_3" / Int32ub, + + ) + + resp_data = resp_struct.build(dict( + result=self.result, + party_data_list_size=self.party_data_list_size, + + user_party_id_size=len(self.user_party_id) * 2, + user_party_id=[ord(x) for x in self.user_party_id], + team_no=self.team_no, + party_team_data_list_size=self.party_team_data_list_size, + + user_party_team_id_1_size=len(self.user_party_team_id_1) * 2, + user_party_team_id_1=[ord(x) for x in self.user_party_team_id_1], + arrangement_num_1=self.arrangement_num_1, + user_hero_log_id_1_size=len(self.user_hero_log_id_1) * 2, + user_hero_log_id_1=[ord(x) for x in self.user_hero_log_id_1], + main_weapon_user_equipment_id_1_size=len(self.main_weapon_user_equipment_id_1) * 2, + main_weapon_user_equipment_id_1=[ord(x) for x in self.main_weapon_user_equipment_id_1], + sub_equipment_user_equipment_id_1_size=len(self.sub_equipment_user_equipment_id_1) * 2, + sub_equipment_user_equipment_id_1=[ord(x) for x in self.sub_equipment_user_equipment_id_1], + skill_slot1_skill_id_1=self.skill_slot1_skill_id_1, + skill_slot2_skill_id_1=self.skill_slot2_skill_id_1, + skill_slot3_skill_id_1=self.skill_slot3_skill_id_1, + skill_slot4_skill_id_1=self.skill_slot4_skill_id_1, + skill_slot5_skill_id_1=self.skill_slot5_skill_id_1, + + user_party_team_id_2_size=len(self.user_party_team_id_2) * 2, + user_party_team_id_2=[ord(x) for x in self.user_party_team_id_2], + arrangement_num_2=self.arrangement_num_2, + user_hero_log_id_2_size=len(self.user_hero_log_id_2) * 2, + user_hero_log_id_2=[ord(x) for x in self.user_hero_log_id_2], + main_weapon_user_equipment_id_2_size=len(self.main_weapon_user_equipment_id_2) * 2, + main_weapon_user_equipment_id_2=[ord(x) for x in self.main_weapon_user_equipment_id_2], + sub_equipment_user_equipment_id_2_size=len(self.sub_equipment_user_equipment_id_2) * 2, + sub_equipment_user_equipment_id_2=[ord(x) for x in self.sub_equipment_user_equipment_id_2], + skill_slot1_skill_id_2=self.skill_slot1_skill_id_2, + skill_slot2_skill_id_2=self.skill_slot2_skill_id_2, + skill_slot3_skill_id_2=self.skill_slot3_skill_id_2, + skill_slot4_skill_id_2=self.skill_slot4_skill_id_2, + skill_slot5_skill_id_2=self.skill_slot5_skill_id_2, + + user_party_team_id_3_size=len(self.user_party_team_id_3) * 2, + user_party_team_id_3=[ord(x) for x in self.user_party_team_id_3], + arrangement_num_3=self.arrangement_num_3, + user_hero_log_id_3_size=len(self.user_hero_log_id_3) * 2, + user_hero_log_id_3=[ord(x) for x in self.user_hero_log_id_3], + main_weapon_user_equipment_id_3_size=len(self.main_weapon_user_equipment_id_3) * 2, + main_weapon_user_equipment_id_3=[ord(x) for x in self.main_weapon_user_equipment_id_3], + sub_equipment_user_equipment_id_3_size=len(self.sub_equipment_user_equipment_id_3) * 2, + sub_equipment_user_equipment_id_3=[ord(x) for x in self.sub_equipment_user_equipment_id_3], + skill_slot1_skill_id_3=self.skill_slot1_skill_id_3, + skill_slot2_skill_id_3=self.skill_slot2_skill_id_3, + skill_slot3_skill_id_3=self.skill_slot3_skill_id_3, + skill_slot4_skill_id_3=self.skill_slot4_skill_id_3, + skill_slot5_skill_id_3=self.skill_slot5_skill_id_3, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.profile_card_data = 1 # number of arrays + + self.profile_card_code = "" + self.nick_name = "" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "profile_card_data" / Int32ub, # big endian + + "profile_card_code_size" / Int32ub, # big endian + "profile_card_code" / Int16ul[len(self.profile_card_code)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + "rank_num" / Int16ub, #short + "setting_title_id" / Int32ub, # int + "skill_id" / Int16ub, # short + "hero_log_hero_log_id" / Int32ub, # int + "hero_log_log_level" / Int16ub, # short + "hero_log_awakening_stage" / Int16ub, # short + "hero_log_property1_property_id" / Int32ub, # int + "hero_log_property1_value1" / Int32ub, # int + "hero_log_property1_value2" / Int32ub, # int + "hero_log_property2_property_id" / Int32ub, # int + "hero_log_property2_value1" / Int32ub, # int + "hero_log_property2_value2" / Int32ub, # int + "hero_log_property3_property_id" / Int32ub, # int + "hero_log_property3_value1" / Int32ub, # int + "hero_log_property3_value2" / Int32ub, # int + "hero_log_property4_property_id" / Int32ub, # int + "hero_log_property4_value1" / Int32ub, # int + "hero_log_property4_value2" / Int32ub, # int + "main_weapon_equipment_id" / Int32ub, # int + "main_weapon_enhancement_value" / Int16ub, # short + "main_weapon_awakening_stage" / Int16ub, # short + "main_weapon_property1_property_id" / Int32ub, # int + "main_weapon_property1_value1" / Int32ub, # int + "main_weapon_property1_value2" / Int32ub, # int + "main_weapon_property2_property_id" / Int32ub, # int + "main_weapon_property2_value1" / Int32ub, # int + "main_weapon_property2_value2" / Int32ub, # int + "main_weapon_property3_property_id" / Int32ub, # int + "main_weapon_property3_value1" / Int32ub, # int + "main_weapon_property3_value2" / Int32ub, # int + "main_weapon_property4_property_id" / Int32ub, # int + "main_weapon_property4_value1" / Int32ub, # int + "main_weapon_property4_value2" / Int32ub, # int + "sub_equipment_equipment_id" / Int32ub, # int + "sub_equipment_enhancement_value" / Int16ub, # short + "sub_equipment_awakening_stage" / Int16ub, # short + "sub_equipment_property1_property_id" / Int32ub, # int + "sub_equipment_property1_value1" / Int32ub, # int + "sub_equipment_property1_value2" / Int32ub, # int + "sub_equipment_property2_property_id" / Int32ub, # int + "sub_equipment_property2_value1" / Int32ub, # int + "sub_equipment_property2_value2" / Int32ub, # int + "sub_equipment_property3_property_id" / Int32ub, # int + "sub_equipment_property3_value1" / Int32ub, # int + "sub_equipment_property3_value2" / Int32ub, # int + "sub_equipment_property4_property_id" / Int32ub, # int + "sub_equipment_property4_value1" / Int32ub, # int + "sub_equipment_property4_value2" / Int32ub, # int + "holographic_flag" / Int8ul, # result is either 0 or 1 + ) + + resp_data = resp_struct.build(dict( + result=self.result, + profile_card_data=self.profile_card_data, + + profile_card_code_size=len(self.profile_card_code) * 2, + profile_card_code=[ord(x) for x in self.profile_card_code], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + rank_num=0, + setting_title_id=0, + skill_id=0, + hero_log_hero_log_id=0, + hero_log_log_level=0, + hero_log_awakening_stage=0, + hero_log_property1_property_id=0, + hero_log_property1_value1=0, + hero_log_property1_value2=0, + hero_log_property2_property_id=0, + hero_log_property2_value1=0, + hero_log_property2_value2=0, + hero_log_property3_property_id=0, + hero_log_property3_value1=0, + hero_log_property3_value2=0, + hero_log_property4_property_id=0, + hero_log_property4_value1=0, + hero_log_property4_value2=0, + main_weapon_equipment_id=0, + main_weapon_enhancement_value=0, + main_weapon_awakening_stage=0, + main_weapon_property1_property_id=0, + main_weapon_property1_value1=0, + main_weapon_property1_value2=0, + main_weapon_property2_property_id=0, + main_weapon_property2_value1=0, + main_weapon_property2_value2=0, + main_weapon_property3_property_id=0, + main_weapon_property3_value1=0, + main_weapon_property3_value2=0, + main_weapon_property4_property_id=0, + main_weapon_property4_value1=0, + main_weapon_property4_value2=0, + sub_equipment_equipment_id=0, + sub_equipment_enhancement_value=0, + sub_equipment_awakening_stage=0, + sub_equipment_property1_property_id=0, + sub_equipment_property1_value1=0, + sub_equipment_property1_value2=0, + sub_equipment_property2_property_id=0, + sub_equipment_property2_value1=0, + sub_equipment_property2_value2=0, + sub_equipment_property3_property_id=0, + sub_equipment_property3_value1=0, + sub_equipment_property3_value2=0, + sub_equipment_property4_property_id=0, + sub_equipment_property4_value1=0, + sub_equipment_property4_value2=0, + holographic_flag=0, + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetResourcePathInfoRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetResourcePathInfoResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.resource_base_url = "http://localhost:9000/SDEW/100/" + self.gasha_base_dir = "a" + self.ad_base_dir = "b" + self.event_base_dir = "c" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "resource_base_url_size" / Int32ub, # big endian + "resource_base_url" / Int16ul[len(self.resource_base_url)], + "gasha_base_dir_size" / Int32ub, # big endian + "gasha_base_dir" / Int16ul[len(self.gasha_base_dir)], + "ad_base_dir_size" / Int32ub, # big endian + "ad_base_dir" / Int16ul[len(self.ad_base_dir)], + "event_base_dir_size" / Int32ub, # big endian + "event_base_dir" / Int16ul[len(self.event_base_dir)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + resource_base_url_size=len(self.resource_base_url) * 2, + resource_base_url=[ord(x) for x in self.resource_base_url], + gasha_base_dir_size=len(self.gasha_base_dir) * 2, + gasha_base_dir=[ord(x) for x in self.gasha_base_dir], + ad_base_dir_size=len(self.ad_base_dir) * 2, + ad_base_dir=[ord(x) for x in self.ad_base_dir], + event_base_dir_size=len(self.event_base_dir) * 2, + event_base_dir=[ord(x) for x in self.event_base_dir], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayStartRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayStartResponse(SaoBaseResponse): + def __init__(self, cmd, profile_data) -> None: + super().__init__(cmd) + self.result = 1 + self.play_start_response_data_size = 1 # Number of arrays (minimum 1 mandatory) + self.multi_play_start_response_data_size = 0 # Number of arrays (set 0 due to single play) + + self.appearance_player_trace_data_list_size = 1 + + self.user_quest_scene_player_trace_id = "1003" + self.nick_name = profile_data["nick_name"] + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "play_start_response_data_size" / Int32ub, + "multi_play_start_response_data_size" / Int32ub, + + "appearance_player_trace_data_list_size" / Int32ub, + + "user_quest_scene_player_trace_id_size" / Int32ub, # big endian + "user_quest_scene_player_trace_id" / Int16ul[len(self.user_quest_scene_player_trace_id)], + "nick_name_size" / Int32ub, # big endian + "nick_name" / Int16ul[len(self.nick_name)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + play_start_response_data_size=self.play_start_response_data_size, + multi_play_start_response_data_size=self.multi_play_start_response_data_size, + + appearance_player_trace_data_list_size=self.appearance_player_trace_data_list_size, + + user_quest_scene_player_trace_id_size=len(self.user_quest_scene_player_trace_id) * 2, + user_quest_scene_player_trace_id=[ord(x) for x in self.user_quest_scene_player_trace_id], + nick_name_size=len(self.nick_name) * 2, + nick_name=[ord(x) for x in self.nick_name], + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayEndRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayEndResponse(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.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 + + 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 + ) + + 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, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): + def __init__(self, cmd) -> 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.common_reward_type_1 = 1 + self.common_reward_id_1 = 102000070 + self.common_reward_num_1 = 1 + + def make(self) -> bytes: + # 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, + ) + + resp_data = 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, + + 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, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): + def __init__(self, cmd, questIdsData) -> None: + super().__init__(cmd) + self.result = 1 + + # quest_scene_user_data_list_size + self.quest_type = [1] * len(questIdsData) + self.quest_scene_id = questIdsData + self.clear_flag = [1] * len(questIdsData) + + # quest_scene_best_score_user_data + self.clear_time = 300 + self.combo_num = 0 + self.total_damage = "0" + self.concurrent_destroying_num = 1 + + # 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]] + + 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 + ) + + quest_scene_best_score_user_data_struct = Struct( + "clear_time" / Int32ub, # big endian + "combo_num" / Int32ub, # big endian + "total_damage_size" / Int32ub, # big endian + "total_damage" / Int16ul[len(self.total_damage)], + "concurrent_destroying_num" / Int16ub, + ) + + quest_scene_user_data_list_struct = Struct( + "quest_type" / Int8ul, # result is either 0 or 1 + "quest_scene_id" / Int16ub, #short + "clear_flag" / Int8ul, # result is either 0 or 1 + "quest_scene_best_score_user_data_size" / Rebuild(Int32ub, len_(this.quest_scene_best_score_user_data)), # big endian + "quest_scene_best_score_user_data" / Array(this.quest_scene_best_score_user_data_size, quest_scene_best_score_user_data_struct), + "quest_scene_ex_bonus_user_data_list_size" / Rebuild(Int32ub, len_(this.quest_scene_ex_bonus_user_data_list)), # big endian + "quest_scene_ex_bonus_user_data_list" / Array(this.quest_scene_ex_bonus_user_data_list_size, quest_scene_ex_bonus_user_data_list_struct), + ) + + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "quest_scene_user_data_list_size" / Rebuild(Int32ub, len_(this.quest_scene_user_data_list)), # big endian + "quest_scene_user_data_list" / Array(this.quest_scene_user_data_list_size, quest_scene_user_data_list_struct), + ) + + resp_data = resp_struct.parse(resp_struct.build(dict( + result=self.result, + quest_scene_user_data_list_size=0, + quest_scene_user_data_list=[], + ))) + + for i in range(len(self.quest_scene_id)): + quest_data = dict( + quest_type=self.quest_type[i], + quest_scene_id=self.quest_scene_id[i], + clear_flag=self.clear_flag[i], + + quest_scene_best_score_user_data_size=0, + quest_scene_best_score_user_data=[], + quest_scene_ex_bonus_user_data_list_size=0, + quest_scene_ex_bonus_user_data_list=[], + ) + + quest_data["quest_scene_best_score_user_data"].append(dict( + clear_time=self.clear_time, + combo_num=self.combo_num, + total_damage_size=len(self.total_damage) * 2, + total_damage=[ord(x) for x in self.total_damage], + concurrent_destroying_num=self.concurrent_destroying_num, + )) + + ''' + quest_data["quest_scene_ex_bonus_user_data_list"].append(dict( + ex_bonus_table_id=self.ex_bonus_table_id[i], + achievement_flag=self.achievement_flag[i], + )) + ''' + + resp_data.quest_scene_user_data_list.append(quest_data) + + # finally, rebuild the resp_data + resp_data = resp_struct.build(resp_data) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.elapsed_days = 1 + self.get_yui_medal_num = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag" / Int8ul, # result is either 0 or 1 + "elapsed_days" / Int16ub, #short + "get_yui_medal_num" / Int16ub, #short + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + elapsed_days=self.elapsed_days, + get_yui_medal_num=self.get_yui_medal_num, + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.data_size = 1 # number of arrays + + self.elapsed_days = 1 + self.loop_num = 1 + self.last_check_date = "20230520193000" + self.last_get_date = "20230520193000" + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "data_size" / Int32ub, # big endian + + "elapsed_days" / Int32ub, # big endian + "loop_num" / Int32ub, # big endian + "last_check_date_size" / Int32ub, # big endian + "last_check_date" / Int16ul[len(self.last_check_date)], + "last_get_date_size" / Int32ub, # big endian + "last_get_date" / Int16ul[len(self.last_get_date)], + ) + + resp_data = resp_struct.build(dict( + result=self.result, + data_size=self.data_size, + + elapsed_days=self.elapsed_days, + loop_num=self.loop_num, + last_check_date_size=len(self.last_check_date) * 2, + last_check_date=[ord(x) for x in self.last_check_date], + last_get_date_size=len(self.last_get_date) * 2, + last_get_date=[ord(x) for x in self.last_get_date], + + )) + + self.length = len(resp_data) + return super().make() + resp_data + +class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): + def __init__(self, data: bytes) -> None: + super().__init__(data) + +class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): + def __init__(self, cmd) -> None: + super().__init__(cmd) + self.result = 1 + self.get_flag = 1 + self.used_num = 0 + self.get_vp = 1 + + def make(self) -> bytes: + # create a resp struct + resp_struct = Struct( + "result" / Int8ul, # result is either 0 or 1 + "get_flag" / Int8ul, # result is either 0 or 1 + "used_num" / Int32ub, # big endian + "get_vp" / Int32ub, # big endian + ) + + resp_data = resp_struct.build(dict( + result=self.result, + get_flag=self.get_flag, + used_num=self.used_num, + get_vp=self.get_vp, + + )) + + self.length = len(resp_data) + return super().make() + resp_data \ No newline at end of file diff --git a/titles/sao/index.py b/titles/sao/index.py new file mode 100644 index 0000000..2c903ce --- /dev/null +++ b/titles/sao/index.py @@ -0,0 +1,117 @@ +from typing import Tuple +from twisted.web.http import Request +from twisted.web import resource +import json, ast +from datetime import datetime +import yaml +import logging, coloredlogs +from logging.handlers import TimedRotatingFileHandler +import inflection +from os import path + +from core import CoreConfig, Utils +from titles.sao.config import SaoConfig +from titles.sao.const import SaoConstants +from titles.sao.base import SaoBase +from titles.sao.handlers.base import * + + +class SaoServlet(resource.Resource): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + self.isLeaf = True + self.core_cfg = core_cfg + self.config_dir = cfg_dir + self.game_cfg = SaoConfig() + if path.exists(f"{cfg_dir}/sao.yaml"): + self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/sao.yaml"))) + + self.logger = logging.getLogger("sao") + if not hasattr(self.logger, "inited"): + log_fmt_str = "[%(asctime)s] SAO | %(levelname)s | %(message)s" + log_fmt = logging.Formatter(log_fmt_str) + fileHandler = TimedRotatingFileHandler( + "{0}/{1}.log".format(self.core_cfg.server.log_dir, "sao"), + 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 + ) + self.logger.inited = True + + self.base = SaoBase(core_cfg, self.game_cfg) + + @classmethod + def get_allnet_info( + cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = SaoConfig() + + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "", "") + + return ( + True, + f"http://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", + f"{game_cfg.server.hostname}/SDEW/$v/", + ) + + @classmethod + def get_mucha_info( + cls, core_cfg: CoreConfig, cfg_dir: str + ) -> Tuple[bool, str, str]: + game_cfg = SaoConfig() + + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + + if not game_cfg.server.enable: + return (False, "") + + return (True, "SAO1") + + def setup(self) -> None: + pass + + def render_POST( + self, request: Request, version: int = 0, endpoints: str = "" + ) -> bytes: + req_url = request.uri.decode() + if req_url == "/matching": + self.logger.info("Matching request") + + request.responseHeaders.addRawHeader(b"content-type", b"text/html; charset=utf-8") + + sao_request = request.content.getvalue().hex() + #sao_request = sao_request[:32] + + handler = getattr(self.base, f"handle_{sao_request[:4]}", None) + if handler is None: + self.logger.info(f"Generic Handler for {req_url} - {sao_request[:4]}") + #self.logger.debug(f"Request: {request.content.getvalue().hex()}") + resp = SaoNoopResponse(int.from_bytes(bytes.fromhex(sao_request[:4]), "big")+1) + self.logger.debug(f"Response: {resp.make().hex()}") + return resp.make() + + 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/read.py b/titles/sao/read.py new file mode 100644 index 0000000..5fc9804 --- /dev/null +++ b/titles/sao/read.py @@ -0,0 +1,230 @@ +from typing import Optional, Dict, List +from os import walk, path +import urllib +import csv + +from read import BaseReader +from core.config import CoreConfig +from titles.sao.database import SaoData +from titles.sao.const import SaoConstants + + +class SaoReader(BaseReader): + def __init__( + self, + config: CoreConfig, + version: int, + bin_arg: Optional[str], + opt_arg: Optional[str], + extra: Optional[str], + ) -> None: + super().__init__(config, version, bin_arg, opt_arg, extra) + self.data = SaoData(config) + + try: + self.logger.info( + f"Start importer for {SaoConstants.game_ver_to_string(version)}" + ) + except IndexError: + self.logger.error(f"Invalid project SAO version {version}") + exit(1) + + def read(self) -> None: + pull_bin_ram = True + + if not path.exists(f"{self.bin_dir}"): + self.logger.warn(f"Couldn't find csv file in {self.bin_dir}, skipping") + pull_bin_ram = False + + if pull_bin_ram: + self.read_csv(f"{self.bin_dir}") + + def read_csv(self, bin_dir: str) -> None: + self.logger.info(f"Read csv from {bin_dir}") + + self.logger.info("Now reading QuestScene.csv") + try: + fullPath = bin_dir + "/QuestScene.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + questSceneId = row["QuestSceneId"] + sortNo = row["SortNo"] + name = row["Name"] + enabled = True + + self.logger.info(f"Added quest {questSceneId} | Name: {name}") + + try: + self.data.static.put_quest( + questSceneId, + 0, + sortNo, + name, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading HeroLog.csv") + try: + fullPath = bin_dir + "/HeroLog.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + heroLogId = row["HeroLogId"] + name = row["Name"] + nickname = row["Nickname"] + rarity = row["Rarity"] + skillTableSubId = row["SkillTableSubId"] + awakeningExp = row["AwakeningExp"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added hero {heroLogId} | Name: {name}") + + try: + self.data.static.put_hero( + 0, + heroLogId, + name, + nickname, + rarity, + skillTableSubId, + awakeningExp, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Equipment.csv") + try: + fullPath = bin_dir + "/Equipment.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + equipmentId = row["EquipmentId"] + equipmentType = row["EquipmentType"] + weaponTypeId = row["WeaponTypeId"] + name = row["Name"] + rarity = row["Rarity"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added equipment {equipmentId} | Name: {name}") + + try: + self.data.static.put_equipment( + 0, + equipmentId, + name, + equipmentType, + weaponTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Item.csv") + try: + fullPath = bin_dir + "/Item.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + itemId = row["ItemId"] + itemTypeId = row["ItemTypeId"] + name = row["Name"] + rarity = row["Rarity"] + flavorText = row["FlavorText"] + enabled = True + + self.logger.info(f"Added item {itemId} | Name: {name}") + + try: + self.data.static.put_item( + 0, + itemId, + name, + itemTypeId, + rarity, + flavorText, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading SupportLog.csv") + try: + fullPath = bin_dir + "/SupportLog.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + supportLogId = row["SupportLogId"] + charaId = row["CharaId"] + name = row["Name"] + rarity = row["Rarity"] + salePrice = row["SalePrice"] + skillName = row["SkillName"] + enabled = True + + self.logger.info(f"Added support log {supportLogId} | Name: {name}") + + try: + self.data.static.put_support_log( + 0, + supportLogId, + charaId, + name, + rarity, + salePrice, + skillName, + enabled + ) + except Exception as err: + print(err) + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") + + self.logger.info("Now reading Title.csv") + try: + fullPath = bin_dir + "/Title.csv" + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + titleId = row["TitleId"] + displayName = row["DisplayName"] + requirement = row["Requirement"] + rank = row["Rank"] + imageFilePath = row["ImageFilePath"] + enabled = True + + self.logger.info(f"Added title {titleId} | Name: {displayName}") + + if len(titleId) > 5: + try: + self.data.static.put_title( + 0, + titleId, + displayName, + requirement, + rank, + imageFilePath, + enabled + ) + except Exception as err: + print(err) + elif len(titleId) < 6: # current server code cannot have multiple lengths for the id + continue + except: + self.logger.warn(f"Couldn't read csv file in {self.bin_dir}, skipping") diff --git a/titles/sao/schema/__init__.py b/titles/sao/schema/__init__.py new file mode 100644 index 0000000..3e75fc0 --- /dev/null +++ b/titles/sao/schema/__init__.py @@ -0,0 +1,3 @@ +from .profile import SaoProfileData +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..f6d030e --- /dev/null +++ b/titles/sao/schema/item.py @@ -0,0 +1,212 @@ +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", +) + +sessions = Table( + "sao_play_sessions", + 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("episode_id", Integer, nullable=False), + Column("play_mode", Integer, nullable=False), + Column("quest_drop_boost_apply_flag", Integer, nullable=False), + Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), + UniqueConstraint("user", "user_party_team_id", "play_date", name="sao_play_sessions_uk"), + 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( + user=user_id, + user_party_team_id=user_party_team_id, + episode_id=episode_id, + play_mode=play_mode, + quest_drop_boost_apply_flag=quest_drop_boost_apply_flag + ) + + 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 session for user {user_id}!") + return None + return result.lastrowid + + 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=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, + ) + + 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=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, + ) + + 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() + + def get_session( + self, user_id: int = None + ) -> Optional[List[Row]]: + sql = sessions.select( + and_( + sessions.c.user == user_id, + ) + ).order_by( + sessions.c.play_date.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py new file mode 100644 index 0000000..b125717 --- /dev/null +++ b/titles/sao/schema/profile.py @@ -0,0 +1,80 @@ +from typing import Optional, Dict, List +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case +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 +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata +from ..const import SaoConstants + +profile = Table( + "sao_profile", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + unique=True, + ), + Column("user_type", Integer, server_default="1"), + Column("nick_name", String(16), server_default="PLAYER"), + Column("rank_num", Integer, server_default="1"), + Column("rank_exp", Integer, server_default="0"), + Column("own_col", Integer, server_default="0"), + Column("own_vp", Integer, server_default="0"), + Column("own_yui_medal", Integer, server_default="0"), + Column("setting_title_id", Integer, server_default="20005"), +) + +class SaoProfileData(BaseData): + def create_profile(self, user_id: int) -> Optional[int]: + sql = insert(profile).values(user=user_id) + 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 profile for user {user_id}!") + return None + return result.lastrowid + + def put_profile(self, user_id: int, user_type: int, nick_name: str, rank_num: int, rank_exp: int, own_col: int, own_vp: int, own_yui_medal: int, setting_title_id: int) -> Optional[int]: + sql = insert(profile).values( + user=user_id, + user_type=user_type, + nick_name=nick_name, + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + conflict = sql.on_duplicate_key_update( + rank_num=rank_num, + rank_exp=rank_exp, + own_col=own_col, + own_vp=own_vp, + own_yui_medal=own_yui_medal, + setting_title_id=setting_title_id + ) + + result = self.execute(conflict) + if result is None: + self.logger.error( + f"{__name__} failed to insert profile! user: {user_id}" + ) + return None + + print(result.lastrowid) + return result.lastrowid + + def get_profile(self, user_id: int) -> Optional[Row]: + sql = profile.select(profile.c.user == user_id) + result = self.execute(sql) + if result is None: + return None + return result.fetchone() \ No newline at end of file diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py new file mode 100644 index 0000000..9e96aaf --- /dev/null +++ b/titles/sao/schema/static.py @@ -0,0 +1,297 @@ +from typing import Dict, List, Optional +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float +from sqlalchemy.engine.base import Connection +from sqlalchemy.engine import Row +from sqlalchemy.schema import ForeignKey +from sqlalchemy.sql import func, select +from sqlalchemy.dialects.mysql import insert + +from core.data.schema import BaseData, metadata + +quest = Table( + "sao_static_quest", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("questSceneId", Integer), + Column("sortNo", Integer), + Column("name", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "questSceneId", name="sao_static_quest_uk" + ), + mysql_charset="utf8mb4", +) + +hero = Table( + "sao_static_hero_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("heroLogId", Integer), + Column("name", String(255)), + Column("nickname", String(255)), + Column("rarity", Integer), + Column("skillTableSubId", Integer), + Column("awakeningExp", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), + mysql_charset="utf8mb4", +) + +equipment = Table( + "sao_static_equipment_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("equipmentId", Integer), + Column("equipmentType", Integer), + Column("weaponTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), + mysql_charset="utf8mb4", +) + +item = Table( + "sao_static_item_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("itemId", Integer), + Column("itemTypeId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("flavorText", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), + mysql_charset="utf8mb4", +) + +support = Table( + "sao_static_support_log_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("supportLogId", Integer), + Column("charaId", Integer), + Column("name", String(255)), + Column("rarity", Integer), + Column("salePrice", Integer), + Column("skillName", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "supportLogId", name="sao_static_support_log_list_uk" + ), + mysql_charset="utf8mb4", +) + +title = Table( + "sao_static_title_list", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("version", Integer), + Column("titleId", Integer), + Column("displayName", String(255)), + Column("requirement", Integer), + Column("rank", Integer), + Column("imageFilePath", String(255)), + Column("enabled", Boolean), + UniqueConstraint( + "version", "titleId", name="sao_static_title_list_uk" + ), + mysql_charset="utf8mb4", +) + +class SaoStaticData(BaseData): + def put_quest( self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool ) -> Optional[int]: + sql = insert(quest).values( + questSceneId=questSceneId, + version=version, + sortNo=sortNo, + name=name, + tutorial=tutorial, + ) + + conflict = sql.on_duplicate_key_update( + name=name, questSceneId=questSceneId, version=version + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_hero( self, version: int, heroLogId: int, name: str, nickname: str, rarity: int, skillTableSubId: int, awakeningExp: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(hero).values( + version=version, + heroLogId=heroLogId, + name=name, + nickname=nickname, + rarity=rarity, + skillTableSubId=skillTableSubId, + awakeningExp=awakeningExp, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, heroLogId=heroLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_equipment( self, version: int, equipmentId: int, name: str, equipmentType: int, weaponTypeId:int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(equipment).values( + version=version, + equipmentId=equipmentId, + name=name, + equipmentType=equipmentType, + weaponTypeId=weaponTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, equipmentId=equipmentId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_item( self, version: int, itemId: int, name: str, itemTypeId: int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + sql = insert(item).values( + version=version, + itemId=itemId, + name=name, + itemTypeId=itemTypeId, + rarity=rarity, + flavorText=flavorText, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, itemId=itemId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_support_log( self, version: int, supportLogId: int, charaId: int, name: str, rarity: int, salePrice: int, skillName: str, enabled: bool ) -> Optional[int]: + sql = insert(support).values( + version=version, + supportLogId=supportLogId, + charaId=charaId, + name=name, + rarity=rarity, + salePrice=salePrice, + skillName=skillName, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + name=name, supportLogId=supportLogId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]: + sql = insert(title).values( + version=version, + titleId=titleId, + displayName=displayName, + requirement=requirement, + rank=rank, + imageFilePath=imageFilePath, + enabled=enabled + ) + + conflict = sql.on_duplicate_key_update( + displayName=displayName, titleId=titleId + ) + + result = self.execute(conflict) + if result is None: + return None + return result.lastrowid + + def get_quests_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = quest.select(quest.c.version == version and quest.c.enabled == enabled).order_by( + quest.c.questSceneId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_hero_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = hero.select(hero.c.version == version and hero.c.enabled == enabled).order_by( + hero.c.heroLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_equipment_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = equipment.select(equipment.c.version == version and equipment.c.enabled == enabled).order_by( + equipment.c.equipmentId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_item_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = item.select(item.c.version == version and item.c.enabled == enabled).order_by( + item.c.itemId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_support_log_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = support.select(support.c.version == version and support.c.enabled == enabled).order_by( + support.c.supportLogId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] + + def get_title_ids(self, version: int, enabled: bool) -> Optional[List[Dict]]: + sql = title.select(title.c.version == version and title.c.enabled == enabled).order_by( + title.c.titleId.asc() + ) + + result = self.execute(sql) + if result is None: + return None + return [list[2] for list in result.fetchall()] \ No newline at end of file diff --git a/titles/wacca/base.py b/titles/wacca/base.py index ada40c6..eeafe4c 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -624,10 +624,10 @@ class WaccaBase: current_wp = profile["wp"] tickets = self.data.item.get_tickets(user_id) - new_tickets = [] + new_tickets: List[TicketItem] = [] for ticket in tickets: - new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999]) + new_tickets.append(TicketItem(ticket["id"], ticket["ticket_id"], 9999999999)) for item in req.itemsUsed: if ( @@ -645,11 +645,11 @@ class WaccaBase: and not self.game_config.mods.infinite_tickets ): for x in range(len(new_tickets)): - if new_tickets[x][1] == item.itemId: + if new_tickets[x].ticketId == item.itemId: self.logger.debug( - f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}" + f"Remove ticket ID {new_tickets[x].userTicketId} type {new_tickets[x].ticketId} from {user_id}" ) - self.data.item.spend_ticket(new_tickets[x][0]) + self.data.item.spend_ticket(new_tickets[x].userTicketId) new_tickets.pop(x) break diff --git a/titles/wacca/frontend.py b/titles/wacca/frontend.py index 69ab1ee..cc40644 100644 --- a/titles/wacca/frontend.py +++ b/titles/wacca/frontend.py @@ -2,8 +2,9 @@ import yaml import jinja2 from twisted.web.http import Request from os import path +from twisted.web.server import Session -from core.frontend import FE_Base +from core.frontend import FE_Base, IUserSession from core.config import CoreConfig from titles.wacca.database import WaccaData from titles.wacca.config import WaccaConfig @@ -27,7 +28,11 @@ class WaccaFrontend(FE_Base): template = self.environment.get_template( "titles/wacca/frontend/wacca_index.jinja" ) + sesh: Session = request.getSession() + usr_sesh = IUserSession(sesh) + return template.render( title=f"{self.core_config.server.name} | {self.nav_name}", game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh) ).encode("utf-16") diff --git a/titles/wacca/handlers/user_music.py b/titles/wacca/handlers/user_music.py index a8c80bf..26c2167 100644 --- a/titles/wacca/handlers/user_music.py +++ b/titles/wacca/handlers/user_music.py @@ -93,13 +93,10 @@ class UserMusicUnlockRequest(BaseRequest): class UserMusicUnlockResponse(BaseResponse): - def __init__(self, current_wp: int = 0, tickets_remaining: List = []) -> None: + def __init__(self, current_wp: int = 0, tickets_remaining: List[TicketItem] = []) -> 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])) + self.tickets = tickets_remaining def make(self) -> Dict: tickets = []