diff --git a/changelog.md b/changelog.md index 42c7cfe..17fef90 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,41 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240628 +### maimai ++ Add present support + +## 20240627 +### SAO ++ Fix ghost items, character and player XP, EX Bonuses, unlocks, and much much more + +## 20240620 +### CHUNITHM ++ CHUNITHM LUMINOUS support + +## 20240616 +### CHUNITHM ++ Support network encryption for Export/International versions + +### DIVA ++ Working frontend with name and level strings edit and playlog + +## 20240530 +### DIVA ++ Fix reader for when dificulty is not a int + +## 20240526 +### DIVA ++ Fixed missing awaits causing coroutine error + +## 20240524 +### DIVA ++ Fixed new profile start request causing coroutine error + +## 20240523 +### DIVA ++ Fixed binary handler & render_POST errors + ## 20240408 ### System + Modified the game specific documentation diff --git a/contributing.md b/contributing.md index 5397f70..d12425a 100644 --- a/contributing.md +++ b/contributing.md @@ -1,8 +1,182 @@ # Contributing to ARTEMiS -If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected. +If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). This guide assume you're familiar with both git, python, and the libraries that artemis uses. + +This document is a work in progress. If you have any questions or notice any errors, please report it to the discord. ## Adding games -Guide WIP +### Step 0 ++ Follow the "n-1" rule of thumb. PRs for game versions that are currently active in arcades will be deleted. If you're unsure, ask! ++ Always PR against the `develop` branch. ++ Check to see if somebody else is already PRing the features/games you want to add. If they are, consider contributing to them rather then making an entirely new PR. ++ We don't technically have a written code style guide (TODO) but try to keep your code consistant with code that's already there where possible. + +### Step 1 (Setup) +1) Fork the gitea repo, clone your fork, and checkout the develop branch. +2) Make a new folder in the `titles` folder, name it some recogniseable shorthand for your game (Chunithm becomes chuni, maimai dx is mai2, etc) +3) In this new folder, create a file named `__init__.py`. This is the first thing that will load when your title module is loaded by the core system, and it acts as sort of a directory for where everything lives in your module. This file will contain the following required items: + + `index`: must point to a subclass of `BaseServlet` that will handle setup and dispatching of your game. + + `game_codes`: must be a list of 4 letter SEGA game codes as strings. + + It can also contain the following optional fields: + + `database`: points to a subclass of `Data` that contains one or more subclasses of `BaseData` that act as database transaction handlers. Required for the class to store and retrieve data from the database. + + `reader`: points to a subclass of `BaseReader` that handles importing static data from game files into the database. + + `frontend`: points to a subclass of `FE_Base` that handles frontend routes for your game. + + The next step will focus on `index` + +### Step 2 (Index) +1) Create another file in your game's folder. By convention, it should be called `index.py`. + +2) Inside `index.py`, add the following code, replacing {Game name here} with the name of your game, without spaces or special characters. Look at other titles for examples. +```py +from core.title import BaseServlet +from core import CoreConfig + +class {Game name here}Servlet(BaseServlet): + def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: + pass +``` +3) The `__init__` function should acomplish the following: + + Reading your game's config + + Setting up your games logger + + Instancing your games versions + + It's usually safe to copy and paste the `__init__` functions from other games, just make sure you change everything that needs to be changed! + +4) Go back to the `__init__.py` that you created and add the following: +```py +from .index import {Game name here}Servlet + +index = {Game name here}Servlet +``` + +5) Going back to `index.py`, within the Servlet class, define the following functions from `BaseServlet` as needed (see function documentation): + + `is_game_enabled`: Returns true if the game is enabled and should be served, false otherwise. Returns false by default, so override this to allow your game to be served. + + `get_routes`: Returns a list of Starlette routes that your game will serve. + + `get_allnet_info`: Returns a tuple of strings where the first is the allnet uri and the second is the allnet host. The function takes the game ID, version and keychip ID as parameters, so you can send different responses if need be. + + `get_mucha_info`: Only used by games that use Mucha as authentication. Returns a tuple where the first is a bool that is weather or not the game is enabled, the 2nd is a list of game CDs as strings that this servlet should handle, and the 3rd is a list of netID prefixes that each game CD should use. If your game does not use mucha, do not define this function. + + `setup`: Preforms any setup your servlet requires, such as spinning up matching servers. It is run once when the server starts. If you don't need any setup, do not define. + +6) Make sure any functions you specify to handle routes in `get_routes` are defined as async, as follows: `async def handle_thing(self, request: Request) -> Response:` where Response is whatever kind of Response class you'll be returning. Make sure all paths in this function return some subclass of Response, otherwise you'll get an error when serving. + +### Step 3 (Constants) +1) In your game's folder, create a file to store static values for your game. By convention, we call this `const.py` + +2) Inside, create a class called `{Game name here}Constants`. Do not define an `__init__` function. + +3) Put constants related to your game here. A good example of something to put here is game codes. +```py +class {Game name here}Constants: + GAME_CODE = "SBXX" + CONFIG_NAME = "{game name}.yaml" +``` + +4) If you choose to put game codes in here, add this to your `__init__.py` file: +```py +from .const import {Game name here}Constants +... +game_codes = [{Game name here}Constants.GAME_CODE] +``` + +### Step 4 (Config) +1) Make a file to store your game's config. By convention, it should be called `config.py` + +2) Inside that file, add the following: +```py +from core.config import CoreConfig + +class {game name}ServerConfig: + def __init__(self, parent_config: "{game name}Config") -> None: + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "{game name}", "server", "enable", default=True + ) + + @property + def loglevel(self) -> int: + return CoreConfig.str_to_loglevel( + CoreConfig.get_config_field( + self.__config, "{game name}", "server", "loglevel", default="info" + ) + ) + +class {game name}Config(dict): + def __init__(self) -> None: + self.server = {game name}ServerConfig(self) +``` + +3) In the `example_config` folder, create a yaml file for your game. By convention, it should be called `{game folder name}.ymal`. Add the following: +```yaml +server: + enable: True + loglevel: "info" +``` + +4) Add any additional config options that you feel the game needs. Look to other games for config examples. + +5) In `index.py` import your config and instance it in `__init__` with: +```py +self.game_cfg = {game folder name}Config() +if path.exists(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}")) + ) +``` +This will attempt to load the config file you specified in your constants, and if not, go with the defaults specified in `config.py`. This game_cfg object can then be passed down to your handlers when you create them. + +At this stage your game should be loaded by allnet, and serve whatever routes you put in `get_routes`. See the next section about adding versions and handlers. + +### Step 5 (Database) +TODO + +### Step 6 (Frontend) +TODO + +### Step 7 (Reader) +TODO ## Adding game versions -Guide WIP \ No newline at end of file +See the above section about code expectations and how to PR. +1) In the game's folder, create a python file to contain the version handlers. By convention, the first version is version 0, and is stored in `base.py`. Versions following that increment the version number, and are stored in `{short version name}.py`. See Wacca's folder for an example of how to name versions. + +2) Internal version numbers should be defined in `const.py`. The version should change any time the game gets a major update (i.e. a new version or plus version.) +```py +# in const.py +VERSION_{game name} = 0 +VERSION_{game name}_PLUS = 1 +``` + +3) Inside `base.py` (or whatever your version is named) add the following: +```py +class {game name}Base: + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + self.game_config = game_cfg + self.core_config = cfg + self.version = {game name}Constants.VERSION_{game name} + self.data = {game name}Data(cfg) + # Any other initialization stuff +``` + +4) Define your handlers. This will vary wildly by game, but best practice is to keep the naming consistant, so that the main dispatch function in `index.py` can use `getattr` to get the handler, rather then having a static list of what endpoint or request type goes to which handler. See Wacca's `index.py` and `base.py` for examples of how to do this. + +5) If your version is not the base version, make sure it inherits from the base version: +```py +class {game name}Plus({game name}Base): + def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None: + super().__init__(cfg, game_cfg) + self.version = {game name}Constants.VERSION_{game name}_PLUS +``` + +6) Back in `index.py` make sure to import your new class, and add it to `__init__`. Some games may opt to just a single list called `self.versions` that contains all the version classes at their internal version's index. Others may simply define them as seperate members. See Wacca for an example of `self.versions` + +7) Add your version to your game's dispatching logic. + +8) Test to make sure your game is being handled properly. + +9) Submit a PR. + +## Adding/improving core services +If you intend to submit improvements or additions to core services (allnet, mucha, billing, aimedb, database, etc) please get in touch with a maintainer. diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py index 479e84d..22c9f05 100644 --- a/core/adb_handlers/felica.py +++ b/core/adb_handlers/felica.py @@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest): self.pmm = hex(pmm)[2:].upper() class ADBFelicaLookupResponse(ADBBaseResponse): - def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: + def __init__(self, access_code: str = None, idx: int = 0, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: super().__init__(code, length, status, game_id, store_id, keychip_id) self.access_code = access_code if access_code is not None else "00000000000000000000" + self.idx = idx @classmethod - def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse": - c = cls(access_code, req.game_id, req.store_id, req.keychip_id) + def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse": + c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id) c.head.protocol_ver = req.protocol_ver return c @@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse): "access_code" / Int8ub[10], Padding(2) ).build(dict( - felica_idx = 0, + felica_idx = self.idx, access_code = bytes.fromhex(self.access_code) )) diff --git a/core/aimedb.py b/core/aimedb.py index 08a1b65..88796c7 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -194,6 +194,9 @@ class AimedbServlette(): if user_id and user_id > 0: await self.data.card.update_card_last_login(req.access_code) + if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number:08X} for access code {req.access_code}") return ret async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: @@ -229,23 +232,34 @@ class AimedbServlette(): async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: """ - On official, I think a card has to be registered for this to actually work, but - I'm making the executive decision to not implement that and just kick back our - faux generated access code. The real felica IDm -> access code conversion is done - on the ADB server, which we do not and will not ever have access to. Because we can - assure that all IDms will be unique, this basic 0-padded hex -> int conversion will - be fine. + On official, the IDm is used as a key to look up the stored access code in a large + database. We do not have access to that database so we have to make due with what we got. + Interestingly, namco games are able to read S_PAD0 and send the server the correct access + code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans + on a game that reads it correctly from the card, this will have to do. It's the same conversion + used on the big boy networks. """ req = ADBFelicaLookupRequest(data) - ac = self.data.card.to_access_code(req.idm) + idm = req.idm.zfill(16) + card = await self.data.card.get_card_by_idm(idm) + if not card: + ac = self.data.card.to_access_code(idm) + test = await self.data.card.get_card_by_access_code(ac) + if test: + await self.data.card.set_idm_by_access_code(ac, idm) + + else: + ac = card['access_code'] + self.logger.info( - f"idm {req.idm} ipm {req.pmm} -> access_code {ac}" + f"idm {idm} ipm {req.pmm.zfill(16)} -> access_code {ac}" ) return ADBFelicaLookupResponse.from_req(req.head, ac) async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: """ - I've never seen this used. + Used to register felica moble access codes. Will never be used on our network + because we don't implement felica_lookup properly. """ req = ADBFelicaLookupRequest(data) ac = self.data.card.to_access_code(req.idm) @@ -279,17 +293,36 @@ class AimedbServlette(): async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBFelicaLookup2Request(data) - access_code = self.data.card.to_access_code(req.idm) - user_id = await self.data.card.get_user_id_from_card(access_code=access_code) + user_id = None + idm = req.idm.zfill(16) + card = await self.data.card.get_card_by_idm(idm) + if not card: + access_code = self.data.card.to_access_code(idm) + card = await self.data.card.get_card_by_access_code(access_code) + if card: + user_id = card['user'] + await self.data.card.set_idm_by_access_code(access_code, idm) + + else: + user_id = card['user'] + access_code = card['access_code'] if user_id is None: user_id = -1 self.logger.info( - f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" + f"idm {idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" ) resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) + + if user_id > 0: + if card['is_banned'] and card['is_locked']: + resp.head.status = ADBStatus.BAN_SYS_USER + elif card['is_banned']: + resp.head.status = ADBStatus.BAN_SYS + elif card['is_locked']: + resp.head.status = ADBStatus.LOCK_USER if user_id and user_id > 0 and self.config.aimedb.id_secret: auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds) @@ -337,6 +370,16 @@ class AimedbServlette(): self.logger.info( f"Registration blocked!: access code {req.access_code}" ) + + if user_id > 0: + if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn: + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}") + + elif req.access_code.startswith("0008"): + idm = self.data.card.to_idm(req.access_code) + await self.data.card.set_idm_by_access_code(req.access_code, idm) + self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}") resp = ADBLookupResponse.from_req(req.head, user_id) if resp.user_id <= 0: diff --git a/core/allnet.py b/core/allnet.py index b03d168..9eb6595 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -171,7 +171,7 @@ class AllnetServlet: if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." await self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg + "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver ) self.logger.warning(msg) @@ -183,9 +183,9 @@ class AllnetServlet: arcade = await self.data.arcade.get_arcade(machine["arcade"]) if self.config.server.check_arcade_ip: if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip: - msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." + msg = f"{req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." await self.data.base.log_event( - "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg + "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver ) self.logger.warning(msg) @@ -194,9 +194,9 @@ class AllnetServlet: return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking: - msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." + msg = f"{req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." await self.data.base.log_event( - "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg + "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver ) self.logger.warning(msg) @@ -204,7 +204,17 @@ class AllnetServlet: resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") + if machine['game'] and machine['game'] != req.game_id: + msg = f"{req.serial} attempted allnet auth with bad game ID {req.game_id} (expected {machine['game']})." + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_BAD_GAME", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver + ) + self.logger.warning(msg) + resp.stat = ALLNET_STAT.bad_game.value + resp_dict = {k: v for k, v in vars(resp).items() if v is not None} + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") + country = ( arcade["country"] if machine["country"] is None else machine["country"] ) @@ -236,11 +246,14 @@ class AllnetServlet: arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00" ) + else: + arcade = None + if req.game_id not in TitleServlet.title_registry: if not self.config.server.is_develop: msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." await self.data.base.log_event( - "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg + "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg, {}, None, arcade['id'] if arcade else None, machine['id'] if machine else None, request_ip, req.game_id, req.ver ) self.logger.warning(msg) @@ -271,8 +284,17 @@ class AllnetServlet: resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") - msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" - await self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) + if machine and arcade: + msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver + ) + else: + msg = f"Allow unregistered serial {req.serial} to authenticate from {request_ip}: {req.game_id} v{req.ver}" + await self.data.base.log_event( + "allnet", "ALLNET_AUTH_SUCCESS_UNREG", logging.INFO, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver + ) + self.logger.info(msg) resp_dict = {k: v for k, v in vars(resp).items() if v is not None} @@ -329,7 +351,11 @@ class AllnetServlet: ): return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") - else: # TODO: Keychip check + else: + machine = await self.data.arcade.get_machine(req.serial) + if not machine or not machine['ota_enable'] or not machine['is_cab'] or machine['is_blacklisted']: + return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") + if path.exists( f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" ): @@ -340,8 +366,13 @@ class AllnetServlet: ): resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" - self.logger.debug(f"Sending download uri {resp.uri}") - await self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}") + if resp.uri: + self.logger.info(f"Sending download uri {resp.uri}") + await self.data.base.log_event( + "allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"Send download URI to {req.serial} for {req.game_id} v{req.ver} from {Utils.get_ip_addr(request)}", {"uri": resp.uri}, None, + machine['arcade'], machine['id'], request_ip, req.game_id, req.ver + ) + # Maybe add a log event for checkin but no url sent? res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n" @@ -357,13 +388,16 @@ class AllnetServlet: async def handle_dlorder_ini(self, request: Request) -> bytes: req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "") + request_ip = Utils.get_ip_addr(request) if not req_file: return PlainTextResponse(status_code=404) if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): - self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful") - await self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}") + self.logger.info(f"Request for DL INI file {req_file} from {request_ip} successful") + await self.data.base.log_event( + "allnet", "DLORDER_INI_SENT", logging.INFO, f"{request_ip} successfully recieved {req_file}", {"file": req_file}, ip=request_ip + ) return PlainTextResponse(open( f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8" @@ -401,7 +435,13 @@ class AllnetServlet: msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\ f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete." - await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data) + machine = await self.data.arcade.get_machine(rep.serial) + if machine: + await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data, None, machine['arcade'], machine['id'], client_ip, rep.gd, rep.dav) + + else: + msg = "Unknown serial " + msg + await self.data.base.log_event("allnet", "DL_REPORT_UNREG", logging.INFO, msg, dl_data, None, None, None, client_ip, rep.gd, rep.dav) self.logger.info(msg) return PlainTextResponse("OK") @@ -421,14 +461,24 @@ class AllnetServlet: if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: return PlainTextResponse("NG") - self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})") + msg = f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})" + machine = await self.data.arcade.get_machine(serial) + if machine: + await self.data.base.log_event("allnet", "LSR_REPORT", logging.INFO, msg, req_dict, None, machine['arcade'], machine['id'], ip) + + else: + msg = "Unregistered " + msg + await self.data.base.log_event("allnet", "LSR_REPORT_UNREG", logging.INFO, msg, req_dict, None, None, None, ip) + + self.logger.info(msg) return PlainTextResponse("OK") async def handle_alive(self, request: Request) -> bytes: return PlainTextResponse("OK") async def handle_naomitest(self, request: Request) -> bytes: - self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") + # This could be spam-able, removing + #self.logger.info(f"Ping from {Utils.get_ip_addr(request)}") return PlainTextResponse("naomi ok") def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: @@ -558,18 +608,35 @@ class BillingServlet: if machine is None and not self.config.server.allow_unregistered_serials: msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}." await self.data.base.log_event( - "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg + "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever ) self.logger.warning(msg) return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n") + + log_details = { + "playcount": req.playcnt, + "billing_type": req.billingtype.name, + "nearfull": req.nearfull, + "playlimit": req.playlimit, + } + + if machine is not None: + await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, "", log_details, None, machine['arcade'], machine['id'], request_ip, req.gameid, req.gamever) + + self.logger.info( + f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " + f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" + ) + else: + log_details['serial'] = req.keychipid + await self.data.base.log_event("billing", "BILLING_CHECKIN_OK_UNREG", logging.INFO, "", log_details, None, None, None, request_ip, req.gameid, req.gamever) + + self.logger.info( + f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " + f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" + ) - msg = ( - f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount " - f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}" - ) - self.logger.info(msg) - await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg) if req.traceleft > 0: self.logger.warn(f"{req.traceleft} unsent tracelogs") kc_playlimit = req.playlimit @@ -893,7 +960,7 @@ class DLReport: return True -cfg_dir = environ.get("DIANA_CFG_DIR", "config") +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg: CoreConfig = CoreConfig() if path.exists(f"{cfg_dir}/core.yaml"): cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) diff --git a/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py new file mode 100644 index 0000000..5fcd5a8 --- /dev/null +++ b/core/data/alembic/versions/1e150d16ab6b_chuni_add_net_battle_uk.py @@ -0,0 +1,27 @@ +"""chuni_add_net_battle_uk + +Revision ID: 1e150d16ab6b +Revises: b23f985100ba +Create Date: 2024-06-21 22:57:18.418488 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '1e150d16ab6b' +down_revision = 'b23f985100ba' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, 'chuni_profile_net_battle', ['user']) + # ### end Alembic commands ### + + +def downgrade(): + op.drop_constraint(None, 'chuni_profile_net_battle', type_='unique') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py b/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py new file mode 100644 index 0000000..5c2492d --- /dev/null +++ b/core/data/alembic/versions/2bf9f38d9444_add_event_log_info.py @@ -0,0 +1,48 @@ +"""add_event_log_info + +Revision ID: 2bf9f38d9444 +Revises: 81e44dd6047a +Create Date: 2024-05-21 23:00:17.468407 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '2bf9f38d9444' +down_revision = '81e44dd6047a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event_log', sa.Column('user', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('arcade', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('machine', sa.INTEGER(), nullable=True)) + op.add_column('event_log', sa.Column('ip', sa.TEXT(length=39), nullable=True)) + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.create_foreign_key(None, 'event_log', 'machine', ['machine'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'event_log', 'arcade', ['arcade'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'event_log', 'aime_user', ['user'], ['id'], onupdate='cascade', ondelete='cascade') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.drop_constraint(None, 'event_log', type_='foreignkey') + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('current_timestamp()'), + existing_nullable=False) + op.drop_column('event_log', 'ip') + op.drop_column('event_log', 'machine') + op.drop_column('event_log', 'arcade') + op.drop_column('event_log', 'user') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py b/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py new file mode 100644 index 0000000..b947763 --- /dev/null +++ b/core/data/alembic/versions/2d024cf145a1_add_event_log_game_version.py @@ -0,0 +1,46 @@ +"""add_event_log_game_version + +Revision ID: 2d024cf145a1 +Revises: 2bf9f38d9444 +Create Date: 2024-05-21 23:41:31.445331 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '2d024cf145a1' +down_revision = '2bf9f38d9444' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('event_log', sa.Column('game', sa.TEXT(length=4), nullable=True)) + op.add_column('event_log', sa.Column('version', sa.TEXT(length=24), nullable=True)) + op.alter_column('event_log', 'ip', + existing_type=mysql.TINYTEXT(), + type_=sa.TEXT(length=39), + existing_nullable=True) + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('event_log', 'when_logged', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('current_timestamp()'), + existing_nullable=False) + op.alter_column('event_log', 'ip', + existing_type=sa.TEXT(length=39), + type_=mysql.TINYTEXT(), + existing_nullable=True) + op.drop_column('event_log', 'version') + op.drop_column('event_log', 'game') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py b/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py new file mode 100644 index 0000000..2867fb7 --- /dev/null +++ b/core/data/alembic/versions/3657efefc5a4_pokken_fix_pokemon_uk.py @@ -0,0 +1,54 @@ +"""pokken_fix_pokemon_uk + +Revision ID: 3657efefc5a4 +Revises: 4a02e623e5e6 +Create Date: 2024-06-13 23:50:57.611998 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '3657efefc5a4' +down_revision = '4a02e623e5e6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('pokken_pokemon_data', 'char_id', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('pokken_pokemon_data', 'illustration_book_no', + existing_type=mysql.INTEGER(display_width=11), + nullable=False) + op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey') + op.drop_index('pokken_pokemon_data_uk', table_name='pokken_pokemon_data') + op.create_unique_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', ['user', 'illustration_book_no']) + op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id']) + op.alter_column('pokken_profile', 'trainer_name', + existing_type=mysql.VARCHAR(length=16), + type_=sa.String(length=14), + existing_nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('pokken_profile', 'trainer_name', + existing_type=sa.String(length=14), + type_=mysql.VARCHAR(length=16), + existing_nullable=True) + op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey') + op.drop_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', type_='unique') + op.create_index('pokken_pokemon_data_uk', 'pokken_pokemon_data', ['user', 'char_id'], unique=True) + op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id']) + op.alter_column('pokken_pokemon_data', 'illustration_book_no', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('pokken_pokemon_data', 'char_id', + existing_type=mysql.INTEGER(display_width=11), + nullable=False) + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py new file mode 100644 index 0000000..98872e8 --- /dev/null +++ b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py @@ -0,0 +1,50 @@ +"""card_add_idm_chip_id + +Revision ID: 48f4acc43a7e +Revises: 1e150d16ab6b +Create Date: 2024-06-21 23:53:34.369134 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '48f4acc43a7e' +down_revision = '1e150d16ab6b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aime_card', sa.Column('idm', sa.String(length=16), nullable=True)) + op.add_column('aime_card', sa.Column('chip_id', sa.BIGINT(), nullable=True)) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=False) + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=True) + op.create_unique_constraint(None, 'aime_card', ['chip_id']) + op.create_unique_constraint(None, 'aime_card', ['idm']) + op.create_unique_constraint(None, 'aime_card', ['access_code']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("chip_id", 'aime_card', type_='unique') + op.drop_constraint("idm", 'aime_card', type_='unique') + op.drop_constraint("access_code", 'aime_card', type_='unique') + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=True) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=True) + op.drop_column('aime_card', 'chip_id') + op.drop_column('aime_card', 'idm') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py new file mode 100644 index 0000000..d221bb6 --- /dev/null +++ b/core/data/alembic/versions/4a02e623e5e6_mai2_add_favs_rivals.py @@ -0,0 +1,48 @@ +"""mai2_add_favs_rivals + +Revision ID: 4a02e623e5e6 +Revises: 8ad40a6e7be2 +Create Date: 2024-06-08 19:02:43.856395 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '4a02e623e5e6' +down_revision = '8ad40a6e7be2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_favorite_music', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('musicId', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('mai2_user_rival', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('rival', sa.Integer(), nullable=False), + sa.Column('show', sa.Boolean(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_user_rival') + op.drop_table('mai2_item_favorite_music') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/5ea363686347_mai2_presents.py b/core/data/alembic/versions/5ea363686347_mai2_presents.py new file mode 100644 index 0000000..8e07a5c --- /dev/null +++ b/core/data/alembic/versions/5ea363686347_mai2_presents.py @@ -0,0 +1,41 @@ +"""mai2_presents + +Revision ID: 5ea363686347 +Revises: 680789dabab3 +Create Date: 2024-06-28 14:49:07.666879 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '5ea363686347' +down_revision = '680789dabab3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_present', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('version', sa.INTEGER(), nullable=True), + sa.Column('user', sa.Integer(), nullable=True), + sa.Column('itemKind', sa.INTEGER(), nullable=False), + sa.Column('itemId', sa.INTEGER(), nullable=False), + sa.Column('stock', sa.INTEGER(), server_default='1', nullable=False), + sa.Column('startDate', sa.TIMESTAMP(), nullable=True), + sa.Column('endDate', sa.TIMESTAMP(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('version', 'user', 'itemKind', 'itemId', name='mai2_item_present_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_item_present') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/680789dabab3_sao_player_changes.py b/core/data/alembic/versions/680789dabab3_sao_player_changes.py new file mode 100644 index 0000000..3e4aef7 --- /dev/null +++ b/core/data/alembic/versions/680789dabab3_sao_player_changes.py @@ -0,0 +1,295 @@ +"""sao_player_changes + +Revision ID: 680789dabab3 +Revises: a616fd164e40 +Create Date: 2024-06-26 23:19:16.863778 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '680789dabab3' +down_revision = 'a616fd164e40' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('sao_equipment_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_equipment_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) + op.alter_column('sao_equipment_data', 'equipment_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + existing_nullable=False) + op.alter_column('sao_equipment_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_equipment_list', ['equipment_id'], ['EquipmentId'], onupdate='cascade', ondelete='cascade') + op.add_column('sao_hero_log_data', sa.Column('max_level_extend_num', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_awakenable', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('awakening_stage', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('awakening_exp', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) + op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) + op.alter_column('sao_hero_log_data', 'main_weapon', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('sao_hero_log_data', 'sub_equipment', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id', + existing_type=mysql.INTEGER(), + type_=sa.BIGINT(), + nullable=True) + op.alter_column('sao_hero_log_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column("sao_hero_log_data", "user_hero_log_id", + existing_type=sa.Integer(), + new_column_name="hero_log_id", + type_=sa.BIGINT(), + nullable=False) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot1_skill_id = NULL WHERE skill_slot1_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot2_skill_id = NULL WHERE skill_slot2_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot3_skill_id = NULL WHERE skill_slot3_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot4_skill_id = NULL WHERE skill_slot4_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot5_skill_id = NULL WHERE skill_slot5_skill_id = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET main_weapon = NULL WHERE main_weapon = 0;")) + op.execute(sa.text("UPDATE sao_hero_log_data SET sub_equipment = NULL WHERE sub_equipment = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_1 = NULL WHERE user_hero_log_id_1 = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_2 = NULL WHERE user_hero_log_id_2 = 0;")) + op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_3 = NULL WHERE user_hero_log_id_3 = 0;")) + + op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.main_weapon = sao_equipment_data.equipment_id SET sao_hero_log_data.main_weapon = sao_equipment_data.id;")) + op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.sub_equipment = sao_equipment_data.equipment_id SET sao_hero_log_data.sub_equipment = sao_equipment_data.id;")) + + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.id;")) + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.id;")) + op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.id;")) + + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot1_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot5_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot2_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot3_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['main_weapon'], ['id'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot4_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['sub_equipment'], ['id'], onupdate='set null', ondelete='set null') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_hero_list', ['hero_log_id'], ['HeroLogId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_3'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_1'], ['id'], onupdate='cascade', ondelete='cascade') + op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_2'], ['id'], onupdate='cascade', ondelete='cascade') + op.alter_column('sao_item_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column('sao_play_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.add_column('sao_player_quest', sa.Column('quest_type', sa.INTEGER(), server_default='1', nullable=False)) + op.alter_column('sao_player_quest', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + op.alter_column('sao_player_quest', 'episode_id', + existing_type=mysql.INTEGER(), + new_column_name="quest_scene_id", + type_=sa.BIGINT(), + nullable=False) + op.create_foreign_key(None, 'sao_player_quest', 'sao_static_quest', ['quest_scene_id'], ['QuestSceneId'], onupdate='cascade', ondelete='cascade') + op.add_column('sao_profile', sa.Column('my_shop', sa.INTEGER(), nullable=True)) + op.add_column('sao_profile', sa.Column('fav_hero', sa.INTEGER(), nullable=True)) + op.add_column('sao_profile', sa.Column('when_register', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=True)) + op.add_column('sao_profile', sa.Column('last_login_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_yui_medal_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_bonus_yui_medal_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_comeback_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('last_login_bonus_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('ad_confirm_date', sa.TIMESTAMP(), nullable=True)) + op.add_column('sao_profile', sa.Column('login_ct', sa.INTEGER(), server_default='0', nullable=True)) + op.create_foreign_key(None, 'sao_profile', 'sao_hero_log_data', ['fav_hero'], ['id'], onupdate='cascade', ondelete='set null') + + +def downgrade(): + op.drop_constraint("sao_profile_ibfk_2", 'sao_profile', type_='foreignkey') + op.drop_column('sao_profile', 'login_ct') + op.drop_column('sao_profile', 'ad_confirm_date') + op.drop_column('sao_profile', 'last_login_bonus_date') + op.drop_column('sao_profile', 'last_comeback_date') + op.drop_column('sao_profile', 'last_bonus_yui_medal_date') + op.drop_column('sao_profile', 'last_yui_medal_date') + op.drop_column('sao_profile', 'last_login_date') + op.drop_column('sao_profile', 'when_register') + op.drop_column('sao_profile', 'fav_hero') + op.drop_column('sao_profile', 'my_shop') + op.alter_column('sao_player_quest', 'quest_scene_id', + existing_type=mysql.BIGINT(), + new_column_name="episode_id", + type_=sa.INTEGER(), + nullable=False) + op.drop_constraint("sao_player_quest_ibfk_2", 'sao_player_quest', type_='foreignkey') + op.alter_column('sao_player_quest', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_column('sao_player_quest', 'quest_scene_id') + op.drop_column('sao_player_quest', 'quest_type') + op.alter_column('sao_play_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_item_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_constraint("sao_hero_party_ibfk_2", 'sao_hero_party', type_='foreignkey') + op.drop_constraint("sao_hero_party_ibfk_3", 'sao_hero_party', type_='foreignkey') + op.drop_constraint("sao_hero_party_ibfk_4", 'sao_hero_party', type_='foreignkey') + op.alter_column("sao_hero_log_data", "hero_log_id", + existing_type=sa.BIGINT(), + new_column_name="user_hero_log_id", + type_=sa.Integer(), + nullable=False) + op.drop_constraint("sao_hero_log_data_ibfk_2", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_3", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_4", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_5", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_6", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_7", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_8", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_9", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_10", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_11", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_12", 'sao_hero_log_data', type_='foreignkey') + op.drop_constraint("sao_hero_log_data_ibfk_13", 'sao_hero_log_data', type_='foreignkey') + op.alter_column('sao_hero_log_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'sub_equipment', + existing_type=mysql.INTEGER(), + nullable=False) + op.alter_column('sao_hero_log_data', 'main_weapon', + existing_type=mysql.INTEGER(), + nullable=False) + op.drop_column('sao_hero_log_data', 'converted_card_num') + op.drop_column('sao_hero_log_data', 'property4_value2') + op.drop_column('sao_hero_log_data', 'property4_value1') + op.drop_column('sao_hero_log_data', 'property4_property_id') + op.drop_column('sao_hero_log_data', 'property3_value2') + op.drop_column('sao_hero_log_data', 'property3_value1') + op.drop_column('sao_hero_log_data', 'property3_property_id') + op.drop_column('sao_hero_log_data', 'property2_value2') + op.drop_column('sao_hero_log_data', 'property2_value1') + op.drop_column('sao_hero_log_data', 'property2_property_id') + op.drop_column('sao_hero_log_data', 'property1_value2') + op.drop_column('sao_hero_log_data', 'property1_value1') + op.drop_column('sao_hero_log_data', 'property1_property_id') + op.drop_column('sao_hero_log_data', 'is_protect') + op.drop_column('sao_hero_log_data', 'is_shop_purchase') + op.drop_column('sao_hero_log_data', 'awakening_exp') + op.drop_column('sao_hero_log_data', 'awakening_stage') + op.drop_column('sao_hero_log_data', 'is_awakenable') + op.drop_column('sao_hero_log_data', 'max_level_extend_num') + op.drop_constraint("sao_equipment_data_ibfk_2", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_3", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_4", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_5", 'sao_equipment_data', type_='foreignkey') + op.drop_constraint("sao_equipment_data_ibfk_6", 'sao_equipment_data', type_='foreignkey') + op.alter_column('sao_equipment_data', 'get_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.alter_column('sao_equipment_data', 'equipment_id', + existing_type=sa.BIGINT(), + type_=mysql.INTEGER(), + existing_nullable=False) + op.drop_column('sao_equipment_data', 'converted_card_num') + op.drop_column('sao_equipment_data', 'property4_value2') + op.drop_column('sao_equipment_data', 'property4_value1') + op.drop_column('sao_equipment_data', 'property4_property_id') + op.drop_column('sao_equipment_data', 'property3_value2') + op.drop_column('sao_equipment_data', 'property3_value1') + op.drop_column('sao_equipment_data', 'property3_property_id') + op.drop_column('sao_equipment_data', 'property2_value2') + op.drop_column('sao_equipment_data', 'property2_value1') + op.drop_column('sao_equipment_data', 'property2_property_id') + op.drop_column('sao_equipment_data', 'property1_value2') + op.drop_column('sao_equipment_data', 'property1_value1') + op.drop_column('sao_equipment_data', 'property1_property_id') + op.drop_column('sao_equipment_data', 'is_protect') + op.drop_column('sao_equipment_data', 'is_shop_purchase') diff --git a/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py b/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py new file mode 100644 index 0000000..abde273 --- /dev/null +++ b/core/data/alembic/versions/7dc13e364e53_cxb_add_playlog_grade.py @@ -0,0 +1,28 @@ +"""cxb_add_playlog_grade + +Revision ID: 7dc13e364e53 +Revises: 2d024cf145a1 +Create Date: 2024-05-28 22:31:22.264926 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '7dc13e364e53' +down_revision = '2d024cf145a1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('cxb_playlog', sa.Column('grade', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('cxb_playlog', 'grade') + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py index 04d2217..0a2267c 100644 --- a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py +++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py @@ -1,7 +1,7 @@ """mai2_buddies_support Revision ID: 81e44dd6047a -Revises: d8950c7ce2fc +Revises: 6a7e8277763b Create Date: 2024-03-12 19:10:37.063907 """ diff --git a/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py new file mode 100644 index 0000000..8b6ec51 --- /dev/null +++ b/core/data/alembic/versions/8ad40a6e7be2_ongeki_fix_clearstatus.py @@ -0,0 +1,30 @@ +"""ongeki: fix clearStatus + +Revision ID: 8ad40a6e7be2 +Revises: 7dc13e364e53 +Create Date: 2024-05-29 19:03:30.062157 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '8ad40a6e7be2' +down_revision = '7dc13e364e53' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=mysql.TINYINT(display_width=1), + type_=sa.Integer(), + existing_nullable=False) + + +def downgrade(): + op.alter_column('ongeki_score_best', 'clearStatus', + existing_type=sa.Integer(), + type_=mysql.TINYINT(display_width=1), + existing_nullable=False) diff --git a/core/data/alembic/versions/a616fd164e40_sao_backport.py b/core/data/alembic/versions/a616fd164e40_sao_backport.py new file mode 100644 index 0000000..5228e0b --- /dev/null +++ b/core/data/alembic/versions/a616fd164e40_sao_backport.py @@ -0,0 +1,437 @@ +"""sao_backport + +Revision ID: a616fd164e40 +Revises: 48f4acc43a7e +Create Date: 2024-06-24 20:28:34.471282 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'a616fd164e40' +down_revision = '48f4acc43a7e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sao_static_quest') + op.create_table('sao_static_quest', + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.Column('SortNo', sa.INTEGER(), nullable=False), + sa.Column('Tutorial', sa.BOOLEAN(), nullable=False), + sa.Column('ColRate', sa.DECIMAL(), nullable=False), + sa.Column('LimitDefault', sa.INTEGER(), nullable=False), + sa.Column('LimitResurrection', sa.INTEGER(), nullable=False), + sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False), + sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False), + sa.Column('SuccessPlayerExp', sa.INTEGER(), nullable=False), + sa.Column('FailedPlayerExp', sa.INTEGER(), nullable=False), + sa.Column('PairExpRate', sa.INTEGER(), nullable=False), + sa.Column('TrioExpRate', sa.INTEGER(), nullable=False), + sa.Column('SingleRewardVp', sa.INTEGER(), nullable=False), + sa.Column('PairRewardVp', sa.INTEGER(), nullable=False), + sa.Column('TrioRewardVp', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('QuestSceneId'), + mysql_charset='utf8mb4' + ) + + op.create_table('sao_static_property', + sa.Column('PropertyId', sa.BIGINT(), nullable=False), + sa.Column('PropertyTargetType', sa.INTEGER(), nullable=False), + sa.Column('PropertyName', sa.VARCHAR(length=255), nullable=False), + sa.Column('PropertyName_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('PropertyNameFormat', sa.VARCHAR(length=255), nullable=False), + sa.Column('PropertyNameFormat_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('PropertyTypeId', sa.INTEGER(), nullable=False), + sa.Column('Value1Min', sa.INTEGER(), nullable=False), + sa.Column('Value1Max', sa.INTEGER(), nullable=False), + sa.Column('Value2Min', sa.INTEGER(), nullable=False), + sa.Column('Value2Max', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('PropertyId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_reward', + sa.Column('RewardTableId', sa.BIGINT(), nullable=False), + sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False), + sa.Column('UnanalyzedLogGradeId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('StrengthMin', sa.INTEGER(), nullable=False), + sa.Column('StrengthMax', sa.INTEGER(), nullable=False), + sa.Column('PropertyTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestInfoDisplayFlag', sa.BOOLEAN(), nullable=False), + sa.Column('Rate', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('RewardTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_skill', + sa.Column('SkillId', sa.BIGINT(), nullable=False), + sa.Column('WeaponTypeId', sa.INTEGER(), nullable=False), + sa.Column('Name', sa.VARCHAR(length=255), nullable=False), + sa.Column('Name_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('Attack', sa.BOOLEAN(), nullable=False), + sa.Column('Passive', sa.BOOLEAN(), nullable=False), + sa.Column('Pet', sa.BOOLEAN(), nullable=False), + sa.Column('Level', sa.INTEGER(), nullable=False), + sa.Column('SkillCondition', sa.INTEGER(), nullable=False), + sa.Column('CoolTime', sa.INTEGER(), nullable=False), + sa.Column('SkillIcon', sa.VARCHAR(length=255), nullable=False), + sa.Column('FriendSkillIcon', sa.VARCHAR(length=255), nullable=False), + sa.Column('InfoText', sa.VARCHAR(length=255), nullable=False), + sa.Column('InfoText_en', sa.VARCHAR(length=255), nullable=True), + sa.PrimaryKeyConstraint('SkillId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_trace_table', + sa.Column('PlayerTraceTableId', sa.BIGINT(), nullable=False), + sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('Rate', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('PlayerTraceTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_beginner_mission', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('beginner_mission_id', sa.INTEGER(), nullable=False), + sa.Column('condition_id', sa.INTEGER(), nullable=False), + sa.Column('is_seat', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('achievement_num', sa.INTEGER(), nullable=False), + sa.Column('complete_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('complete_date', sa.TIMESTAMP(), nullable=True), + sa.Column('reward_received_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('reward_received_date', sa.TIMESTAMP(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user'), + sa.UniqueConstraint('user', 'condition_id', name='sao_player_beginner_mission_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_resource_card', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('common_reward_type', sa.INTEGER(), nullable=False), + sa.Column('common_reward_id', sa.INTEGER(), nullable=False), + sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('serial', sa.VARCHAR(length=20), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('serial'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_tutorial', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('tutorial_byte', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'tutorial_byte', name='sao_player_tutorial_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_episode', + sa.Column('EpisodeId', sa.BIGINT(), nullable=False), + sa.Column('EpisodeChapterId', sa.INTEGER(), nullable=False), + sa.Column('ReleaseEpisodeId', sa.INTEGER(), nullable=False), + sa.Column('Title', sa.VARCHAR(length=255), nullable=False), + sa.Column('CommentSummary', sa.VARCHAR(length=255), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=True), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('EpisodeId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_ex_bonus', + sa.Column('ExBonusTableId', sa.BIGINT(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('ExBonusConditionId', sa.INTEGER(), nullable=False), + sa.Column('ConditionValue1', sa.INTEGER(), nullable=False), + sa.Column('ConditionValue2', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardType', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardId', sa.INTEGER(), nullable=False), + sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False), + sa.Column('Strength', sa.INTEGER(), nullable=False), + sa.Column('Property1PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property1Value1', sa.INTEGER(), nullable=False), + sa.Column('Property1Value2', sa.INTEGER(), nullable=False), + sa.Column('Property2PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property2Value1', sa.INTEGER(), nullable=False), + sa.Column('Property2Value2', sa.INTEGER(), nullable=False), + sa.Column('Property3PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property3Value1', sa.INTEGER(), nullable=False), + sa.Column('Property3Value2', sa.INTEGER(), nullable=False), + sa.Column('Property4PropertyId', sa.BIGINT(), nullable=False), + sa.Column('Property4Value1', sa.INTEGER(), nullable=False), + sa.Column('Property4Value2', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['Property1PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property2PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property3PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['Property4PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('ExBonusTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_ex_tower', + sa.Column('ExTowerQuestId', sa.BIGINT(), nullable=False), + sa.Column('ExTowerId', sa.INTEGER(), nullable=False), + sa.Column('ReleaseExTowerQuestId', sa.INTEGER(), nullable=False), + sa.Column('Title', sa.VARCHAR(length=255), nullable=False), + sa.Column('Title_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('ExTowerQuestId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_side_quest', + sa.Column('SideQuestId', sa.BIGINT(), nullable=False), + sa.Column('DisplayName', sa.VARCHAR(length=255), nullable=False), + sa.Column('DisplayName_en', sa.VARCHAR(length=255), nullable=True), + sa.Column('EpisodeNum', sa.INTEGER(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('SideQuestId'), + sa.UniqueConstraint('SideQuestId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_skill_table', + sa.Column('SkillTableId', sa.BIGINT(), nullable=False), + sa.Column('SkillId', sa.BIGINT(), nullable=False), + sa.Column('SkillTableSubId', sa.INTEGER(), nullable=False), + sa.Column('LevelObtained', sa.INTEGER(), nullable=False), + sa.Column('AwakeningId', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['SkillId'], ['sao_static_skill.SkillId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('SkillTableId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_static_tower', + sa.Column('TowerId', sa.BIGINT(), nullable=False), + sa.Column('ReleaseTowerId', sa.INTEGER(), nullable=False), + sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False), + sa.Column('QuestSceneId', sa.BIGINT(), nullable=False), + sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('TowerId'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_ex_bonus', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('quest_scene_id', sa.BIGINT(), nullable=False), + sa.Column('ex_bonus_table_id', sa.BIGINT(), nullable=False), + sa.Column('quest_clear_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.ForeignKeyConstraint(['ex_bonus_table_id'], ['sao_static_ex_bonus.ExBonusTableId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['quest_scene_id'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user', 'quest_scene_id', 'ex_bonus_table_id', name='sao_player_ex_bonus_uk'), + mysql_charset='utf8mb4' + ) + op.create_table('sao_player_hero_card', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('user_hero_id', sa.INTEGER(), nullable=False), + sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False), + sa.Column('serial', sa.VARCHAR(length=20), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.ForeignKeyConstraint(['user_hero_id'], ['sao_hero_log_data.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('serial'), + mysql_charset='utf8mb4' + ) + op.alter_column('sao_end_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=False) + + op.drop_table('sao_static_equipment_list') + op.create_table("sao_static_equipment_list", + sa.Column("EquipmentId", sa.BIGINT, primary_key=True, nullable=False), + sa.Column("EquipmentType", sa.INTEGER, nullable=False), + sa.Column("WeaponTypeId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("Power", sa.INTEGER, nullable=False), + sa.Column("StrengthIncrement", sa.INTEGER, nullable=False), + sa.Column("SkillCondition", sa.INTEGER, nullable=False), + sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property1Value1", sa.INTEGER, nullable=False), + sa.Column("Property1Value2", sa.INTEGER, nullable=False), + sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property2Value1", sa.INTEGER, nullable=False), + sa.Column("Property2Value2", sa.INTEGER, nullable=False), + sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property3Value1", sa.INTEGER, nullable=False), + sa.Column("Property3Value2", sa.INTEGER, nullable=False), + sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property4Value1", sa.INTEGER, nullable=False), + sa.Column("Property4Value2", sa.INTEGER, nullable=False), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("CompositionExp", sa.INTEGER, nullable=False), + sa.Column("AwakeningExp", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_hero_list') + op.create_table("sao_static_hero_list", + sa.Column("HeroLogId", sa.BIGINT, primary_key=True, nullable=False), + sa.Column("CharaId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Nickname", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Nickname_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("WeaponTypeId", sa.INTEGER, nullable=False), + sa.Column("HeroLogRoleId", sa.INTEGER, nullable=False), + sa.Column("CostumeTypeId", sa.INTEGER, nullable=False), + sa.Column("UnitId", sa.INTEGER, nullable=False), + sa.Column("DefaultEquipmentId1", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + sa.Column("DefaultEquipmentId2", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + sa.Column("SkillTableSubId", sa.INTEGER, nullable=False), + sa.Column("HpMin", sa.INTEGER, nullable=False), + sa.Column("HpMax", sa.INTEGER, nullable=False), + sa.Column("StrMin", sa.INTEGER, nullable=False), + sa.Column("StrMax", sa.INTEGER, nullable=False), + sa.Column("VitMin", sa.INTEGER, nullable=False), + sa.Column("VitMax", sa.INTEGER, nullable=False), + sa.Column("IntMin", sa.INTEGER, nullable=False), + sa.Column("IntMax", sa.INTEGER, nullable=False), + sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property1Value1", sa.INTEGER, nullable=False), + sa.Column("Property1Value2", sa.INTEGER, nullable=False), + sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property2Value1", sa.INTEGER, nullable=False), + sa.Column("Property2Value2", sa.INTEGER, nullable=False), + sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property3Value1", sa.INTEGER, nullable=False), + sa.Column("Property3Value2", sa.INTEGER, nullable=False), + sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("Property4Value1", sa.INTEGER, nullable=False), + sa.Column("Property4Value2", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("CompositionExp", sa.INTEGER, nullable=False), + sa.Column("AwakeningExp", sa.INTEGER, nullable=False), + sa.Column("Slot4UnlockLevel", sa.INTEGER, nullable=False), + sa.Column("Slot5UnlockLevel", sa.INTEGER, nullable=False), + sa.Column("CollectionEmptyFrameDisplayFlag", sa.BOOLEAN, nullable=False), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_item_list') + op.create_table("sao_static_item_list", + sa.Column("ItemId", sa.INTEGER, nullable=False, primary_key=True), + sa.Column("ItemTypeId", sa.INTEGER, nullable=False), + sa.Column("Name", sa.VARCHAR(255), nullable=False), + sa.Column("Name_en", sa.VARCHAR(255)), + sa.Column("Rarity", sa.INTEGER, nullable=False), + sa.Column("Value", sa.INTEGER, nullable=False), + sa.Column("PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + sa.Column("PropertyValue1Min", sa.INTEGER, nullable=False), + sa.Column("PropertyValue1Max", sa.INTEGER, nullable=False), + sa.Column("PropertyValue2Min", sa.INTEGER, nullable=False), + sa.Column("PropertyValue2Max", sa.INTEGER, nullable=False), + sa.Column("FlavorText", sa.VARCHAR(255), nullable=False), + sa.Column("FlavorText_en", sa.VARCHAR(255)), + sa.Column("SalePrice", sa.INTEGER, nullable=False), + sa.Column("ItemIcon", sa.VARCHAR(255), nullable=False), + mysql_charset="utf8mb4" + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('sao_static_item_list') + op.create_table("sao_static_item_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("itemId", sa.Integer), + sa.Column("itemTypeId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "itemId", name="sao_static_item_list_uk" + ), + mysql_charset="utf8mb4" + ) + + op.drop_table('sao_static_hero_list') + op.create_table("sao_static_hero_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("heroLogId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("nickname", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("skillTableSubId", sa.Integer), + sa.Column("awakeningExp", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "heroLogId", name="sao_static_hero_list_uk" + ), + mysql_charset="utf8mb4", + ) + + op.drop_table('sao_static_equipment_list') + op.create_table("sao_static_equipment_list", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("version", sa.Integer), + sa.Column("equipmentId", sa.Integer), + sa.Column("equipmentType", sa.Integer), + sa.Column("weaponTypeId", sa.Integer), + sa.Column("name", sa.String(255)), + sa.Column("rarity", sa.Integer), + sa.Column("flavorText", sa.String(255)), + sa.Column("enabled", sa.Boolean), + sa.UniqueConstraint( + "version", "equipmentId", name="sao_static_equipment_list_uk" + ), + mysql_charset="utf8mb4" + + ) + + op.alter_column('sao_end_sessions', 'play_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=False) + op.drop_table('sao_player_hero_card') + op.drop_table('sao_player_ex_bonus') + op.drop_table('sao_static_tower') + op.drop_table('sao_static_skill_table') + op.drop_table('sao_static_side_quest') + op.drop_table('sao_static_ex_tower') + op.drop_table('sao_static_ex_bonus') + op.drop_table('sao_static_episode') + op.drop_table('sao_player_tutorial') + op.drop_table('sao_player_resource_card') + op.drop_table('sao_player_beginner_mission') + op.drop_table('sao_static_trace_table') + op.drop_table('sao_static_skill') + op.drop_table('sao_static_reward') + op.drop_table('sao_static_property') + op.drop_table('sao_static_quest') + op.create_table('sao_static_quest', + sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False), + sa.Column('enabled', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True), + sa.Column('version', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('questSceneId', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('sortNo', mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column('name', mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_general_ci', length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint("version", "questSceneId", name="sao_static_quest_uk"), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### diff --git a/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py new file mode 100644 index 0000000..dd52974 --- /dev/null +++ b/core/data/alembic/versions/b23f985100ba_chunithm_luminous.py @@ -0,0 +1,87 @@ +"""CHUNITHM LUMINOUS + +Revision ID: b23f985100ba +Revises: 3657efefc5a4 +Create Date: 2024-06-20 08:08:08.759261 + +""" +from alembic import op +from sqlalchemy import Column, Integer, Boolean, UniqueConstraint + + +# revision identifiers, used by Alembic. +revision = 'b23f985100ba' +down_revision = '3657efefc5a4' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "chuni_profile_net_battle", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_profile_net_battle", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + op.create_table( + "chuni_item_cmission_progress", + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + "chuni_item_cmission_progress", + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + + +def downgrade(): + op.drop_table("chuni_profile_net_battle") + op.drop_table("chuni_item_cmission") + op.drop_table("chuni_item_cmission_progress") diff --git a/core/data/schema/base.py b/core/data/schema/base.py index 55eceaf..d74198b 100644 --- a/core/data/schema/base.py +++ b/core/data/schema/base.py @@ -8,7 +8,8 @@ from sqlalchemy.engine.base import Connection from sqlalchemy.sql import text, func, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import MetaData, Table, Column -from sqlalchemy.types import Integer, String, TIMESTAMP, JSON +from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, INTEGER, TEXT +from sqlalchemy.schema import ForeignKey from sqlalchemy.dialects.mysql import insert from core.config import CoreConfig @@ -22,6 +23,12 @@ event_log = Table( Column("system", String(255), nullable=False), Column("type", String(255), nullable=False), Column("severity", Integer, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("arcade", INTEGER, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade")), + Column("machine", INTEGER, ForeignKey("machine.id", ondelete="cascade", onupdate="cascade")), + Column("ip", TEXT(39)), + Column("game", TEXT(4)), + Column("version", TEXT(24)), Column("message", String(1000), nullable=False), Column("details", JSON, nullable=False), Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()), @@ -75,12 +82,19 @@ class BaseData: return randrange(10000, 9999999) async def log_event( - self, system: str, type: str, severity: int, message: str, details: Dict = {} + self, system: str, type: str, severity: int, message: str, details: Dict = {}, user: int = None, + arcade: int = None, machine: int = None, ip: str = None, game: str = None, version: str = None ) -> Optional[int]: sql = event_log.insert().values( system=system, type=type, severity=severity, + user=user, + arcade=arcade, + machine=machine, + ip=ip, + game=game, + version=version, message=message, details=json.dumps(details), ) @@ -94,8 +108,8 @@ class BaseData: return result.lastrowid - async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]: - sql = event_log.select().limit(entries).all() + async def get_event_log(self, entries: int = 100) -> Optional[List[Row]]: + sql = event_log.select().order_by(event_log.c.id.desc()).limit(entries) result = await self.execute(sql) if result is None: diff --git a/core/data/schema/card.py b/core/data/schema/card.py index c31e1b2..6205b4c 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional from sqlalchemy import Table, Column, UniqueConstraint -from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP +from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func from sqlalchemy.engine import Row @@ -11,12 +11,10 @@ aime_card = Table( "aime_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("access_code", String(20)), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("access_code", String(20), nullable=False, unique=True), + Column("idm", String(16), unique=True), + Column("chip_id", BIGINT, unique=True), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), @@ -27,6 +25,9 @@ aime_card = Table( class CardData(BaseData): + moble_os_codes = set([0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18]) + card_os_codes = set([0x20, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7]) + async def get_card_by_access_code(self, access_code: str) -> Optional[Row]: sql = aime_card.select(aime_card.c.access_code == access_code) @@ -121,7 +122,32 @@ class CardData(BaseData): result = await self.execute(sql) if result is None: self.logger.warn(f"Failed to update last login time for {access_code}") - + + async def get_card_by_idm(self, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.idm == idm)) + if result: + return result.fetchone() + + async def get_card_by_chip_id(self, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.chip_id == chip_id)) + if result: + return result.fetchone() + + async def set_chip_id_by_access_code(self, access_code: str, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(chip_id=chip_id)) + if not result: + self.logger.error(f"Failed to update chip ID to {chip_id} for {access_code}") + + async def set_idm_by_access_code(self, access_code: str, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(idm=idm)) + if not result: + self.logger.error(f"Failed to update IDm to {idm} for {access_code}") + + async def set_access_code_by_access_code(self, old_ac: str, new_ac: str) -> None: + result = await self.execute(aime_card.update(aime_card.c.access_code == old_ac).values(access_code=new_ac)) + if not result: + self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}") + def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string @@ -132,4 +158,4 @@ class CardData(BaseData): """ Given a 20 digit access code as a string, return the 16 hex character luid """ - return f"{int(access_code):0{16}x}" + return f"{int(access_code):0{16}X}" diff --git a/core/data/schema/user.py b/core/data/schema/user.py index 5f1ccf2..8c3695c 100644 --- a/core/data/schema/user.py +++ b/core/data/schema/user.py @@ -120,3 +120,7 @@ class UserData(BaseData): result = await self.execute(sql) return result is not None + + async def get_user_by_username(self, username: str) -> Optional[Row]: + result = await self.execute(aime_user.select(aime_user.c.username == username)) + if result: return result.fetchone() diff --git a/core/frontend.py b/core/frontend.py index 382a082..f15f58a 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -44,12 +44,13 @@ class ShopOwner(): self.permissions = perms class UserSession(): - def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1): + def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1): self.user_id = usr_id self.current_ip = ip self.permissions = perms self.ongeki_version = ongeki_ver self.chunithm_version = chunithm_ver + self.maimai_version = maimai_version class FrontendServlet(): def __init__(self, cfg: CoreConfig, config_dir: str) -> None: @@ -133,6 +134,7 @@ class FrontendServlet(): ]), Mount("/sys", routes=[ Route("/", self.system.render_GET, methods=['GET']), + Route("/logs", self.system.render_logs, methods=['GET']), Route("/lookup.user", self.system.lookup_user, methods=['GET']), Route("/lookup.shop", self.system.lookup_shop, methods=['GET']), Route("/add.user", self.system.add_user, methods=['POST']), @@ -191,7 +193,7 @@ class FE_Base(): ), media_type="text/html; charset=utf-8") if sesh is None: - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp def get_routes(self) -> List[Route]: @@ -215,6 +217,8 @@ class FE_Base(): sesh.current_ip = tk['current_ip'] sesh.permissions = tk['permissions'] sesh.chunithm_version = tk['chunithm_version'] + sesh.maimai_version = tk['maimai_version'] + sesh.ongeki_version = tk['ongeki_version'] if sesh.user_id <= 0: self.logger.error("User session failed to validate due to an invalid ID!") @@ -240,7 +244,7 @@ class FE_Base(): return UserSession() def validate_session(self, request: Request) -> Optional[UserSession]: - sesh = request.cookies.get('DIANA_SESH', "") + sesh = request.cookies.get('ARTEMIS_SESH', "") if not sesh: return None @@ -259,7 +263,17 @@ class FE_Base(): def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str: try: - return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "chunithm_version": sesh.chunithm_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256") + return jwt.encode({ + "user_id": sesh.user_id, + "current_ip": sesh.current_ip, + "permissions": sesh.permissions, + "ongeki_version": sesh.ongeki_version, + "chunithm_version": sesh.chunithm_version, + "maimai_version": sesh.maimai_version, + "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, + b64decode(self.core_config.frontend.secret), + algorithm="HS256" + ) except jwt.InvalidKeyError: self.logger.error("Failed to encode User session because the secret is invalid!") return "" @@ -291,7 +305,7 @@ class FE_Gate(FE_Base): error=err, sesh=vars(UserSession()), ), media_type="text/html; charset=utf-8") - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp async def render_login(self, request: Request): @@ -307,8 +321,12 @@ class FE_Gate(FE_Base): uid = await self.data.card.get_user_id_from_card(access_code) if uid is None: - self.logger.debug(f"Failed to find user for card {access_code}") - return RedirectResponse("/gate/?e=1", 303) + user = await self.data.user.get_user_by_username(access_code) # Lookup as username + if not user: + self.logger.debug(f"Failed to find user for card/username {access_code}") + return RedirectResponse("/gate/?e=1", 303) + + uid = user['id'] user = await self.data.user.get_user(uid) if user is None: @@ -337,7 +355,7 @@ class FE_Gate(FE_Base): usr_sesh = self.encode_session(sesh) self.logger.debug(f"Created session with JWT {usr_sesh}") resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) return resp @@ -376,7 +394,7 @@ class FE_Gate(FE_Base): usr_sesh = self.encode_session(sesh) self.logger.debug(f"Created session with JWT {usr_sesh}") resp = RedirectResponse("/user/", 303) - resp.set_cookie("DIANA_SESH", usr_sesh) + resp.set_cookie("ARTEMIS_SESH", usr_sesh) return resp @@ -494,7 +512,7 @@ class FE_User(FE_Base): async def render_logout(self, request: Request): resp = RedirectResponse("/gate/", 303) - resp.delete_cookie("DIANA_SESH") + resp.delete_cookie("ARTEMIS_SESH") return resp async def edit_card(self, request: Request) -> RedirectResponse: @@ -783,6 +801,35 @@ class FE_System(FE_Base): cabadd={"id": cab_id, "serial": serial}, ), media_type="text/html; charset=utf-8") + async def render_logs(self, request: Request): + template = self.environment.get_template("core/templates/sys/logs.jinja") + events = [] + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/sys/?e=11", 303) + + logs = await self.data.base.get_event_log() + if not logs: + logs = [] + + for log in logs: + evt = log._asdict() + if not evt['user']: evt["user"] = "NONE" + if not evt['arcade']: evt["arcade"] = "NONE" + if not evt['machine']: evt["machine"] = "NONE" + if not evt['ip']: evt["ip"] = "NONE" + if not evt['game']: evt["game"] = "NONE" + if not evt['version']: evt["version"] = "NONE" + evt['when_logged'] = evt['when_logged'].strftime("%x %X") + events.append(evt) + + return Response(template.render( + title=f"{self.core_config.server.name} | Event Logs", + sesh=vars(usr_sesh), + events=events + ), media_type="text/html; charset=utf-8") + class FE_Arcade(FE_Base): async def render_GET(self, request: Request): template = self.environment.get_template("core/templates/arcade/index.jinja") @@ -849,7 +896,7 @@ class FE_Machine(FE_Base): arcade={} ), media_type="text/html; charset=utf-8") -cfg_dir = environ.get("DIANA_CFG_DIR", "config") +cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg: CoreConfig = CoreConfig() if path.exists(f"{cfg_dir}/core.yaml"): cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) diff --git a/core/templates/gate/gate.jinja b/core/templates/gate/gate.jinja index ca3e2eb..d398cbd 100644 --- a/core/templates/gate/gate.jinja +++ b/core/templates/gate/gate.jinja @@ -15,18 +15,18 @@ -moz-appearance: textfield; } -
+
-
- +
+ +
20 Digit access code from a card registered to your account, or your account username. (NOT your username from a game!)

- + +
Leave blank if registering for the webui. Your card must have been used on a game connected to this server to register.

-
*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.
-
*If you have not registered a card with this server, you cannot create a webui account.
{% endblock content %} \ No newline at end of file diff --git a/core/templates/sys/index.jinja b/core/templates/sys/index.jinja index b589d90..637b8c9 100644 --- a/core/templates/sys/index.jinja +++ b/core/templates/sys/index.jinja @@ -51,6 +51,9 @@ +
+ +
{% endif %}
diff --git a/core/templates/sys/logs.jinja b/core/templates/sys/logs.jinja new file mode 100644 index 0000000..9b7a987 --- /dev/null +++ b/core/templates/sys/logs.jinja @@ -0,0 +1,198 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Event Logs

+ + + + + + + + + + + + + + + + + + {% if events is not defined or events|length == 0 %} + + + + {% endif %} +
Viewing last 100 logs
SeverityTimestampSystemNameUserArcadeMachineGameVersionMessageParams
No Events
+
+ +  + + +
+ +{% endblock content %} \ No newline at end of file diff --git a/core/templates/widgets/err_banner.jinja b/core/templates/widgets/err_banner.jinja index eec204a..25208df 100644 --- a/core/templates/widgets/err_banner.jinja +++ b/core/templates/widgets/err_banner.jinja @@ -21,6 +21,8 @@ New Nickname too long You must be logged in to preform this action {% elif error == 10 %} Invalid serial number +{% elif error == 11 %} +Access Denied {% else %} An unknown error occoured {% endif %} diff --git a/core/title.py b/core/title.py index 7aeb433..016e09a 100644 --- a/core/title.py +++ b/core/title.py @@ -80,7 +80,8 @@ class BaseServlet: cfg_dir (str): Config directory Returns: - Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD + Tuple[bool, List[str], List[str]]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CDs handled + by this servlette, and offset 2 is mucha netID prefixes that should be used for each game CD. """ return (False, [], []) diff --git a/core/utils.py b/core/utils.py index a9d1705..24c174c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -49,7 +49,7 @@ class Utils: def get_title_port_ssl(cls, cfg: CoreConfig): if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl - cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else Utils.get_title_port(cfg) + cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else 443 return cls.real_title_port_ssl diff --git a/docs/INSTALL_UBUNTU_old.md b/docs/INSTALL_UBUNTU_old.md deleted file mode 100644 index 710c757..0000000 --- a/docs/INSTALL_UBUNTU_old.md +++ /dev/null @@ -1,129 +0,0 @@ -# ARTEMiS - Ubuntu 20.04 LTS Guide -This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment - -# Setup -## Install memcached module -1. sudo apt-get install memcached -2. Under the file /etc/memcached.conf, please make sure the following parameters are set: - -``` -# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default -# Note that the daemon will grow to this size, but does not start out holding this much -# memory - --I 128m --m 1024 -``` - -** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles - -3. Restart memcached using: sudo systemctl restart memcached - -## Install MySQL 5.7 -``` -sudo apt update -sudo apt install wget -y -wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb -sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb -``` - 1. During the first prompt, select Ubuntu Bionic - 2. Select the default option - 3. Select MySQL 5.7 - 4. Select the last option -``` -sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29 -sudo apt-get update -sudo apt-cache policy mysql-server -sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* -``` - -## Default Configuration for MySQL Server -1. sudo mysql_secure_installation -> Make sure to follow the steps that will be prompted such as changing the mysql root password and such - -2. Test your MySQL Server login by doing the following command : -> mysql -u root -p - -## Create the default ARTEMiS database and user -1. mysql -u root -p -2. Please change the password indicated in the next line for a custom secure one and continue with the next commands - -``` -CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.'; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; -``` - -3. sudo systemctl restart mysql - -## Install Python modules -``` -sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev -sudo apt install libpython3.8-dev -sudo apt-get install python3-software-properties -sudo apt install python3-pip -sudo pip3 install --upgrade pip testresources -sudo pip3 install --upgrade pip setuptools -sudo apt-get install python3-tk -``` -7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements: -> sudo python3 -m pip install -r requirements.txt - -## Copy/Rename the folder example_config to config - -## Adjust /config/core.yaml -1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx) -2. Adjust the proper MySQL information you created earlier -3. Add the AimeDB key at the bottom of the file - -## Create the database tables for ARTEMiS -1. sudo python3 dbutils.py create - -2. If you get "No module named Crypto", run the following command: -``` -sudo pip uninstall crypto -sudo pip uninstall pycrypto -sudo pip install pycrypto -``` - -## Firewall Adjustements -``` -sudo ufw allow 80 -sudo ufw allow 443 -sudo ufw allow 8443 -sudo ufw allow 22345 -sudo ufw allow 8090 -sudo ufw allow 8444 -sudo ufw allow 8080 -``` - -## Running the ARTEMiS instance -1. sudo python3 index.py - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## Memcache failed, error 3 -1. Make sure memcached is properly installed and running. You can check the status of the service using the following command: -> sudo systemctl status memcached -2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m -3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file. -``` -# Make memcache optional -try: - has_mc = False -except ModuleNotFoundError: - has_mc = False -``` diff --git a/docs/INSTALL_WINDOWS.md b/docs/INSTALL_WINDOWS.md index 1cbe140..d17886a 100644 --- a/docs/INSTALL_WINDOWS.md +++ b/docs/INSTALL_WINDOWS.md @@ -59,7 +59,7 @@ GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* T - Put the password you created for the aime user into the `database` section. - Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE). - Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1). - - Optional: generate base64-encoded secrets for aimedb and frontend. + - Optional: generate base64-encoded secrets for aimedb and frontend using something like `openssl rand -base64 64`. It is advised to make all secrets different. - See [config.md](docs/config.md) for a full list of options. - edit `idz.yaml` - If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot. diff --git a/docs/INSTALL_WINDOWS_old.md b/docs/INSTALL_WINDOWS_old.md deleted file mode 100644 index b976b26..0000000 --- a/docs/INSTALL_WINDOWS_old.md +++ /dev/null @@ -1,102 +0,0 @@ -# ARTEMiS - Windows 10/11 Guide -This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment - -# Setup -## Install Python Python 3.9 (recommended) or 3.10 -1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe) -2. Install python-3.9.13-amd64.exe - 1. Select Customize installation - 2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next - 3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install - -## Install MySQL 8.0 -1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi) -2. Install mysql-installer-web-community-8.0.34.0.msi - 1. Click on "Add ..." on the side - 2. Click on the "+" next to MySQL Servers - 3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed. - 4. Hit Next and Next once installed - 5. Select the configuration type "Development Computer" - 6. Hit Next - 7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next - 8. Enter a root password and then hit Next > - 9. Leave everything under Windows Service as default and hit Next > - 10. Click on Execute and for it to finish and hit Next> and then Finish -3. Open MySQL 8.0 Command Line Client and login as your root user -4. Change `` to a new password for the user aime, type those commands to create your user and the database - -```sql -CREATE USER 'aime'@'localhost' IDENTIFIED BY ''; -CREATE DATABASE aime; -GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; -FLUSH PRIVILEGES; -exit; -``` - -## Install Python modules -1. Change your work path to the artemis-master folder using 'cd' and install the requirements: - -```shell -pip install -r requirements.txt -``` - -## Copy/Rename the folder `example_config` to `config` - -## Adjust `config/core.yaml` - -1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx) - - In case you want to run this only locally, set the following values: - -```yaml -server: - listen_address: 0.0.0.0 -title: - hostname: 192.168.xxx.xxx -``` - -1. Adjust the proper MySQL information you created earlier -```yaml -database: - host: "localhost" - username: "aime" - password: "" - name: "aime" -``` -3. Add the AimeDB key at the bottom of the file -4. If the webui is needed, change the flag from False to True - -## Create the database tables for ARTEMiS - -```shell -python dbutils.py create -``` - -## Firewall Adjustements -Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): -> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha - -## Running the ARTEMiS instance -```shell -python index.py -``` - -# Troubleshooting - -## Game does not connect to ARTEMiS Allnet server -1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened - -## Game does not connect to Title Server -1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname -2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox. -3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml. - -## Unhandled command under AimeDB -1. Double check your AimeDB key under core.yaml, it is incorrect. - -## AttributeError: module 'collections' has no attribute 'Hashable' -1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below. - - Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands: - -```shell -pip install -r requirements.txt -U -``` diff --git a/docs/config.md b/docs/config.md index 39855dc..8a482e3 100644 --- a/docs/config.md +++ b/docs/config.md @@ -17,8 +17,8 @@ - `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False` ## Title - `loglevel`: Logging level for the title server. Default `info` -- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. Leave blank for no maintenance time. Default: `""` -- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `05:00`. Leave blank for no maintenance time. Default: `""` +- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. A few games or early version will report errors if it is empty, ex maimai DX 1.00 +- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `07:00`. this must be set to 7:00 am for some game, please do not change it. ## Database - `host`: Host of the database. Default `localhost` - `username`: Username of the account the server should connect to the database with. Default `aime` diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index a8e63c5..9fadf80 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -63,6 +63,7 @@ Games listed below have been tested and confirmed working. | 12 | CHUNITHM NEW PLUS!! | | 13 | CHUNITHM SUN | | 14 | CHUNITHM SUN PLUS | +| 15 | CHUNITHM LUMINOUS | ### Importer @@ -87,13 +88,19 @@ Config file is located in `config/chuni.yaml`. | `crypto` | This option is used to enable the TLS Encryption | -**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** +If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key +is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value +is an array containing `[key, iv, salt, iter_count]` in order. + +`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions. +You will receive an error in the logs if it needs to be specified. ```yaml crypto: encrypted_only: False keys: 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42] ``` ### Database upgrade @@ -175,6 +182,14 @@ Config file is located in `config/cxb.yaml`. ## maimai DX +### Presents +Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add: + +| Game Version | Item ID | Item Kind | Item Description | Present Description | +|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------| +| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial | +| | | | | number, for project raputa | + ### Versions | Game Code | Version ID | Version Name | diff --git a/docs/migrating.md b/docs/migrating.md new file mode 100644 index 0000000..3e2e056 --- /dev/null +++ b/docs/migrating.md @@ -0,0 +1,34 @@ +# Migrating from an older build of ARTEMiS +If you haven't updated artemis in a while, you may find that configuration options have moved, been renamed, or no longer exist. This document exists to help migrate from legacy versions of artemis to newer builds. + +## Dependancies +Make sure your dependiences are up to date with what's required to run artemis. A simple `pip install -r requirements.txt` will get you up to date. + +## Database +Database migration is required if you are using a version of artemis that still uses the old custom-rolled database versioning system (raw SQL scripts). Artemis now uses alembic to manage database versioning, and you will need to move to this new system. + +**BEFORE DOING ANY DATABASE WORK, ALWAYS MAKE SURE YOU HAVE FUNCTIONAL, UP-TO-DATE BACKUPS!!** + +For almost all situations, simply running `python dbutils.py migrate` will do the job. This will upgrade you to the latest version of the old system, move you over to alembic, then upgrade you to the newest alembic version. If you encounter any errors or data loss, you should report this as a bug to our issue tracker. + +## Configuration +Configuration management is the sewage cleaning of the sysadmin world. It sucks and nobody likes to do it, but it needs to be done or everyone ends up in deep shit. This section will walk through what configuration options have changed, and how to set them properly. + +### core.yaml +`title`->`hostname` is now `server`->`hostname`. This hostname is what gets sent to clients in response to auth requests, so it should be both accessable from whereever the client is, and point properly to the title server. + +With the move to starlette and uvicorn, different services now run as seperate USGI applications. `billing`->`standalone` and `allnet`->`standalone` are flags that determine weather the service runs as a stand-alone service, on it's own seperate port, or as a part of the whole application. For example, setting `billing`->`standalone` to `True` will cause a seperate instance of the billing server to spin up listening on 8443 with SSL using the certs listed in the config file. Setting it to `False` will just allow the main server to also serve `/request/` and assumes that something is standing in front of it proxying 8443 SSL to whatever `server`->`port` is set to. + +Beforehand, if `server`->`is_develop` was `False`, the server assumed that there was a proxy standing in front of it, proxying requests to proper channels. This was, in hindsight, a very dumb assumption. Now, `server`->`is_using_proxy` is what flags the server as having nginx or another proxy in front of it. The effects of setting this to true are somewhat game-dependant, but generally artemis will use the port listed in `server`->`proxy_port` (and `server`->`proxy_port_ssl` for SSL connections, as defined by the games) instead of `server`->`port`. If set to 0, `server`->`proxy_port` will default to what `server`->`port` (and `server`->`proxy_port_ssl` will default to 443) make sure to set them accordingly. Note that some title servers have their own needs and specify their own specific ports. Refer to [game_specific_info.md](docs/game_specific_info.md) for more infomation. (For example, pokken requires SSL using an older, weaker certificate, and always requires the port to be sent even if it's port 443) + +`index.py`'s args have changed. You can now override what port the title server listens on with `-p` and tell the server to use ssl with `-s`. + +Rather then having a `standalone` config variable, the frontend is a seperate wsgi app entirely. Having `enable` be `True` will launch it on the port specified in the config file. Otherwise, the fontend will not run. + +`title`->`reboot_start_time`/`reboot_end_time` allow you to specify when the games should be told network maintanence is happening. It's exact implementation depends on the game. Do note that many games will beave unexpectly if `reboot_end_time` is not `07:00`. + +If you wish to make use of aimedb's SegaAuthId system to better protect the few title servers that actually use it, set `aimedb`->`id_secret` to base64-encoded random bytes (32 is a good length) using something like `openssl rand -base64 64`. If you intend to use the frontend, the same thing must be done for `frontend`->`secret` or you won't be able to log in. + +`mucha`'s only option is now just log level. + +`aimedb` now has it's own `listen_address` field, in case you want to proxy everything but aimedb, so it can still listen on `0.0.0.0` instead of `127.0.0.1`. diff --git a/example_config/chuni.yaml b/example_config/chuni.yaml index 53da186..4855fa1 100644 --- a/example_config/chuni.yaml +++ b/example_config/chuni.yaml @@ -22,6 +22,9 @@ version: 14: rom: 2.15.00 data: 2.15.00 + 15: + rom: 2.20.00 + data: 2.20.00 crypto: encrypted_only: False diff --git a/example_config/core.yaml b/example_config/core.yaml index 758a089..daf18fc 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -18,7 +18,7 @@ server: title: loglevel: "info" reboot_start_time: "04:00" - reboot_end_time: "05:00" + reboot_end_time: "07:00" # this must be set to 7:00 am for some game, please do not change it database: host: "localhost" diff --git a/example_config/cxb.yaml b/example_config/cxb.yaml index 5cc4f90..628d49c 100644 --- a/example_config/cxb.yaml +++ b/example_config/cxb.yaml @@ -1,4 +1,4 @@ server: enable: True loglevel: "info" - use:https: True \ No newline at end of file + use_https: True \ No newline at end of file diff --git a/example_config/sao.yaml b/example_config/sao.yaml index 0209ffe..d405461 100644 --- a/example_config/sao.yaml +++ b/example_config/sao.yaml @@ -2,12 +2,19 @@ server: enable: True loglevel: "info" auto_register: True + photon_app_id: "7df3a2f6-d69d-4073-aafe-810ee61e1cea" + data_version: 1 + game_version: 33 crypt: enable: False key: "" - iv: "" hash: verify_hash: False - hash_base: "" \ No newline at end of file + hash_base: "" + +card: + enable: True + crypt_password: "" + crypt_salt: "" diff --git a/readme.md b/readme.md index 114fef5..505f241 100644 --- a/readme.md +++ b/readme.md @@ -10,6 +10,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + CHUNITHM INTL + SUPERSTAR + + SUPERSTAR PLUS + NEW + NEW PLUS + SUN diff --git a/titles/chuni/base.py b/titles/chuni/base.py index e53c2d2..d9d35cc 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -952,6 +952,31 @@ class ChuniBase: rating_type, upsert[rating_type], ) + + # added in LUMINOUS + if "userCMissionList" in upsert: + for cmission in upsert["userCMissionList"]: + mission_id = cmission["missionId"] + + await self.data.item.put_cmission( + user_id, + { + "missionId": mission_id, + "point": cmission["point"], + }, + ) + + for progress in cmission["userCMissionProgressList"]: + await self.data.item.put_cmission_progress(user_id, mission_id, progress) + + if "userNetBattleData" in upsert: + net_battle = upsert["userNetBattleData"][0] + + # fix the boolean + net_battle["isRankUpChallengeFailed"] = ( + False if net_battle["isRankUpChallengeFailed"] == "false" else True + ) + await self.data.profile.put_net_battle(user_id, net_battle) return {"returnCode": "1"} @@ -980,4 +1005,4 @@ class ChuniBase: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } \ No newline at end of file + } diff --git a/titles/chuni/const.py b/titles/chuni/const.py index 3e83378..d037842 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -1,3 +1,6 @@ +from enum import Enum + + class ChuniConstants: GAME_CODE = "SDBT" GAME_CODE_NEW = "SDHD" @@ -20,6 +23,7 @@ class ChuniConstants: VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 + VER_CHUNITHM_LUMINOUS = 15 VERSION_NAMES = [ "CHUNITHM", "CHUNITHM PLUS", @@ -35,9 +39,22 @@ class ChuniConstants: "CHUNITHM NEW!!", "CHUNITHM NEW PLUS!!", "CHUNITHM SUN", - "CHUNITHM SUN PLUS" + "CHUNITHM SUN PLUS", + "CHUNITHM LUMINOUS", ] @classmethod def game_ver_to_string(cls, ver: int): - return cls.VERSION_NAMES[ver] \ No newline at end of file + return cls.VERSION_NAMES[ver] + + +class MapAreaConditionType(Enum): + UNLOCKED = 0 + MAP_CLEARED = 1 + MAP_AREA_CLEARED = 2 + TROPHY_OBTAINED = 3 + + +class MapAreaConditionLogicalOperator(Enum): + AND = 1 + OR = 2 diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index b0fa9bc..0dbefac 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -68,7 +68,7 @@ class ChuniFrontend(FE_Base): if usr_sesh.chunithm_version >= 0: encoded_sesh = self.encode_session(usr_sesh) - resp.set_cookie("DIANA_SESH", encoded_sesh) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: @@ -240,7 +240,7 @@ class ChuniFrontend(FE_Base): encoded_sesh = self.encode_session(usr_sesh) self.logger.info(f"Created session with JWT {encoded_sesh}") resp = RedirectResponse("/game/chuni/", 303) - resp.set_cookie("DIANA_SESH", encoded_sesh) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: return RedirectResponse("/gate/", 303) \ No newline at end of file diff --git a/titles/chuni/index.py b/titles/chuni/index.py index f781898..dd49cc6 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -1,7 +1,8 @@ from starlette.requests import Request from starlette.routing import Route from starlette.responses import Response -import logging, coloredlogs +import logging +import coloredlogs from logging.handlers import TimedRotatingFileHandler import zlib import yaml @@ -34,12 +35,13 @@ from .new import ChuniNew from .newplus import ChuniNewPlus from .sun import ChuniSun from .sunplus import ChuniSunPlus +from .luminous import ChuniLuminous class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = ChuniConfig() - self.hash_table: Dict[Dict[str, str]] = {} + self.hash_table: Dict[str, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) @@ -61,6 +63,7 @@ class ChuniServlet(BaseServlet): ChuniNewPlus, ChuniSun, ChuniSunPlus, + ChuniLuminous, ] self.logger = logging.getLogger("chuni") @@ -89,30 +92,60 @@ class ChuniServlet(BaseServlet): ) self.logger.inited = True + known_iter_counts = { + ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, + ChuniConstants.VER_CHUNITHM_PARADISE: 44, + f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25, + ChuniConstants.VER_CHUNITHM_NEW: 54, + f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, + ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, + ChuniConstants.VER_CHUNITHM_SUN: 70, + ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, + ChuniConstants.VER_CHUNITHM_LUMINOUS: 8, + } + for version, keys in self.game_cfg.crypto.keys.items(): if len(keys) < 3: continue - self.hash_table[version] = {} + if isinstance(version, int): + version_idx = version + else: + version_idx = int(version.split("_")[0]) + + salt = bytes.fromhex(keys[2]) + if len(keys) >= 4: + iter_count = keys[3] + elif (iter_count := known_iter_counts.get(version)) is None: + self.logger.error( + "Number of iteration rounds for version %s is not known, but it is not specified in the config", + version, + ) + continue + + self.hash_table[version] = {} method_list = [ method - for method in dir(self.versions[version]) + for method in dir(self.versions[version_idx]) if not method.startswith("__") ] + for method in method_list: method_fixed = inflection.camelize(method)[6:-7] - # number of iterations was changed to 70 in SUN and then to 36 - if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: - iter_count = 36 - elif version == ChuniConstants.VER_CHUNITHM_SUN: - iter_count = 70 - else: - iter_count = 44 + + # This only applies for CHUNITHM NEW International and later for some reason. + # CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing. + if ( + isinstance(version, str) + and version.endswith("_int") + and version_idx >= ChuniConstants.VER_CHUNITHM_NEW + ): + method_fixed += "C3Exp" hash = PBKDF2( method_fixed, - bytes.fromhex(keys[2]), + salt, 128, count=iter_count, hmac_hash_module=SHA1, @@ -122,7 +155,7 @@ class ChuniServlet(BaseServlet): 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()}" + f"Hashed v{version} method {method_fixed} with {salt} to get {hashed_name}" ) @classmethod @@ -195,47 +228,59 @@ class ChuniServlet(BaseServlet): internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 210 and version < 215: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 215: # SUN + elif version >= 215 and version < 220: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 220: # LUMINOUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif game_code == "SDGS": # Int - if version < 110: # SUPERSTAR - internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE" + if version < 110: # SUPERSTAR / SUPERSTAR PLUS + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it elif version >= 110 and version < 115: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW elif version >= 115 and version < 120: # NEW PLUS!! internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS elif version >= 120 and version < 125: # SUN internal_ver = ChuniConstants.VER_CHUNITHM_SUN - elif version >= 125: # SUN PLUS + elif version >= 125 and version < 130: # SUN PLUS internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS + elif version >= 130: # LUMINOUS + internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS 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 - # doing encrypted. The likelyhood of false positives is low but + # doing encrypted. The likelihood of false positives is low but # technically not 0 + + if game_code == "SDGS": + crypto_cfg_key = f"{internal_ver}_int" + hash_table_key = f"{internal_ver}_int" + else: + crypto_cfg_key = internal_ver + hash_table_key = internal_ver + if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: endpoint = request.headers.get("User-Agent").split("#")[0] else: - if internal_ver not in self.hash_table: + if hash_table_key not in self.hash_table: self.logger.error( f"v{version} does not support encryption or no keys entered" ) return Response(zlib.compress(b'{"stat": "0"}')) - elif endpoint.lower() not in self.hash_table[internal_ver]: + elif endpoint.lower() not in self.hash_table[hash_table_key]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) return Response(zlib.compress(b'{"stat": "0"}')) - endpoint = self.hash_table[internal_ver][endpoint.lower()] + endpoint = self.hash_table[hash_table_key][endpoint.lower()] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) req_raw = crypt.decrypt(req_raw) @@ -272,11 +317,13 @@ class ChuniServlet(BaseServlet): self.logger.info(f"v{version} {endpoint} request from {client_ip}") self.logger.debug(req_data) - endpoint = ( - endpoint.replace("C3Exp", "") - if game_code == "SDGS" - else endpoint - ) + if game_code == "SDGS" and version >= 110: + endpoint = endpoint.replace("C3Exp", "") + elif game_code == "SDGS" and version < 110: + endpoint = endpoint.replace("Exp", "") + else: + endpoint = endpoint + func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) @@ -293,7 +340,7 @@ class ChuniServlet(BaseServlet): self.logger.error(f"Error handling v{version} method {endpoint} - {e}") return Response(zlib.compress(b'{"stat": "0"}')) - if resp == None: + if resp is None: resp = {"returnCode": 1} self.logger.debug(f"Response {resp}") @@ -311,4 +358,4 @@ class ChuniServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return Response(crypt.encrypt(padded)) \ No newline at end of file + return Response(crypt.encrypt(padded)) diff --git a/titles/chuni/luminous.py b/titles/chuni/luminous.py new file mode 100644 index 0000000..8f02820 --- /dev/null +++ b/titles/chuni/luminous.py @@ -0,0 +1,298 @@ +from datetime import timedelta +from typing import Dict + +from core.config import CoreConfig +from titles.chuni.sunplus import ChuniSunPlus +from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType +from titles.chuni.config import ChuniConfig + + +class ChuniLuminous(ChuniSunPlus): + def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None: + super().__init__(core_cfg, game_cfg) + self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # Does CARD MAKER 1.35 work this far up? + user_data["lastDataVersion"] = "2.20.00" + return user_data + + async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + mission_id = data["missionId"] + + progress_list = [] + point = 0 + + mission_data = await self.data.item.get_cmission(user_id, mission_id) + progress_data = await self.data.item.get_cmission_progress(user_id, mission_id) + + if mission_data and progress_data: + point = mission_data["point"] + + for progress in progress_data: + progress_list.append( + { + "order": progress["order"], + "stage": progress["stage"], + "progress": progress["progress"], + } + ) + + return { + "userId": user_id, + "missionId": mission_id, + "point": point, + "userCMissionProgressList": progress_list, + } + + async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + + net_battle = {} + net_battle_data = await self.data.profile.get_net_battle(user_id) + + if net_battle_data: + net_battle = { + "isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"], + "highestBattleRankId": net_battle_data["highestBattleRankId"], + "battleIconId": net_battle_data["battleIconId"], + "battleIconNum": net_battle_data["battleIconNum"], + "avatarEffectPoint": net_battle_data["avatarEffectPoint"], + } + + return { + "userId": user_id, + "userNetBattleData": net_battle, + } + + async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict: + # There is no game data for this, everything is server side. + # However, we can selectively show/hide events as data is imported into the server. + events = await self.data.static.get_enabled_events(self.version) + event_by_id = {evt["eventId"]: evt for evt in events} + conditions = [] + + # The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area + # (ep. I, ep. II, ep. III) are completed. + mystic_area_1_conditions = { + "mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1 + "length": 0, + "mapAreaConditionList": [], + } + mystic_area_1_added = False + + # Secret AREA: MUSIC GAME + if 14029 in event_by_id: + start_date = event_by_id[14029]["startDate"].strftime(self.date_time_format) + mission_in_progress_end_date = "2099-12-31 00:00:00.0" + + # The "MISSION in progress" trophy required to trigger the secret area + # is only available in the first CHUNITHM mission. If the second mission + # (event ID 14214) was imported into ARTEMiS, we disable the requirement + # for this trophy. + if 14214 in event_by_id: + mission_in_progress_end_date = (event_by_id[14214]["startDate"] - timedelta(hours=2)).strftime(self.date_time_format) + + conditions.extend([ + { + "mapAreaId": 2206201, # BlythE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA + "length": 1, + # Obtain the trophy "MISSION in progress". + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6832, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": mission_in_progress_end_date, + } + ], + }, + { + "mapAreaId": 2206203, # New York Back Raise + "length": 1, + # SS NightTheater's EXPERT chart and get the title + # "今宵、劇場に映し出される景色とは――――。" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6833, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206204, # Spasmodic + "length": 2, + # - Get 1 miss on Random (any difficulty) and get the title "当たり待ち" + # - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6834, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6835, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206205, # ΩΩPARTS + "length": 2, + # - S Sage EXPERT to get the title "マターリ進行キボンヌ" + # - Equip this title and play cab-to-cab with another person with this title + # to get "マターリしようよ". Disabled because it is difficult to play cab2cab + # on data setups. A network operator may consider re-enabling it by uncommenting + # the second condition. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6836, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + # { + # "type": MapAreaConditionType.TROPHY_OBTAINED.value, + # "conditionId": 6837, + # "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + # "startDate": start_date, + # "endDate": "2099-12-31 00:00:00.0", + # }, + ], + }, + { + "mapAreaId": 2206206, # Blow My Mind + "length": 1, + # SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT + # to get the title "Can you hear me?" + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.TROPHY_OBTAINED.value, + "conditionId": 6838, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + }, + { + "mapAreaId": 2206207, # VALLIS-NERIA + "length": 6, + # Finish the 6 other areas + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_AREA_CLEARED.value, + "conditionId": x, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + for x in range(2206201, 2206207) + ], + }, + ]) + + # LUMINOUS ep. I + if 14005 in event_by_id: + start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format) + + if not mystic_area_1_added: + conditions.append(mystic_area_1_conditions) + mystic_area_1_added = True + + mystic_area_1_conditions["length"] += 1 + mystic_area_1_conditions["mapAreaConditionList"].append( + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020701, + "logicalOpe": MapAreaConditionLogicalOperator.OR.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + ) + + conditions.append( + { + "mapAreaId": 3229302, # Mystic Rainbow of LUMINOUS Area 2, + "length": 1, + # Unlocks when LUMINOUS ep. I is completed. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020701, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ) + + # LUMINOUS ep. II + if 14251 in event_by_id: + start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format) + + if not mystic_area_1_added: + conditions.append(mystic_area_1_conditions) + mystic_area_1_added = True + + mystic_area_1_conditions["length"] += 1 + mystic_area_1_conditions["mapAreaConditionList"].append( + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020702, + "logicalOpe": MapAreaConditionLogicalOperator.OR.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + } + ) + + conditions.append( + { + "mapAreaId": 3229303, # Mystic Rainbow of LUMINOUS Area 3, + "length": 1, + # Unlocks when LUMINOUS ep. II is completed. + "mapAreaConditionList": [ + { + "type": MapAreaConditionType.MAP_CLEARED.value, + "conditionId": 3020702, + "logicalOpe": MapAreaConditionLogicalOperator.AND.value, + "startDate": start_date, + "endDate": "2099-12-31 00:00:00.0", + }, + ], + } + ) + + + return { + "length": len(conditions), + "gameMapAreaConditionList": conditions, + } diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 9709f00..2275a6e 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -32,6 +32,8 @@ class ChuniNew(ChuniBase): return "210" if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: return "215" + if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS: + return "220" async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: # use UTC time and convert it to JST time by adding +9 diff --git a/titles/chuni/read.py b/titles/chuni/read.py index 5082f7a..db7435c 100644 --- a/titles/chuni/read.py +++ b/titles/chuni/read.py @@ -48,9 +48,8 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(f"{root_dir}loginBonusPreset"): for dir in dirs: if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"): - with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -121,9 +120,8 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(evt_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Event.xml"): - with open(f"{root}/{dir}/Event.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -144,9 +142,8 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(music_dir): for dir in dirs: if path.exists(f"{root}/{dir}/Music.xml"): - with open(f"{root}/{dir}/Music.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -210,9 +207,8 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(charge_dir): for dir in dirs: if path.exists(f"{root}/{dir}/ChargeItem.xml"): - with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): @@ -240,9 +236,8 @@ class ChuniReader(BaseReader): for root, dirs, files in walk(avatar_dir): for dir in dirs: if path.exists(f"{root}/{dir}/AvatarAccessory.xml"): - with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp: - bytedata = fp.read() - strdata = bytedata.decode("UTF-8") + with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp: + strdata = fp.read() xml_root = ET.fromstring(strdata) for name in xml_root.findall("name"): diff --git a/titles/chuni/schema/item.py b/titles/chuni/schema/item.py index 5077e14..30db4b8 100644 --- a/titles/chuni/schema/item.py +++ b/titles/chuni/schema/item.py @@ -243,6 +243,36 @@ matching = Table( mysql_charset="utf8mb4", ) +cmission = Table( + "chuni_item_cmission", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("missionId", Integer, nullable=False), + Column("point", Integer), + UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"), + mysql_charset="utf8mb4", +) + +cmission_progress = Table( + "chuni_item_cmission_progress", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("missionId", Integer, nullable=False), + Column("order", Integer), + Column("stage", Integer), + Column("progress", Integer), + UniqueConstraint( + "user", "missionId", "order", name="chuni_item_cmission_progress_uk" + ), + mysql_charset="utf8mb4", +) + class ChuniItemData(BaseData): async def get_oldest_free_matching(self, version: int) -> Optional[Row]: @@ -594,3 +624,66 @@ class ChuniItemData(BaseData): ) return None return result.lastrowid + + async def put_cmission_progress( + self, user_id: int, mission_id: int, progress_data: Dict + ) -> Optional[int]: + progress_data["user"] = user_id + progress_data["missionId"] = mission_id + + sql = insert(cmission_progress).values(**progress_data) + conflict = sql.on_duplicate_key_update(**progress_data) + result = await self.execute(conflict) + + if result is None: + return None + + return result.lastrowid + + async def get_cmission_progress( + self, user_id: int, mission_id: int + ) -> Optional[List[Row]]: + sql = cmission_progress.select( + and_( + cmission_progress.c.user == user_id, + cmission_progress.c.missionId == mission_id, + ) + ).order_by(cmission_progress.c.order.asc()) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchall() + + async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]: + sql = cmission.select( + and_(cmission.c.user == user_id, cmission.c.missionId == mission_id) + ) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchone() + + async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]: + mission_data["user"] = user_id + + sql = insert(cmission).values(**mission_data) + conflict = sql.on_duplicate_key_update(**mission_data) + result = await self.execute(conflict) + + if result is None: + return None + + return result.lastrowid + + async def get_cmissions(self, user_id: int) -> Optional[List[Row]]: + sql = cmission.select(cmission.c.user == user_id) + result = await self.execute(sql) + + if result is None: + return None + + return result.fetchall() diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 9b4d645..daa7f4a 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -414,6 +414,18 @@ rating = Table( mysql_charset="utf8mb4", ) +net_battle = Table( + "chuni_profile_net_battle", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column("isRankUpChallengeFailed", Boolean), + Column("highestBattleRankId", Integer), + Column("battleIconId", Integer), + Column("battleIconNum", Integer), + Column("avatarEffectPoint", Integer), + mysql_charset="utf8mb4", +) class ChuniProfileData(BaseData): async def update_name(self, user_id: int, new_name: str) -> bool: @@ -806,4 +818,32 @@ class ChuniProfileData(BaseData): else: versions_raw = result.fetchall() versions = [row[0] for row in versions_raw] - return sorted(versions, reverse=True) \ No newline at end of file + return sorted(versions, reverse=True) + + async def put_net_battle(self, aime_id: int, net_battle_data: Dict) -> Optional[int]: + sql = insert(net_battle).values( + user=aime_id, + isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'], + highestBattleRankId=net_battle_data['highestBattleRankId'], + battleIconId=net_battle_data['battleIconId'], + battleIconNum=net_battle_data['battleIconNum'], + avatarEffectPoint=net_battle_data['avatarEffectPoint'], + ) + + conflict = sql.on_duplicate_key_update( + isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'], + highestBattleRankId=net_battle_data['highestBattleRankId'], + battleIconId=net_battle_data['battleIconId'], + battleIconNum=net_battle_data['battleIconNum'], + avatarEffectPoint=net_battle_data['avatarEffectPoint'], + ) + + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to put net battle data for user {aime_id}") + + async def get_net_battle(self, aime_id: int) -> Optional[Row]: + result = await self.execute(net_battle.select(net_battle.c.user == aime_id)) + if result: + return result.fetchone() diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index fa95374..766b4b9 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -242,6 +242,8 @@ class ChuniScoreData(BaseData): # Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved # This prevents tracks that are not accessible in your version from counting towards the 10 results romVer = { + 15: "2.20%", + 14: "2.15%", 13: "2.10%", 12: "2.05%", 11: "2.00%", diff --git a/titles/chuni/templates/chuni_index.jinja b/titles/chuni/templates/chuni_index.jinja index 69248e8..1854a89 100644 --- a/titles/chuni/templates/chuni_index.jinja +++ b/titles/chuni/templates/chuni_index.jinja @@ -5,7 +5,7 @@
{% include 'titles/chuni/templates/chuni_header.jinja' %} - {% if profile is defined and profile is not none and profile.id > 0 %} + {% if profile is defined and profile is not none and profile|length > 0 %}
diff --git a/titles/cxb/rev.py b/titles/cxb/rev.py index f811721..7e4f591 100644 --- a/titles/cxb/rev.py +++ b/titles/cxb/rev.py @@ -40,6 +40,7 @@ class CxbRev(CxbBase): score_data["slow2"], score_data["fail"], score_data["combo"], + score_data["grade"], ) return {"data": True} return {"data": True} diff --git a/titles/cxb/schema/score.py b/titles/cxb/schema/score.py index 7be33d8..5acc1ae 100644 --- a/titles/cxb/schema/score.py +++ b/titles/cxb/schema/score.py @@ -39,6 +39,7 @@ playlog = Table( Column("slow2", Integer), Column("fail", Integer), Column("combo", Integer), + Column("grade", Integer), Column("date_scored", TIMESTAMP, server_default=func.now()), mysql_charset="utf8mb4", ) @@ -104,6 +105,7 @@ class CxbScoreData(BaseData): this_slow2: int, fail: int, combo: int, + grade: int, ) -> Optional[int]: """ Add an entry to the user's play log @@ -123,6 +125,7 @@ class CxbScoreData(BaseData): slow2=this_slow2, fail=fail, combo=combo, + grade=grade, ) result = await self.execute(sql) diff --git a/titles/diva/__init__.py b/titles/diva/__init__.py index d298ba2..7bfa2cc 100644 --- a/titles/diva/__init__.py +++ b/titles/diva/__init__.py @@ -2,8 +2,10 @@ from titles.diva.index import DivaServlet from titles.diva.const import DivaConstants from titles.diva.database import DivaData from titles.diva.read import DivaReader +from .frontend import DivaFrontend index = DivaServlet database = DivaData reader = DivaReader +frontend = DivaFrontend game_codes = [DivaConstants.GAME_CODE] diff --git a/titles/diva/base.py b/titles/diva/base.py index 7fe3fb5..3b96848 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -431,7 +431,7 @@ class DivaBase: profile = await self.data.profile.get_profile(data["pd_id"], self.version) profile_shop = await self.data.item.get_shop(data["pd_id"], self.version) if profile is None: - return + return {} mdl_have = "F" * 250 # generate the mdl_have string if "unlock_all_modules" is disabled diff --git a/titles/diva/frontend.py b/titles/diva/frontend.py new file mode 100644 index 0000000..cc5c332 --- /dev/null +++ b/titles/diva/frontend.py @@ -0,0 +1,182 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import DivaData +from .config import DivaConfig +from .const import DivaConstants + +class DivaFrontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = DivaData(cfg) + self.game_cfg = DivaConfig() + if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}")) + ) + self.nav_name = "diva" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), + Route("/update.name", self.update_name, methods=['POST']), + Route("/update.lv", self.update_lv, methods=['POST']), + ] + + async def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/diva/templates/diva_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + profile = await self.data.profile.get_profile(usr_sesh.user_id, 1) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile + ), media_type="text/html; charset=utf-8") + return resp + else: + return RedirectResponse("/gate") + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/diva/templates/diva_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + path_index = request.path_params.get("index") + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + score_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs(user_id, index, 20) #Maybe change to the playlog instead of direct scores + playlog_with_title = [] + for record in playlog: + song = await self.data.static.get_music_chart(record[2], record[3], record[4]) + if song: + title = song.title + vocaloid_arranger = song.vocaloid_arranger + else: + title = "Unknown" + vocaloid_arranger = "Unknown" + playlog_with_title.append({ + "raw": record, + "title": title, + "vocaloid_arranger": vocaloid_arranger + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 300) + + async def update_name(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate") + + form_data = await request.form() + new_name: str = form_data.get("new_name") + new_name_full = "" + + if not new_name: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_name) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_name: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_name_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_profile(usr_sesh.user_id, player_name=new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/diva", 303) + + async def update_lv(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate") + + form_data = await request.form() + new_lv: str = form_data.get("new_lv") + new_lv_full = "" + + if not new_lv: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_lv) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_lv: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_lv_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_lv_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_lv_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_profile(usr_sesh.user_id, lv_str=new_lv_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/diva", 303) diff --git a/titles/diva/index.py b/titles/diva/index.py index f414ab2..01e5eeb 100644 --- a/titles/diva/index.py +++ b/titles/diva/index.py @@ -79,7 +79,7 @@ class DivaServlet(BaseServlet): return True - async def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes: + async def render_POST(self, request: Request) -> bytes: req_raw = await request.body() url_header = request.headers @@ -98,8 +98,17 @@ class DivaServlet(BaseServlet): self.logger.info(f"Binary {bin_req_data['cmd']} Request") self.logger.debug(bin_req_data) - handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") - resp = handler(bin_req_data) + try: + handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") + resp = handler(bin_req_data) + + except AttributeError as e: + self.logger.warning(f"Unhandled {bin_req_data['cmd']} request {e}") + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok") + + except Exception as e: + self.logger.error(f"Error handling method {e}") + return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok") self.logger.debug( f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" diff --git a/titles/diva/read.py b/titles/diva/read.py index 97c9481..64603ca 100644 --- a/titles/diva/read.py +++ b/titles/diva/read.py @@ -183,7 +183,11 @@ class DivaReader(BaseReader): pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) for pv_id, pv_data in pv_list.items(): - song_id = int(pv_id.split("_")[1]) + try: + song_id = int(pv_id.split("_")[1]) + except ValueError: + self.logger.error(f"Invalid song ID format: {pv_id}") + continue if "songinfo" not in pv_data: continue if "illustrator" not in pv_data["songinfo"]: diff --git a/titles/diva/schema/customize.py b/titles/diva/schema/customize.py index 838a7d2..21ccf7d 100644 --- a/titles/diva/schema/customize.py +++ b/titles/diva/schema/customize.py @@ -54,7 +54,7 @@ class DivaCustomizeItemData(BaseData): Given a game version and an aime id, return the cstmz_itm_have hex string required for diva directly """ - items_list = self.get_customize_items(aime_id, version) + items_list = await self.get_customize_items(aime_id, version) if items_list is None: items_list = [] item_have = 0 diff --git a/titles/diva/schema/module.py b/titles/diva/schema/module.py index a6286ee..de091b1 100644 --- a/titles/diva/schema/module.py +++ b/titles/diva/schema/module.py @@ -50,7 +50,7 @@ class DivaModuleData(BaseData): Given a game version and an aime id, return the mdl_have hex string required for diva directly """ - module_list = self.get_modules(aime_id, version) + module_list = await self.get_modules(aime_id, version) if module_list is None: module_list = [] module_have = 0 diff --git a/titles/diva/schema/profile.py b/titles/diva/schema/profile.py index f3d00ae..10d3b51 100644 --- a/titles/diva/schema/profile.py +++ b/titles/diva/schema/profile.py @@ -90,7 +90,7 @@ class DivaProfileData(BaseData): return None return result.lastrowid - async def update_profile(self, aime_id: int, **profile_args) -> None: + async def update_profile(self, aime_id: int, **profile_args) -> bool: """ Given an aime_id update the profile corresponding to the arguments which are the diva_profile Columns @@ -102,7 +102,9 @@ class DivaProfileData(BaseData): self.logger.error( f"update_profile: failed to update profile! profile: {aime_id}" ) - return None + return False + + return True async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]: """ diff --git a/titles/diva/schema/score.py b/titles/diva/schema/score.py index e802a41..ce89f74 100644 --- a/titles/diva/schema/score.py +++ b/titles/diva/schema/score.py @@ -239,3 +239,23 @@ class DivaScoreData(BaseData): if result is None: return None return result.fetchall() + + async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]: + sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc()) + + if limit: + sql = sql.limit(limit) + if idx: + sql = sql.offset(idx) + + result = await self.execute(sql) + if result: + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"aimu_id {aime_id} has no scores ") + return None + return result.scalar() diff --git a/titles/diva/templates/css/diva_style.css b/titles/diva/templates/css/diva_style.css new file mode 100644 index 0000000..672db0f --- /dev/null +++ b/titles/diva/templates/css/diva_style.css @@ -0,0 +1,195 @@ +.diva-header { + text-align: center; +} + +ul.diva-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.diva-navi li { + display: inline-block; +} + +ul.diva-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.diva-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.diva-navi li a.active { + background-color: #4CAF50; +} + +ul.diva-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.diva-navi li.right, + ul.diva-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.basic { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/diva/templates/diva_header.jinja b/titles/diva/templates/diva_header.jinja new file mode 100644 index 0000000..b92379a --- /dev/null +++ b/titles/diva/templates/diva_header.jinja @@ -0,0 +1,17 @@ +
+

diva

+ +
+ \ No newline at end of file diff --git a/titles/diva/templates/diva_index.jinja b/titles/diva/templates/diva_index.jinja new file mode 100644 index 0000000..c2f0888 --- /dev/null +++ b/titles/diva/templates/diva_index.jinja @@ -0,0 +1,111 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/diva/templates/diva_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
Player name:{{ profile[3] }} + + Level string:{{ profile[4] }} + +
Lvl:{{ profile[5] }}
Lvl points:{{ profile[6] }}
Vocaloid points:{{ profile[7] }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/titles/diva/templates/diva_playlog.jinja b/titles/diva/templates/diva_playlog.jinja new file mode 100644 index 0000000..c5a5618 --- /dev/null +++ b/titles/diva/templates/diva_playlog.jinja @@ -0,0 +1,169 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/diva/templates/diva_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Score counts: {{ playlog_count }}

+ {% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %} + {% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %} + {% for record in playlog %} +
+
+
+
+
+
{{ record.title }}
+
+
{{ record.vocaloid_arranger }}
+
+
+
{{record.raw.date_scored}}
+
+
+
+
+

{{ record.raw.score }}

+

{{ record.raw.atn_pnt / 100 }}%

+
{{ difficultyName[record.raw.difficulty] }}
+
+
+ + + + + + + + + + + + + + + + + + + + + +
COOL{{ record.raw.cool }}
FINE{{ record.raw.fine }}
SAFE{{ record.raw.safe }}
SAD{{ record.raw.sad }}
WORST{{ record.raw.worst }}
+
+
+
{{ record.raw.max_combo }}
+ {% if record.raw.clr_kind == -1 %} +
{{ clearState[0] }}
+ {% elif record.raw.clr_kind == 2 %} +
{{ clearState[1] }}
+ {% elif record.raw.clr_kind == 3 %} +
{{ clearState[2] }}
+ {% elif record.raw.clr_kind == 4 %} +
{{ clearState[3] }}
+ {% elif record.raw.clr_kind == 5 %} +
{{ clearState[4] }}
+ {% endif %} + {% if record.raw.clr_kind == -1 %} +
NOT CLEAR
+ {% else %} +
CLEAR
+ {% endif %} +
+
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Score information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 74cfddf..234e864 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -2,10 +2,12 @@ from titles.mai2.index import Mai2Servlet from titles.mai2.const import Mai2Constants from titles.mai2.database import Mai2Data from titles.mai2.read import Mai2Reader +from .frontend import Mai2Frontend index = Mai2Servlet database = Mai2Data reader = Mai2Reader +frontend = Mai2Frontend game_codes = [ Mai2Constants.GAME_CODE_DX, Mai2Constants.GAME_CODE_FINALE, diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 90597d1..b3c6a1d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -4,6 +4,7 @@ import logging from base64 import b64decode from os import path, stat, remove from PIL import ImageFile +from random import randint import pytz from core.config import CoreConfig @@ -470,7 +471,27 @@ class Mai2Base: } async def handle_get_user_present_api_request(self, data: Dict) -> Dict: - return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + items: List[Dict[str, Any]] = [] + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + + return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items} async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: return {} @@ -886,3 +907,45 @@ class Mai2Base: self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually") return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'} + + async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs + next_index = data.get("nextIndex", 0) + max_ct = data.get("maxCount", 100) # always 100 + is_all = data.get("isAllFavoriteItem", False) # always false + id_list: List[Dict] = [] + + if user_id: + if kind == 1: + fav_music = await self.data.item.get_fav_music(user_id) + if fav_music: + for fav in fav_music: + id_list.append({"orderId": 0, "id": fav["musicId"]}) + if len(id_list) >= 100: # Lazy but whatever + break + + elif kind == 2: + rivals = await self.data.profile.get_rivals_game(user_id) + if rivals: + for rival in rivals: + id_list.append({"orderId": 0, "id": rival["rival"]}) + + return { + "userId": user_id, + "kind": kind, + "nextIndex": 0, + "userFavoriteItemList": id_list, + } + + async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendRateMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + """ + userRecommendSelectionMusicIdList: list[int] + """ + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py index f04b215..38049a1 100644 --- a/titles/mai2/buddies.py +++ b/titles/mai2/buddies.py @@ -17,16 +17,3 @@ class Mai2Buddies(Mai2FestivalPlus): # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.40.00" return user_data - - async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: - # TODO: Added in 1.41, implement this? - user_id = data["userId"] - version = data.get("version", 1041000) - user_playlog_list = data.get("userPlaylogList", []) - - return { - "userId": user_id, - "itemKind": -1, - "itemId": -1, - } - diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 4423824..7a067d7 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -196,10 +196,17 @@ class Mai2DX(Mai2Base): if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: + if item["itemKind"] == 4: + item_id = item["itemId"] % 1000000 + item_kind = item["itemId"] // 1000000 + else: + item_id = item["itemId"] + item_kind = item["itemKind"] + await self.data.item.put_item( user_id, - int(item["itemKind"]), - item["itemId"], + item_kind, + item_id, item["stock"], item["isValid"], ) @@ -325,18 +332,39 @@ class Mai2DX(Mai2Base): } async def handle_get_user_item_api_request(self, data: Dict) -> Dict: - kind = int(data["nextIndex"] / 10000000000) - next_idx = int(data["nextIndex"] % 10000000000) - user_item_list = await self.data.item.get_items(data["userId"], kind) - + kind = data["nextIndex"] // 10000000000 + next_idx = data["nextIndex"] % 10000000000 items: List[Dict[str, Any]] = [] - for i in range(next_idx, len(user_item_list)): - tmp = user_item_list[i]._asdict() - tmp.pop("user") - tmp.pop("id") - items.append(tmp) - if len(items) >= int(data["maxCount"]): - break + + if kind == 4: # presents + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + + else: + user_item_list = await self.data.item.get_items(data["userId"], kind) + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): + break xout = kind * 10000000000 + next_idx + len(items) @@ -563,33 +591,76 @@ class Mai2DX(Mai2Base): return {"userId": data["userId"], "length": 0, "userRegionList": []} async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - rival_id = data["rivalId"] + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) - """ - class UserRivalData: - rivalId: int - rivalName: str - """ - return {"userId": user_id, "userRivalData": {}} + if not user_id or not rival_id: return {} + + rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version) + if not rival_pf: return {} + + return { + "userId": user_id, + "userRivalData": { + "rivalId": rival_id, + "rivalName": rival_pf['userName'] + } + } async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: + user_id = data.get("userId", 0) + rival_id = data.get("rivalId", 0) + next_index = data.get("nextIndex", 0) + max_ct = 100 + upper_lim = next_index + max_ct + rival_music_list: Dict[int, List] = {} + + songs = await self.data.score.get_best_scores(rival_id) + if songs is None: + self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!") + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": 0, + "userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax + } + + num_user_songs = len(songs) + + for x in range(next_index, upper_lim): + if x >= num_user_songs: + break + + tmp = songs[x]._asdict() + if tmp['musicId'] in rival_music_list: + rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]) + + else: + if len(rival_music_list) >= max_ct: + break + rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}] + + next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim + self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})") + + return { + "userId": user_id, + "rivalId": rival_id, + "nextIndex": next_index, + "userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()] + } + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? user_id = data["userId"] - rival_id = data["rivalId"] - next_idx = data["nextIndex"] - rival_music_levels = data["userRivalMusicLevelList"] - - """ - class UserRivalMusicList: - class UserRivalMusicDetailList: - level: int - achievement: int - deluxscoreMax: int - - musicId: int - userRivalMusicDetailList: list[UserRivalMusicDetailList] - """ - return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []} + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } async def handle_get_user_music_api_request(self, data: Dict) -> Dict: user_id = data.get("userId", 0) @@ -636,3 +707,208 @@ class Mai2DX(Mai2Base): return ret ret['loginId'] = ret.get('loginCount', 0) return ret + + # CardMaker support added in Universe + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + if p is None: + return {} + + return { + "userName": p["userName"], + "rating": p["playerRating"], + # hardcode lastDataVersion for CardMaker + "lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version + # checks if the user is still logged in + "isLogin": False, + "isExistSellingCard": True, + } + + async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: + # user already exists, because the preview checks that already + p = await self.data.profile.get_profile_detail(data["userId"], self.version) + + cards = await self.data.card.get_user_cards(data["userId"]) + if cards is None or len(cards) == 0: + # This should never happen + self.logger.error( + f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" + ) + return {} + + # get the dict representation of the row so we can modify values + user_data = p._asdict() + + # remove the values the game doesn't want + user_data.pop("id") + user_data.pop("user") + user_data.pop("version") + + return {"userId": data["userId"], "userData": user_data} + + async def handle_cm_login_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_logout_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} + + async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: + selling_cards = await self.data.static.get_enabled_cards(self.version) + if selling_cards is None: + return {"length": 0, "sellingCardList": []} + + selling_card_list = [] + for card in selling_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("version") + tmp.pop("cardName") + tmp.pop("enabled") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeStartDate"] = datetime.strftime( + tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["noticeEndDate"] = datetime.strftime( + tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT + ) + + selling_card_list.append(tmp) + + return {"length": len(selling_card_list), "sellingCardList": selling_card_list} + + async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: + user_cards = await self.data.item.get_cards(data["userId"]) + if user_cards is None: + return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} + + max_ct = data["maxCount"] + next_idx = data["nextIndex"] + start_idx = next_idx + end_idx = max_ct + start_idx + + if len(user_cards[start_idx:]) > max_ct: + next_idx += max_ct + else: + next_idx = 0 + + card_list = [] + for card in user_cards: + tmp = card._asdict() + tmp.pop("id") + tmp.pop("user") + + tmp["startDate"] = datetime.strftime( + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) + tmp["endDate"] = datetime.strftime( + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) + card_list.append(tmp) + + return { + "returnCode": 1, + "length": len(card_list[start_idx:end_idx]), + "nextIndex": next_idx, + "userCardList": card_list[start_idx:end_idx], + } + + async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: + await self.handle_get_user_item_api_request(data) + + async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: + characters = await self.data.item.get_characters(data["userId"]) + + chara_list = [] + for chara in characters: + chara_list.append( + { + "characterId": chara["characterId"], + # no clue why those values are even needed + "point": 0, + "count": 0, + "level": chara["level"], + "nextAwake": 0, + "nextAwakePercent": 0, + "favorite": False, + "awakening": chara["awakening"], + "useCount": chara["useCount"], + } + ) + + return { + "returnCode": 1, + "length": len(chara_list), + "userCharacterList": chara_list, + } + + async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: + return {"length": 0, "userPrintDetailList": []} + + async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + upsert = data["userPrintDetail"] + + # set a random card serial number + serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) + + # calculate start and end date of the card + start_date = datetime.utcnow() + end_date = datetime.utcnow() + timedelta(days=15) + + user_card = upsert["userCard"] + await self.data.item.put_card( + user_id, + user_card["cardId"], + user_card["cardTypeId"], + user_card["charaId"], + user_card["mapId"], + # add the correct start date and also the end date in 15 days + start_date, + end_date, + ) + + # get the profile extend to save the new bought card + extend = await self.data.profile.get_profile_extend(user_id, self.version) + if extend: + extend = extend._asdict() + # parse the selectedCardList + # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) + selected_cards: List = extend["selectedCardList"] + + # if no pass is already added, add the corresponding pass + if not user_card["cardTypeId"] in selected_cards: + selected_cards.insert(0, user_card["cardTypeId"]) + + extend["selectedCardList"] = selected_cards + await self.data.profile.put_profile_extend(user_id, self.version, extend) + + # properly format userPrintDetail for the database + upsert.pop("userCard") + upsert.pop("serialId") + upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") + + await self.data.item.put_user_print_detail(user_id, serial_id, upsert) + + return { + "returnCode": 1, + "orderId": 0, + "serialId": serial_id, + "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), + "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), + } + + async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: + return { + "returnCode": 1, + "orderId": 0, + "serialId": data["userPrintlog"]["serialId"], + } + + async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: + return {"returnCode": 1} diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py index 94ce3ec..451f0ba 100644 --- a/titles/mai2/festival.py +++ b/titles/mai2/festival.py @@ -20,18 +20,6 @@ class Mai2Festival(Mai2UniversePlus): async def handle_user_login_api_request(self, data: Dict) -> Dict: user_login = await super().handle_user_login_api_request(data) - # useless? + # TODO: Make use of this user_login["Bearer"] = "ARTEMiSTOKEN" return user_login - - async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendRateMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendRateMusicIdList": []} - - async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: - """ - userRecommendSelectionMusicIdList: list[int] - """ - return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/festivalplus.py b/titles/mai2/festivalplus.py index 375d546..3314e34 100644 --- a/titles/mai2/festivalplus.py +++ b/titles/mai2/festivalplus.py @@ -17,22 +17,3 @@ class Mai2FestivalPlus(Mai2Festival): # hardcode lastDataVersion for CardMaker user_data["lastDataVersion"] = "1.35.00" return user_data - - async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: - user_id = data.get("userId", 0) - kind = data.get("kind", 2) - next_index = data.get("nextIndex", 0) - max_ct = data.get("maxCount", 100) - is_all = data.get("isAllFavoriteItem", False) - - """ - class userFavoriteItemList: - orderId: int - id: int - """ - return { - "userId": user_id, - "kind": kind, - "nextIndex": 0, - "userFavoriteItemList": [], - } diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py new file mode 100644 index 0000000..635c2fa --- /dev/null +++ b/titles/mai2/frontend.py @@ -0,0 +1,190 @@ +from typing import List +from starlette.routing import Route, Mount +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse +from os import path +import yaml +import jinja2 + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import Mai2Data +from .config import Mai2Config +from .const import Mai2Constants + +class Mai2Frontend(FE_Base): + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = Mai2Data(cfg) + self.game_cfg = Mai2Config() + if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}")) + ) + self.nav_name = "maimai" + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Mount("/playlog", routes=[ + Route("/", self.render_GET_playlog, methods=['GET']), + Route("/{index}", self.render_GET_playlog, methods=['GET']), + ]), + Route("/update.name", self.update_name, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), + ] + + async def render_GET(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_index.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) + profile = [] + if versions: + # maimai_version is -1 means it is not initialized yet, select a default version from existing. + if usr_sesh.maimai_version < 0: + usr_sesh.maimai_version = versions[0]['version'] + profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) + versions = [x['version'] for x in versions] + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + profile=profile, + version_list=Mai2Constants.VERSION_STRING, + versions=versions, + cur_version=usr_sesh.maimai_version + ), media_type="text/html; charset=utf-8") + + if usr_sesh.maimai_version >= 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.delete_cookie("ARTEMIS_SESH") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + + else: + return RedirectResponse("/gate/", 303) + + async def render_GET_playlog(self, request: Request) -> bytes: + template = self.environment.get_template( + "titles/mai2/templates/mai2_playlog.jinja" + ) + usr_sesh = self.validate_session(request) + if not usr_sesh: + print("wtf") + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + if usr_sesh.maimai_version < 0: + print(usr_sesh.maimai_version) + return RedirectResponse("/game/mai2/", 303) + path_index = request.path_params.get('index') + if not path_index or int(path_index) < 1: + index = 0 + else: + index = int(path_index) - 1 # 0 and 1 are 1st page + user_id = usr_sesh.user_id + playlog_count = await self.data.score.get_user_playlogs_count(user_id) + if playlog_count < index * 20 : + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + playlog_count=0 + ), media_type="text/html; charset=utf-8") + playlog = await self.data.score.get_playlogs(user_id, index, 20) + playlog_with_title = [] + for record in playlog: + music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level) + if music_chart: + difficultyNum=music_chart.chartId + difficulty=music_chart.difficulty + artist=music_chart.artist + title=music_chart.title + else: + difficultyNum=0 + difficulty=0 + artist="unknown" + title="musicid: " + str(record.musicId) + playlog_with_title.append({ + "raw": record, + "title": title, + "difficultyNum": difficultyNum, + "difficulty": difficulty, + "artist": artist, + }) + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + playlog=playlog_with_title, + playlog_count=playlog_count + ), media_type="text/html; charset=utf-8") + else: + return RedirectResponse("/gate/", 303) + + async def update_name(self, request: Request) -> bytes: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + form_data = await request.form() + new_name: str = form_data.get("new_name") + new_name_full = "" + + if not new_name: + return RedirectResponse("/gate/?e=4", 303) + + if len(new_name) > 8: + return RedirectResponse("/gate/?e=8", 303) + + for x in new_name: # FIXME: This will let some invalid characters through atm + o = ord(x) + try: + if o == 0x20: + new_name_full += chr(0x3000) + elif o < 0x7F and o > 0x20: + new_name_full += chr(o + 0xFEE0) + elif o <= 0x7F: + self.logger.warn(f"Invalid ascii character {o:02X}") + return RedirectResponse("/gate/?e=4", 303) + else: + new_name_full += x + + except Exception as e: + self.logger.error(f"Something went wrong parsing character {o:04X} - {e}") + return RedirectResponse("/gate/?e=4", 303) + + if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full): + return RedirectResponse("/gate/?e=999", 303) + + return RedirectResponse("/game/mai2/?s=1", 303) + + async def version_change(self, request: Request): + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + if usr_sesh.user_id > 0: + form_data = await request.form() + maimai_version = form_data.get("version") + self.logger.info(f"version change to: {maimai_version}") + if(maimai_version.isdigit()): + usr_sesh.maimai_version=int(maimai_version) + encoded_sesh = self.encode_session(usr_sesh) + self.logger.info(f"Created session with JWT {encoded_sesh}") + resp = RedirectResponse("/game/mai2/", 303) + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + return resp + else: + return RedirectResponse("/gate/", 303) \ No newline at end of file diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 9aaf592..d53ebbc 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -2,8 +2,8 @@ from core.data.schema import BaseData, metadata from datetime import datetime from typing import Optional, Dict, List -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, or_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BIGINT, INTEGER from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert @@ -134,6 +134,20 @@ favorite = Table( mysql_charset="utf8mb4", ) +fav_music = Table( + "mai2_item_favorite_music", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("musicId", Integer, nullable=False), + UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"), + mysql_charset="utf8mb4", +) + charge = Table( "mai2_item_charge", metadata, @@ -184,6 +198,20 @@ print_detail = Table( mysql_charset="utf8mb4", ) +present = Table( + "mai2_item_present", + metadata, + Column('id', BIGINT, primary_key=True, nullable=False), + Column('version', INTEGER), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("itemKind", INTEGER, nullable=False), + Column("itemId", INTEGER, nullable=False), + Column("stock", INTEGER, nullable=False, server_default="1"), + Column("startDate", TIMESTAMP), + Column("endDate", TIMESTAMP), + UniqueConstraint("version", "user", "itemKind", "itemId", name="mai2_item_present_uk"), + mysql_charset="utf8mb4", +) class Mai2ItemData(BaseData): async def put_item( @@ -451,6 +479,30 @@ class Mai2ItemData(BaseData): return None return result.fetchall() + async def get_fav_music(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(fav_music.select(fav_music.c.user == user_id)) + if result: + return result.fetchall() + + async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]: + sql = insert(fav_music).values( + user = user_id, + musicId = music_id + ) + + conflict = sql.on_duplicate_key_update(musicId = music_id) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!") + + async def remove_fav_music(self, user_id: int, music_id: int) -> None: + result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id))) + if not result: + self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!") + async def put_card( self, user_id: int, @@ -548,3 +600,49 @@ class Mai2ItemData(BaseData): ) return None return result.lastrowid + + async def put_present(self, item_kind: int, item_id: int, version: int = None, user_id: int = None, start_date: datetime = None, end_date: datetime = None) -> Optional[int]: + sql = insert(present).values( + version = version, + user = user_id, + itemKind = item_kind, + itemId = item_id, + startDate = start_date, + endDate = end_date + ) + + conflict = sql.on_duplicate_key_update( + startDate = start_date, + endDate = end_date + ) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add present item {item_id}!") + + async def get_presents_by_user(self, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.user == user_id, present.c.user is None))) + if result: + return result.fetchall() + + async def get_presents_by_version(self, ver: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.version == ver, present.c.version is None))) + if result: + return result.fetchall() + + async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select( + and_( + or_(present.c.user == user_id, present.c.user == None), + or_(present.c.version == ver, present.c.version == None) + ) + )) + if result: + return result.fetchall() + + async def get_present_by_id(self, present_id: int) -> Optional[Row]: + result = await self.execute(present.select(present.c.id == present_id)) + if result: + return result.fetchone() diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 8f1d5f3..c191a1a 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -491,8 +491,31 @@ consec_logins = Table( mysql_charset="utf8mb4", ) +rival = Table( + "mai2_user_rival", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column( + "rival", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("show", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "rival", name="mai2_user_rival_uk"), + mysql_charset="utf8mb4", +) class Mai2ProfileData(BaseData): + async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(detail.select(detail.c.user == user_id)) + if result: + return result.fetchall() + async def put_profile_detail( self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True ) -> Optional[Row]: @@ -843,3 +866,52 @@ class Mai2ProfileData(BaseData): if result is None: return None return result.fetchone() + + async def get_rivals(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(rival.c.user == user_id)) + if result: + return result.fetchall() + + async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]: + result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3)) + if result: + return result.fetchall() + + async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None: + sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values( + show = is_shown + ) + + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}") + + async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]: + sql = insert(rival).values( + user = user_id, + rival = rival_id + ) + + conflict = sql.on_duplicate_key_update(rival = rival_id) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!") + + async def remove_rival(self, user_id: int, rival_id: int) -> None: + result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id))) + if not result: + self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!") + + async def update_name(self, user_id: int, new_name: str) -> bool: + sql = detail.update(detail.c.user == user_id).values( + userName=new_name + ) + result = await self.execute(sql) + + if result is None: + self.logger.warning(f"Failed to set user {user_id} name to {new_name}") + return False + return True diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index d13faae..d4ea5b9 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -319,16 +319,16 @@ class Mai2ScoreData(BaseData): sql = best_score.select( and_( best_score.c.user == user_id, - (best_score.c.song_id == song_id) if song_id is not None else True, + (best_score.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) else: sql = best_score_old.select( and_( best_score_old.c.user == user_id, - (best_score_old.c.song_id == song_id) if song_id is not None else True, + (best_score_old.c.musicId == song_id) if song_id is not None else True, ) - ) + ).order_by(best_score.c.musicId).order_by(best_score.c.level) result = await self.execute(sql) if result is None: @@ -398,3 +398,23 @@ class Mai2ScoreData(BaseData): if result is None: return None return result.fetchall() + + async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]: + sql = playlog.select(playlog.c.user == user_id) + + if limit: + sql = sql.limit(limit) + if idx: + sql = sql.offset(idx * limit) + + result = await self.execute(sql) + if result: + return result.fetchall() + + async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]: + sql = select(func.count()).where(playlog.c.user == aime_id) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"aime_id {aime_id} has no playlog ") + return None + return result.scalar() diff --git a/titles/mai2/templates/css/mai2_style.css b/titles/mai2/templates/css/mai2_style.css new file mode 100644 index 0000000..4aceab8 --- /dev/null +++ b/titles/mai2/templates/css/mai2_style.css @@ -0,0 +1,195 @@ +.mai2-header { + text-align: center; +} + +ul.mai2-navi { + list-style-type: none; + padding: 0; + overflow: hidden; + background-color: #333; + text-align: center; + display: inline-block; +} + +ul.mai2-navi li { + display: inline-block; +} + +ul.mai2-navi li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; +} + +ul.mai2-navi li a:hover:not(.active) { + background-color: #111; +} + +ul.mai2-navi li a.active { + background-color: #4CAF50; +} + +ul.mai2-navi li.right { + float: right; +} + +@media screen and (max-width: 600px) { + + ul.mai2-navi li.right, + ul.mai2-navi li { + float: none; + display: block; + text-align: center; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; + border-collapse: separate; + overflow: hidden; + background-color: #555555; + +} + +th, td { + text-align: left; + border: none; + +} + +th { + color: white; +} + +.table-rowdistinct tr:nth-child(even) { + background-color: #303030; +} + +.table-rowdistinct tr:nth-child(odd) { + background-color: #555555; +} + +caption { + text-align: center; + color: white; + font-size: 18px; + font-weight: bold; +} + +.table-large { + margin: 16px; +} + +.table-large th, +.table-large td { + padding: 8px; +} + +.table-small { + width: 100%; + margin: 4px; +} + +.table-small th, +.table-small td { + padding: 2px; +} + +.bg-card { + background-color: #555555; +} + +.card-hover { + transition: all 0.2s ease-in-out; +} + +.card-hover:hover { + transform: scale(1.02); +} + +.basic { + color: #28a745; + font-weight: bold; +} + +.hard { + color: #ffc107; + + font-weight: bold; +} + +.expert { + color: #dc3545; + font-weight: bold; +} + +.master { + color: #dd09e8; + font-weight: bold; +} + +.ultimate { + color: #000000; + font-weight: bold; +} + +.score { + color: #ffffff; + font-weight: bold; +} + +.rainbow { + background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-weight: bold; +} + +.platinum { + color: #FFFF00; + font-weight: bold; +} + +.gold { + color: #FFFF00; + font-weight: bold; +} + +.scrolling-text { + overflow: hidden; +} + +.scrolling-text p { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h6 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling-text h5 { + white-space: nowrap; + display: inline-block; + +} + +.scrolling { + animation: scroll 10s linear infinite; +} + +@keyframes scroll { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja new file mode 100644 index 0000000..4a4cb86 --- /dev/null +++ b/titles/mai2/templates/mai2_header.jinja @@ -0,0 +1,17 @@ +
+

maimai

+ +
+ \ No newline at end of file diff --git a/titles/mai2/templates/mai2_index.jinja b/titles/mai2/templates/mai2_index.jinja new file mode 100644 index 0000000..6490fdc --- /dev/null +++ b/titles/mai2/templates/mai2_index.jinja @@ -0,0 +1,134 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if profile is defined and profile is not none and profile|length > 0 %} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
OVERVIEW
{{ profile.userName }} + +
version: + + {% if versions | length > 1 %} +

You have {{ versions | length }} versions.

+ {% endif %} +
Rating: + + {{ profile.playerRating }} + + + (highest: {{ profile.highestRating }}) + +
Play Counts:{{ profile.playCount }}
Last Play Date:{{ profile.lastPlayDate }}
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
SCORE
Total Delux Score:{{ profile.totalDeluxscore }}
Total Basic Delux Score:{{ profile.totalBasicDeluxscore }}
Total Advanced Delux Score:{{ profile.totalAdvancedDeluxscore }}
Total Expert Delux Score:{{ profile.totalExpertDeluxscore }}
Total Master Delux Score:{{ profile.totalMasterDeluxscore }}
Total ReMaster Delux Score:{{ profile.totalReMasterDeluxscore }}
+
+
+
+ {% if error is defined %} + {% include "core/templates/widgets/err_banner.jinja" %} + {% endif %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No profile information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_playlog.jinja b/titles/mai2/templates/mai2_playlog.jinja new file mode 100644 index 0000000..3e1d5fd --- /dev/null +++ b/titles/mai2/templates/mai2_playlog.jinja @@ -0,0 +1,225 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+ {% include 'titles/mai2/templates/mai2_header.jinja' %} + {% if playlog is defined and playlog is not none %} +
+

Playlog counts: {{ playlog_count }}

+ {% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %} + {% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %} + {% for record in playlog %} +
+
+
+
+
{{ record.title }}
+
+
{{ record.artist }}
+
+
+
{{ record.raw.userPlayDate }}
+
TRACK {{ record.raw.trackNo }}
+
+
+
+
+

{{ record.raw.deluxscore }}

+

{{ rankName[record.raw.rank] }}

+
+ {{ difficultyName[record.raw.level] }}  {{ record.difficulty }} +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
CRITICAL PERFECT + Tap: {{ record.raw.tapCriticalPerfect }}
+ Hold: {{ record.raw.holdCriticalPerfect }}
+ Slide: {{ record.raw.slideCriticalPerfect }}
+ Touch: {{ record.raw.touchCriticalPerfect }}
+ Break: {{ record.raw.breakCriticalPerfect }} +
PERFECT + Tap: {{ record.raw.tapPerfect }}
+ Hold: {{ record.raw.holdPerfect }}
+ Slide: {{ record.raw.slidePerfect }}
+ Touch: {{ record.raw.touchPerfect }}
+ Break: {{ record.raw.breakPerfect }} +
GREAT + Tap: {{ record.raw.tapGreat }}
+ Hold: {{ record.raw.holdGreat }}
+ Slide: {{ record.raw.slideGreat }}
+ Touch: {{ record.raw.touchGreat }}
+ Break: {{ record.raw.breakGreat }} +
GOOD + Tap: {{ record.raw.tapGood }}
+ Hold: {{ record.raw.holdGood }}
+ Slide: {{ record.raw.slideGood }}
+ Touch: {{ record.raw.touchGood }}
+ Break: {{ record.raw.breakGood }} +
MISS + Tap: {{ record.raw.tapMiss }}
+ Hold: {{ record.raw.holdMiss }}
+ Slide: {{ record.raw.slideMiss }}
+ Touch: {{ record.raw.touchMiss }}
+ Break: {{ record.raw.breakMiss }} +
+
+
+ {%if record.raw.comboStatus == 1 %} +
FULL COMBO
+ {% endif %} + {%if record.raw.comboStatus == 2 %} +
FULL COMBO +
+ {% endif %} + {%if record.raw.comboStatus == 3 %} +
ALL PERFECT
+ {% endif %} + {%if record.raw.comboStatus == 4 %} +
ALL PERFECT +
+ {% endif %} + {%if record.raw.syncStatus == 1 %} +
FULL SYNC
+ {% endif %} + {%if record.raw.syncStatus == 2 %} +
FULL SYNC +
+ {% endif %} + {%if record.raw.syncStatus == 3 %} +
FULL SYNC DX
+ {% endif %} + {%if record.raw.syncStatus == 4 %} +
FULL SYNC DX +
+ {% endif %} + {%if record.raw.isAchieveNewRecord == 1 or record.raw.isDeluxscoreNewRecord == 1 %} +
NEW RECORD
+ {% endif %} +
+
+
+
+ {% endfor %} +
+ {% set playlog_pages = playlog_count // 20 + 1 %} + {% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + No Playlog information found for this account. + {% else %} + Login to view profile information. + {% endif %} +
+ + + +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/universe.py b/titles/mai2/universe.py index 00353d3..8ed0d88 100644 --- a/titles/mai2/universe.py +++ b/titles/mai2/universe.py @@ -1,8 +1,6 @@ from typing import Any, List, Dict from random import randint from datetime import datetime, timedelta -import pytz -import json from core.config import CoreConfig from titles.mai2.splashplus import Mai2SplashPlus @@ -14,207 +12,3 @@ class Mai2Universe(Mai2SplashPlus): def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: super().__init__(cfg, game_cfg) self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - - async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - if p is None: - return {} - - return { - "userName": p["userName"], - "rating": p["playerRating"], - # hardcode lastDataVersion for CardMaker - "lastDataVersion": "1.20.00", - # checks if the user is still logged in - "isLogin": False, - "isExistSellingCard": True, - } - - async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict: - # user already exists, because the preview checks that already - p = await self.data.profile.get_profile_detail(data["userId"], self.version) - - cards = await self.data.card.get_user_cards(data["userId"]) - if cards is None or len(cards) == 0: - # This should never happen - self.logger.error( - f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}" - ) - return {} - - # get the dict representation of the row so we can modify values - user_data = p._asdict() - - # remove the values the game doesn't want - user_data.pop("id") - user_data.pop("user") - user_data.pop("version") - - return {"userId": data["userId"], "userData": user_data} - - async def handle_cm_login_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_logout_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} - - async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict: - selling_cards = await self.data.static.get_enabled_cards(self.version) - if selling_cards is None: - return {"length": 0, "sellingCardList": []} - - selling_card_list = [] - for card in selling_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("version") - tmp.pop("cardName") - tmp.pop("enabled") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeStartDate"] = datetime.strftime( - tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["noticeEndDate"] = datetime.strftime( - tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT - ) - - selling_card_list.append(tmp) - - return {"length": len(selling_card_list), "sellingCardList": selling_card_list} - - async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict: - user_cards = await self.data.item.get_cards(data["userId"]) - if user_cards is None: - return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []} - - max_ct = data["maxCount"] - next_idx = data["nextIndex"] - start_idx = next_idx - end_idx = max_ct + start_idx - - if len(user_cards[start_idx:]) > max_ct: - next_idx += max_ct - else: - next_idx = 0 - - card_list = [] - for card in user_cards: - tmp = card._asdict() - tmp.pop("id") - tmp.pop("user") - - tmp["startDate"] = datetime.strftime( - tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT - ) - tmp["endDate"] = datetime.strftime( - tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT - ) - card_list.append(tmp) - - return { - "returnCode": 1, - "length": len(card_list[start_idx:end_idx]), - "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx], - } - - async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: - await super().handle_get_user_item_api_request(data) - - async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: - characters = await self.data.item.get_characters(data["userId"]) - - chara_list = [] - for chara in characters: - chara_list.append( - { - "characterId": chara["characterId"], - # no clue why those values are even needed - "point": 0, - "count": 0, - "level": chara["level"], - "nextAwake": 0, - "nextAwakePercent": 0, - "favorite": False, - "awakening": chara["awakening"], - "useCount": chara["useCount"], - } - ) - - return { - "returnCode": 1, - "length": len(chara_list), - "userCharacterList": chara_list, - } - - async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict: - return {"length": 0, "userPrintDetailList": []} - - async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict: - user_id = data["userId"] - upsert = data["userPrintDetail"] - - # set a random card serial number - serial_id = "".join([str(randint(0, 9)) for _ in range(20)]) - - # calculate start and end date of the card - start_date = datetime.utcnow() - end_date = datetime.utcnow() + timedelta(days=15) - - user_card = upsert["userCard"] - await self.data.item.put_card( - user_id, - user_card["cardId"], - user_card["cardTypeId"], - user_card["charaId"], - user_card["mapId"], - # add the correct start date and also the end date in 15 days - start_date, - end_date, - ) - - # get the profile extend to save the new bought card - extend = await self.data.profile.get_profile_extend(user_id, self.version) - if extend: - extend = extend._asdict() - # parse the selectedCardList - # 6 = Freedom Pass, 4 = Gold Pass (cardTypeId) - selected_cards: List = extend["selectedCardList"] - - # if no pass is already added, add the corresponding pass - if not user_card["cardTypeId"] in selected_cards: - selected_cards.insert(0, user_card["cardTypeId"]) - - extend["selectedCardList"] = selected_cards - await self.data.profile.put_profile_extend(user_id, self.version, extend) - - # properly format userPrintDetail for the database - upsert.pop("userCard") - upsert.pop("serialId") - upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d") - - await self.data.item.put_user_print_detail(user_id, serial_id, upsert) - - return { - "returnCode": 1, - "orderId": 0, - "serialId": serial_id, - "startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT), - "endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT), - } - - async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict: - return { - "returnCode": 1, - "orderId": 0, - "serialId": data["userPrintlog"]["serialId"], - } - - async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict: - return {"returnCode": 1} diff --git a/titles/ongeki/frontend.py b/titles/ongeki/frontend.py index 1cdec03..226f318 100644 --- a/titles/ongeki/frontend.py +++ b/titles/ongeki/frontend.py @@ -31,7 +31,8 @@ class OngekiFrontend(FE_Base): def get_routes(self) -> List[Route]: return [ - Route("/", self.render_GET) + Route("/", self.render_GET), + Route("/version.change", self.render_POST, methods=['POST']) ] async def render_GET(self, request: Request) -> bytes: @@ -69,29 +70,34 @@ class OngekiFrontend(FE_Base): return RedirectResponse("/gate/", 303) async def render_POST(self, request: Request): - uri = request.uri.decode() + uri = request.url.path + frm = await request.form() usr_sesh = self.validate_session(request) if not usr_sesh: usr_sesh = UserSession() if usr_sesh.user_id > 0: if uri == "/game/ongeki/rival.add": - rival_id = request.args[b"rivalUserId"][0].decode() + rival_id = frm.get("rivalUserId") await self.data.profile.put_rival(usr_sesh.user_id, rival_id) # self.logger.info(f"{usr_sesh.user_id} added a rival") return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/rival.delete": - rival_id = request.args[b"rivalUserId"][0].decode() + rival_id = frm.get("rivalUserId") await self.data.profile.delete_rival(usr_sesh.user_id, rival_id) # self.logger.info(f"{response}") return RedirectResponse(b"/game/ongeki/", 303) elif uri == "/game/ongeki/version.change": - ongeki_version=request.args[b"version"][0].decode() + ongeki_version=frm.get("version") if(ongeki_version.isdigit()): usr_sesh.ongeki_version=int(ongeki_version) - return RedirectResponse("/game/ongeki/", 303) + enc = self.encode_session(usr_sesh) + resp = RedirectResponse("/game/ongeki/", 303) + resp.delete_cookie('ARTEMIS_SESH') + resp.set_cookie('ARTEMIS_SESH', enc) + return resp else: Response("Something went wrong", status_code=500) diff --git a/titles/ongeki/index.py b/titles/ongeki/index.py index d7f3e0e..3bd0e15 100644 --- a/titles/ongeki/index.py +++ b/titles/ongeki/index.py @@ -216,16 +216,20 @@ class OngekiServlet(BaseServlet): ) return Response(zlib.compress(b'{"stat": "0"}')) - try: - unzip = zlib.decompress(req_raw) + if version < 105: + # O.N.G.E.K.I base don't use zlib + req_data = json.loads(req_raw) + else: + try: + unzip = zlib.decompress(req_raw) + + except zlib.error as e: + self.logger.error( + f"Failed to decompress v{version} {endpoint} request -> {e}" + ) + return Response(zlib.compress(b'{"stat": "0"}')) - except zlib.error as e: - self.logger.error( - f"Failed to decompress v{version} {endpoint} request -> {e}" - ) - return Response(zlib.compress(b'{"stat": "0"}')) - - req_data = json.loads(unzip) + req_data = json.loads(unzip) self.logger.info( f"v{version} {endpoint} request from {client_ip}" @@ -251,9 +255,12 @@ class OngekiServlet(BaseServlet): self.logger.debug(f"Response {resp}") - zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) + resp_raw = json.dumps(resp, ensure_ascii=False).encode("utf-8") + zipped = zlib.compress(resp_raw) if not encrtped or version < 120: + if version < 105: + return Response(resp_raw) return Response(zipped) padded = pad(zipped, 16) diff --git a/titles/ongeki/schema/score.py b/titles/ongeki/schema/score.py index f5ab4e1..6867133 100644 --- a/titles/ongeki/schema/score.py +++ b/titles/ongeki/schema/score.py @@ -30,7 +30,7 @@ score_best = Table( Column("isFullCombo", Boolean, nullable=False), Column("isAllBreake", Boolean, nullable=False), Column("isLock", Boolean, nullable=False), - Column("clearStatus", Boolean, nullable=False), + Column("clearStatus", Integer, nullable=False), Column("isStoryWatched", Boolean, nullable=False), Column("platinumScoreMax", Integer), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), diff --git a/titles/pokken/base.py b/titles/pokken/base.py index 562f3b9..78ca863 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -1,9 +1,7 @@ -from datetime import datetime, timedelta +from datetime import datetime import json, logging from typing import Any, Dict, List -import random -from core.data import Data from core import CoreConfig from .config import PokkenConfig from .proto import jackal_pb2 @@ -18,7 +16,6 @@ class PokkenBase: self.version = 0 self.logger = logging.getLogger("pokken") self.data = PokkenData(core_cfg) - self.SUPPORT_SET_NONE = 4294967295 async def handle_noop(self, request: Any) -> bytes: res = jackal_pb2.Response() @@ -38,7 +35,30 @@ class PokkenBase: res = jackal_pb2.Response() res.result = 1 res.type = jackal_pb2.MessageType.REGISTER_PCB - self.logger.info(f"Register PCB {request.register_pcb.pcb_id}") + pcbid = request.register_pcb.pcb_id + if not pcbid.isdigit() or len(pcbid) != 12 or \ + not pcbid.startswith(f"{PokkenConstants.SERIAL_IDENT[0]}{PokkenConstants.SERIAL_REGIONS[0]}{PokkenConstants.SERIAL_ROLES[0]}{PokkenConstants.SERIAL_CAB_IDENTS[0]}"): + self.logger.warn(f"Bad PCBID {pcbid}") + res.result = 0 + return res + + netid = PokkenConstants.NETID_PREFIX[0] + pcbid[5:] + + self.logger.info(f"Register PCB {pcbid} (netID {netid})") + + minfo = await self.data.arcade.get_machine(netid) + + if not minfo and not self.core_cfg.server.allow_unregistered_serials: + self.logger.warn(f"netID {netid} does not belong to any shop!") + res.result = 0 + return res + + elif not minfo: + self.logger.warn(f"Orphaned netID {netid} allowed to connect") + locid = 0 + + else: + locid = minfo['arcade'] regist_pcb = jackal_pb2.RegisterPcbResponseData() regist_pcb.server_time = int(datetime.now().timestamp()) @@ -46,7 +66,7 @@ class PokkenBase: "MatchingServer": { "host": f"https://{self.game_cfg.server.hostname}", "port": self.game_cfg.ports.game, - "url": "/SDAK/100/matching", + "url": "/pokken/matching", }, "StunServer": { "addr": self.game_cfg.server.stun_server_host, @@ -56,10 +76,10 @@ class PokkenBase: "addr": self.game_cfg.server.stun_server_host, "port": self.game_cfg.server.stun_server_port, }, - "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}", - "locationId": 123, # FIXME: Get arcade's ID from the database - "logfilename": "JackalMatchingLibrary.log", - "biwalogfilename": "./biwa.log", + "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}/pokken/admission", + "locationId": locid, + "logfilename": "J:\\JackalMatchingLibrary.log", + "biwalogfilename": "J:\\biwa_log.log", } regist_pcb.bnp_baseuri = f"{self.core_cfg.server.hostname}/bna" regist_pcb.biwa_setting = json.dumps(biwa_setting) @@ -95,12 +115,11 @@ class PokkenBase: res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS settings = jackal_pb2.LoadClientSettingsResponseData() - # TODO: Make configurable settings.money_magnification = 1 settings.continue_bonus_exp = 100 settings.continue_fight_money = 100 settings.event_bonus_exp = 100 - settings.level_cap = 999 + settings.level_cap = 100 settings.op_movie_flag = 0xFFFFFFFF settings.lucky_bonus_rate = 1 settings.fail_support_num = 10 @@ -132,9 +151,13 @@ class PokkenBase: res.type = jackal_pb2.MessageType.LOAD_USER access_code = request.load_user.access_code load_usr = jackal_pb2.LoadUserResponseData() - user_id = await self.data.card.get_user_id_from_card(access_code) + load_usr.load_hash = 1 + load_usr.access_code = access_code + load_usr.precedent_release_flag = 0xFFFFFFFF + load_usr.cardlock_status = False + card = await self.data.card.get_card_by_access_code(access_code) - if user_id is None and self.game_cfg.server.auto_register: + if card is None and self.game_cfg.server.auto_register: user_id = await self.data.user.create_user() card_id = await self.data.card.create_card(user_id, access_code) @@ -142,54 +165,39 @@ class PokkenBase: f"Register new card {access_code} (UserId {user_id}, CardId {card_id})" ) - elif user_id is None: + if int(request.load_user.chip_id[:8], 16) != 0x04030201: + await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16)) + + elif card is None: self.logger.info(f"Registration of card {access_code} blocked!") res.load_user.CopyFrom(load_usr) return res.SerializeToString() + + else: + user_id = card['user'] + card_id = card['id'] + if not card['chip_id'] and int(request.load_user.chip_id[:8], 16) != 0x04030201: # Default segatools sn: + await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16)) """ - TODO: Add repeated values - tutorial_progress_flag - rankmatch_progress + TODO: Unlock all supports? Probably support_pokemon_list - support_set_1 - support_set_2 - support_set_3 - aid_skill_list - achievement_flag - event_achievement_flag - event_achievement_param """ profile = await self.data.profile.get_profile(user_id) load_usr.commidserv_result = 1 - load_usr.load_hash = 1 - load_usr.cardlock_status = False load_usr.banapass_id = user_id - load_usr.access_code = access_code - load_usr.precedent_release_flag = 0xFFFFFFFF - if profile is None: + if profile is None or profile['trainer_name'] is None: profile_id = await self.data.profile.create_profile(user_id) + self.logger.info(f"Create new profile {profile_id} for user {user_id}") profile_dict = {"id": profile_id, "user": user_id} pokemon_data = [] - tutorial_progress = [] - rankmatch_progress = [] - achievement_flag = [] - event_achievement_flag = [] - event_achievement_param = [] load_usr.new_card_flag = True else: profile_dict = {k: v for k, v in profile._asdict().items() if v is not None} - self.logger.info( - f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})" - ) + self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})") pokemon_data = await self.data.profile.get_all_pokemon_data(user_id) - tutorial_progress = [] - rankmatch_progress = [] - achievement_flag = [] - event_achievement_flag = [] - event_achievement_param = [] load_usr.new_card_flag = False load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True) @@ -201,9 +209,9 @@ class PokkenBase: load_usr.trainer_name = profile_dict.get( "trainer_name", f"Newb{str(user_id).zfill(4)}" ) - load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) - load_usr.wallet = profile_dict.get("wallet", 0) - load_usr.fight_money = profile_dict.get("fight_money", 0) + load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) # determines rank + load_usr.wallet = profile_dict.get("wallet", 0) # pg count + load_usr.fight_money = profile_dict.get("fight_money", 0) # ? load_usr.score_point = profile_dict.get("score_point", 0) load_usr.grade_max_num = profile_dict.get("grade_max_num", 0) load_usr.extra_counter = profile_dict.get("extra_counter", 0) @@ -218,18 +226,18 @@ class PokkenBase: load_usr.rank_event = profile_dict.get("rank_event", 0) load_usr.awake_num = profile_dict.get("awake_num", 0) load_usr.use_support_num = profile_dict.get("use_support_num", 0) - load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) + load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) # flags that next rank match will be rank up load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0) load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0) load_usr.beat_num = profile_dict.get("beat_num", 0) - load_usr.title_text_id = profile_dict.get("title_text_id", 0) - load_usr.title_plate_id = profile_dict.get("title_plate_id", 0) - load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0) + load_usr.title_text_id = profile_dict.get("title_text_id", 2) + load_usr.title_plate_id = profile_dict.get("title_plate_id", 31) + load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 1) load_usr.navi_trainer = profile_dict.get("navi_trainer", 0) load_usr.navi_version_id = profile_dict.get("navi_version_id", 0) load_usr.aid_skill = profile_dict.get("aid_skill", 0) - load_usr.comment_text_id = profile_dict.get("comment_text_id", 0) - load_usr.comment_word_id = profile_dict.get("comment_word_id", 0) + load_usr.comment_text_id = profile_dict.get("comment_text_id", 1) + load_usr.comment_word_id = profile_dict.get("comment_word_id", 1) load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0) load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0) load_usr.wko_num = profile_dict.get("wko_num", 0) @@ -237,11 +245,11 @@ class PokkenBase: load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0) load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0) load_usr.record_flag = profile_dict.get("record_flag", 0) - load_usr.site_register_status = profile_dict.get("site_register_status", 0) + load_usr.site_register_status = profile_dict.get("site_register_status", 1) load_usr.continue_num = profile_dict.get("continue_num", 0) load_usr.avatar_body = profile_dict.get("avatar_body", 0) - load_usr.avatar_gender = profile_dict.get("avatar_gender", 0) + load_usr.avatar_gender = profile_dict.get("avatar_gender", 1) load_usr.avatar_background = profile_dict.get("avatar_background", 0) load_usr.avatar_head = profile_dict.get("avatar_head", 0) load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0) @@ -283,6 +291,31 @@ class PokkenBase: pkm.bp_point_sp = pkmn_d.get('bp_point_sp', 0) load_usr.pokemon_data.append(pkm) + + for x in profile_dict.get("tutorial_progress_flag", []): + load_usr.tutorial_progress_flag.append(x) + + for x in profile_dict.get("achievement_flag", []): + load_usr.achievement_flag.append(x) + + for x in profile_dict.get("aid_skill_list", []): + load_usr.aid_skill_list.append(x) + + for x in profile_dict.get("rankmatch_progress", []): + load_usr.rankmatch_progress.append(x) + + for x in profile_dict.get("event_achievement_flag", []): + load_usr.event_achievement_flag.append(x) + + for x in profile_dict.get("event_achievement_param", []): + load_usr.event_achievement_param.append(x) + + load_usr.support_set_1.append(profile_dict.get("support_set_1_1", 587)) + load_usr.support_set_1.append(profile_dict.get("support_set_1_2", 653)) + load_usr.support_set_2.append(profile_dict.get("support_set_2_1", 495)) + load_usr.support_set_2.append(profile_dict.get("support_set_2_2", 131)) + load_usr.support_set_3.append(profile_dict.get("support_set_3_1", 657)) + load_usr.support_set_3.append(profile_dict.get("support_set_3_2", 133)) res.load_user.CopyFrom(load_usr) return res.SerializeToString() @@ -300,6 +333,8 @@ class PokkenBase: req = request.save_user user_id = req.banapass_id + + self.logger.info(f"Save user data for {user_id}") tut_flgs: List[int] = [] ach_flgs: List[int] = [] @@ -339,7 +374,7 @@ class PokkenBase: for ach_flg in req.achievement_flag: ach_flgs.append(ach_flg) - await self.data.profile.update_profile_tutorial_flags(user_id, ach_flg) + await self.data.profile.update_profile_achievement_flags(user_id, ach_flgs) for evt_flg in req.event_achievement_flag: evt_flgs.append(evt_flg) @@ -353,18 +388,23 @@ class PokkenBase: await self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id) await self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max) + + # Inconsistant underscore use AND a typo?? + #await self.data.profile.update_rankmatch_data(user_id, req.rankmatch_flag, req.rank_match_max, req.rank_match_success, req.rank_match_process) await self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1]) await self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1]) await self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1]) await self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp) - await self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp) + await self.data.profile.add_pokemon_xp(user_id, mon.illustration_book_no, mon.get_pokemon_exp) + await self.data.profile.set_latest_mon(user_id, mon.illustration_book_no) for x in range(len(battle.play_mode)): + self.logger.info(f"Save {PokkenConstants.BATTLE_TYPE(battle.play_mode[x]).name} battle {PokkenConstants.BATTLE_RESULT(battle.result[x]).name} for {user_id} with mon {mon.illustration_book_no}") await self.data.profile.put_pokemon_battle_result( user_id, - mon.char_id, + mon.illustration_book_no, PokkenConstants.BATTLE_TYPE(battle.play_mode[x]), PokkenConstants.BATTLE_RESULT(battle.result[x]) ) @@ -391,7 +431,6 @@ class PokkenBase: last_evt ) - return res.SerializeToString() async def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: @@ -419,6 +458,13 @@ class PokkenBase: async def handle_matching_is_matching( self, data: Dict = {}, client_ip: str = "127.0.0.1" ) -> Dict: + """ + "sessionId":"12345678", + "A":{ + "pcb_id": data["data"]["must"]["pcb_id"], + "gip": client_ip + }, + """ return { "data": { "sessionId":"12345678", @@ -435,6 +481,11 @@ class PokkenBase: ) -> Dict: return {} + async def handle_matching_obtain_matching( + self, data: Dict = {}, client_ip: str = "127.0.0.1" + ) -> Dict: + return {} + async def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict: return {} diff --git a/titles/pokken/data/fighters.json b/titles/pokken/data/fighters.json new file mode 100644 index 0000000..a5ba6f8 Binary files /dev/null and b/titles/pokken/data/fighters.json differ diff --git a/titles/pokken/data/support.json b/titles/pokken/data/support.json new file mode 100644 index 0000000..ef5368e Binary files /dev/null and b/titles/pokken/data/support.json differ diff --git a/titles/pokken/schema/profile.py b/titles/pokken/schema/profile.py index b7237de..2976ff7 100644 --- a/titles/pokken/schema/profile.py +++ b/titles/pokken/schema/profile.py @@ -1,6 +1,6 @@ from typing import Optional, Dict, List, Union from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, INTEGER from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select, update, delete from sqlalchemy.sql.functions import coalesce @@ -16,13 +16,8 @@ profile = Table( "pokken_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("trainer_name", String(16)), # optional + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column("trainer_name", String(14)), # optional Column("home_region_code", Integer), Column("home_loc_name", String(255)), Column("pref_code", Integer), @@ -66,7 +61,7 @@ profile = Table( Column("navi_trainer", Integer), Column("navi_version_id", Integer), Column("aid_skill_list", JSON), # Repeated, Integer - Column("aid_skill", Integer), + Column("aid_skill", Integer), # Cheer skill, 6 of them, unlocked by lucky bonus Column("comment_text_id", Integer), Column("comment_word_id", Integer), Column("latest_use_pokemon", Integer), @@ -105,20 +100,16 @@ profile = Table( Column("battle_num_vs_cpu", Integer), # 2 Column("win_cpu", Integer), Column("battle_num_tutorial", Integer), # 1? - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) pokemon_data = Table( "pokken_pokemon_data", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("char_id", Integer, nullable=False), - Column("illustration_book_no", Integer), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("char_id", Integer), + Column("illustration_book_no", Integer, nullable=False), # This is the fucking pokedex number???? Column("pokemon_exp", Integer), Column("battle_num_vs_wan", Integer), # 4? Column("win_vs_wan", Integer), @@ -132,8 +123,8 @@ pokemon_data = Table( Column("bp_point_res", Integer), Column("bp_point_def", Integer), Column("bp_point_sp", Integer), - UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"), - mysql_charset="utf8mb4", + UniqueConstraint("user", "illustration_book_no", name="pokken_pokemon_uk"), + mysql_charset="utf8mb4" ) @@ -157,8 +148,8 @@ class PokkenProfileData(BaseData): return result.lastrowid async def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None: - sql = update(profile).where(profile.c.user == user_id).values( - trainer_name=new_name, + sql = profile.update(profile.c.user == user_id).values( + trainer_name=new_name if new_name else profile.c.trainer_name, avatar_gender=gender if gender is not None else profile.c.avatar_gender ) result = await self.execute(sql) @@ -179,12 +170,12 @@ class PokkenProfileData(BaseData): aid_skill: int, last_evt: int ) -> None: - sql = update(profile).where(profile.c.user == user_id).values( + sql = profile.update(profile.c.user == user_id).values( extra_counter=extra_counter, event_reward_get_flag=evt_reward_get_flg, - total_play_days=total_play_days, - awake_num=awake_num, - use_support_num=use_support_ct, + total_play_days=coalesce(profile.c.total_play_days, 0) + total_play_days, + awake_num=coalesce(profile.c.awake_num, 0) + awake_num, + use_support_num=coalesce(profile.c.use_support_num, 0) + use_support_ct, beat_num=beat_num, aid_skill=aid_skill, last_play_event_id=last_evt @@ -195,7 +186,7 @@ class PokkenProfileData(BaseData): self.logger.error(f"Failed to put extra data for user {user_id}") async def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None: - sql = update(profile).where(profile.c.user == user_id).values( + sql = profile.update(profile.c.user == user_id).values( tutorial_progress_flag=tutorial_flags, ) result = await self.execute(sql) @@ -205,7 +196,7 @@ class PokkenProfileData(BaseData): ) async def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None: - sql = update(profile).where(profile.c.user == user_id).values( + sql = profile.update(profile.c.user == user_id).values( achievement_flag=achievement_flags, ) result = await self.execute(sql) @@ -215,7 +206,7 @@ class PokkenProfileData(BaseData): ) async def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None: - sql = update(profile).where(profile.c.user == user_id).values( + sql = profile.update(profile.c.user == user_id).values( event_state=event_state, event_achievement_flag=event_flags, event_achievement_param=event_param, @@ -230,12 +221,16 @@ class PokkenProfileData(BaseData): async def add_profile_points( self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int ) -> None: - sql = update(profile).where(profile.c.user == user_id).values( - trainer_rank_point = profile.c.trainer_rank_point + rank_pts, - fight_money = profile.c.fight_money + money, - score_point = profile.c.score_point + score_pts, + sql = profile.update(profile.c.user == user_id).values( + trainer_rank_point = coalesce(profile.c.trainer_rank_point, 0) + rank_pts, + wallet = coalesce(profile.c.wallet, 0) + money, + score_point = coalesce(profile.c.score_point, 0) + score_pts, grade_max_num = grade_max ) + + result = await self.execute(sql) + if result is None: + return None async def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) @@ -248,7 +243,7 @@ class PokkenProfileData(BaseData): self, user_id: int, pokemon_id: int, - illust_no: int, + pokedex_number: int, atk: int, res: int, defe: int, @@ -257,7 +252,7 @@ class PokkenProfileData(BaseData): sql = insert(pokemon_data).values( user=user_id, char_id=pokemon_id, - illustration_book_no=illust_no, + illustration_book_no=pokedex_number, pokemon_exp=0, battle_num_vs_wan=0, win_vs_wan=0, @@ -274,7 +269,7 @@ class PokkenProfileData(BaseData): ) conflict = sql.on_duplicate_key_update( - illustration_book_no=illust_no, + illustration_book_no=pokedex_number, bp_point_atk=pokemon_data.c.bp_point_atk + atk, bp_point_res=pokemon_data.c.bp_point_res + res, bp_point_def=pokemon_data.c.bp_point_def + defe, @@ -293,7 +288,7 @@ class PokkenProfileData(BaseData): pokemon_id: int, xp: int ) -> None: - sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( + sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values( pokemon_exp=coalesce(pokemon_data.c.pokemon_exp, 0) + xp ) @@ -302,7 +297,7 @@ class PokkenProfileData(BaseData): self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}") async def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]: - sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id)) + sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.illustration_book_no == pokemon_id)) result = await self.execute(sql) if result is None: return None @@ -315,6 +310,14 @@ class PokkenProfileData(BaseData): return None return result.fetchall() + async def set_latest_mon(self, user_id: int, pokedex_no: int) -> None: + sql = profile.update(profile.c.user == user_id).values( + latest_use_pokemon=pokedex_no + ) + result = await self.execute(sql) + if result is None: + self.logger.warning(f"Failed to update user {user_id}'s last used pokemon {pokedex_no}") + async def put_pokemon_battle_result( self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT ) -> None: @@ -322,7 +325,7 @@ class PokkenProfileData(BaseData): Records the match stats (type and win/loss) for the pokemon and profile coalesce(pokemon_data.c.win_vs_wan, 0) """ - sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values( + sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values( battle_num_tutorial=coalesce(pokemon_data.c.battle_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_num_tutorial, 0), battle_all_num_tutorial=coalesce(pokemon_data.c.battle_all_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_all_num_tutorial, 0), @@ -353,7 +356,7 @@ class PokkenProfileData(BaseData): """ Records profile stats """ - sql = update(profile).where(profile.c.user==user_id).values( + sql = profile.update(profile.c.user==user_id).values( ex_ko_num=coalesce(profile.c.ex_ko_num, 0) + exkos, wko_num=coalesce(profile.c.wko_num, 0) + wkos, timeup_win_num=coalesce(profile.c.timeup_win_num, 0) + timeout_wins, @@ -367,7 +370,12 @@ class PokkenProfileData(BaseData): self.logger.warning(f"Failed to update stats for user {user_id}") async def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None: - sql = update(profile).where(profile.c.user==user_id).values( + if support1 == 4294967295: + support1 = None + + if support2 == 4294967295: + support2 = None + sql = profile.update(profile.c.user==user_id).values( support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1, support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2, support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1, @@ -379,3 +387,15 @@ class PokkenProfileData(BaseData): result = await self.execute(sql) if result is None: self.logger.warning(f"Failed to update support team {support_id} for user {user_id}") + + async def update_rankmatch_data(self, user_id: int, flag: int, rm_max: Optional[int], success: Optional[int], progress: List[int]) -> None: + sql = profile.update(profile.c.user==user_id).values( + rankmatch_flag=flag, + rankmatch_max=rm_max, + rankmatch_progress=progress, + rankmatch_success=success, + ) + + result = await self.execute(sql) + if result is None: + self.logger.warning(f"Failed to update rankmatch data for user {user_id}") diff --git a/titles/sao/__init__.py b/titles/sao/__init__.py index b6943e6..501af9b 100644 --- a/titles/sao/__init__.py +++ b/titles/sao/__init__.py @@ -1,9 +1,11 @@ from .index import SaoServlet from .const import SaoConstants from .database import SaoData +from .frontend import SaoFrontend from .read import SaoReader index = SaoServlet database = SaoData +frontend = SaoFrontend reader = SaoReader game_codes = [SaoConstants.GAME_CODE] diff --git a/titles/sao/base.py b/titles/sao/base.py index 80f91a2..44a7801 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -1,444 +1,1173 @@ import logging from csv import * -from random import choice +from random import choice, randint from typing import Dict, List from os import path +import re +from sqlalchemy.engine import Row -from core.data import Data from core import CoreConfig from .config import SaoConfig from .database import SaoData +from .const import GameconnectCmd, RewardType, ExBonusCondition, QuestType from titles.sao.handlers.base import * +import csv class SaoBase: + DATA_LIST = {} def __init__(self, core_cfg: CoreConfig, game_cfg: SaoConfig) -> None: self.core_cfg = core_cfg self.game_cfg = game_cfg self.data = SaoData(core_cfg) - self.version = 0 self.logger = logging.getLogger("sao") - def load_data_csv(self, file: str) -> List[Dict]: - ret = [] - if path.exists(f"titles/sao/data/{file}.csv"): - with open(f"titles/sao/data/{file}.csv", "r", encoding="utf8") as f: - data = csv.DictReader(f, delimiter=',') - for x in data: - ret.append(x) - + def load_data_csv(self, file: str, version: int = 1, base_ver: int = 1) -> List[Dict]: + if base_ver > version: + self.logger.warning(f"load_data_csv: Cannot use base version higher then requested version ({base_ver} > {version})") + return [] + + for x in range(version, base_ver - 1, -1): + ret = self.DATA_LIST.get(x, {}).get(file, []) + if ret: + break + + if not ret and base_ver != 1: + ret = self.DATA_LIST.get(1, {}).get(file, []) + + if ret: return ret - self.logger.warning(f"Failed to find csv file {file}.csv") + found = False + for x in range(version, base_ver - 1, -1): + fname = f"./titles/sao/data/{x}/{file}.csv" + if path.exists(fname): + found_ver = x + found = True + break + + if not found and base_ver != 1: # v1 will always be fallback if it isn't already + fname = f"./titles/sao/data/1/{file}.csv" + if path.exists(fname): + found_ver = 1 + found = True + + if not found: + self.logger.warning(f"load_data_csv: Failed to find v{version} csv file {fname}") + return [] + + ret = [] + with open(fname, "r", encoding="utf8") as f: + data = csv.DictReader(f, delimiter=',') + for x in data: + newdict = {} + for k, v in x.items(): + newkey = k + if k.startswith("// "): + newkey = k.replace("// ", "") + + if v.isdigit(): + newdict[newkey] = int(v) + elif v.lower() == "true": + newdict[newkey] = True + elif v.lower() == "false": + newdict[newkey] = False + elif re.match(r"^\d\d\d\d\/\d\d\/\d\d \d{1,2}:\d\d:\d\d$", v): + newdict[newkey] = datetime.strptime(v, "%Y/%m/%d %H:%M:%S") + elif re.match(r"^\d\d\d\d\/\d\d\/\d\d$", v): + newdict[newkey] = datetime.strptime(v, "%Y/%m/%d") + else: + newdict[newkey] = v + ret.append(newdict) + + # Cache the CSV data in memory + if found_ver not in self.DATA_LIST: + self.DATA_LIST[found_ver] = {} + self.DATA_LIST[found_ver][file] = ret + return ret - async def handle_noop(self, header: SaoRequestHeader, request: bytes) -> bytes: - self.logger.info(f"Using Generic handler") - resp_thing = SaoNoopResponse(header.cmd + 1) - return resp_thing.make() + async def add_reward(self, reward: Dict, user_id: int): + reward_type = int(reward.get("CommonRewardType", "0")) + if reward_type == RewardType.HeroLog: + reward_hero_data = await self.data.static.get_hero_by_id(reward['CommonRewardId']) + now_have_skills = await self.hero_default_skills(reward_hero_data['SkillTableSubId']) + + new_hero_id = await self.data.item.put_hero_log( + user_id, + reward['CommonRewardId'], + 1, + 0, + None, + None, + now_have_skills[0], + now_have_skills[1], + now_have_skills[2], + now_have_skills[3], + now_have_skills[4], + ) + self.logger.info(f"Rewarded user {user_id} with hero {reward['CommonRewardId']} (ID {new_hero_id})") + # TODO: add properties + + elif reward_type == RewardType.Equipment: + new_equip_id = await self.data.item.put_equipment(user_id, reward['CommonRewardId']) + self.logger.info(f"Rewarded user {user_id} with equipment {reward['CommonRewardId']} (ID {new_equip_id})") + + elif reward_type == RewardType.Item: + new_item_id = await self.data.item.put_item(user_id, reward['CommonRewardId']) + self.logger.info(f"Rewarded user {user_id} with item {reward['CommonRewardId']} (ID {new_item_id})") + + elif reward_type == RewardType.Col: + col_num = int(reward['CommonRewardNum']) + self.logger.info(f"Rewarded user {user_id} with {col_num} Col") + await self.data.profile.add_col(user_id, col_num) - async def handle_c122(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/get_maintenance_info - resp = SaoGetMaintResponse(header.cmd +1) - return resp.make() + elif reward_type == RewardType.VP: + vp_num = int(reward['CommonRewardNum']) + self.logger.info(f"Rewarded user {user_id} with {vp_num} VP") + await self.data.profile.add_vp(user_id, vp_num) - async def handle_c12a(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/give_free_ticket - req = SaoGiveFreeTicketRequest(header, request) - self.logger.info(f"Give {req.give_num} free tickets (id {req.ticket_id}) to user {req.user_id}") - resp = SaoGiveFreeTicketResponse(header.cmd +1) - return resp.make() + elif reward_type == RewardType.YuiMadal: + medal_num = int(reward['CommonRewardNum']) + self.logger.info(f"Rewarded user {user_id} with {medal_num} Yui Medals") + await self.data.profile.add_yui_medals(user_id, medal_num) + + else: + self.logger.warn(f"User {user_id} Unhandled reward type {reward_type} -> {reward}") - async def handle_c12e(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/ac_cabinet_boot_notification - resp = SaoCommonAcCabinetBootNotificationResponse(header.cmd +1) - return resp.make() + async def hero_default_skills(self, skill_table_id: int) -> List[int]: + skills = await self.data.static.get_skill_table_by_subid(skill_table_id) + if not skills: + self.logger.error(f"Failed to find skill table {skill_table_id}! Please run the reader") + return [None, None, None, None, None] - async def handle_c100(self, header: SaoRequestHeader, request: bytes) -> bytes: + default_skills = [] + now_have_skills = [None, None, None, None, None] + for skill in skills: + if skill['LevelObtained'] == 1 and skill['AwakeningId'] == 0: + default_skills.append(skill['SkillId']) + + for skill in default_skills: + skill_info = await self.data.static.get_skill_by_id(skill) + skill_slot = skill_info['Level'] - 1 + if now_have_skills[skill_slot] is not None: + now_have_skills[skill] + + return now_have_skills + + async def handle_noop(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + return SaoNoopResponse(header.cmd + 1).make() + + async def handle_c000(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #ticket/ticket + req = SaoTicketRequest(header, request) + return SaoTicketResponse().make() + + async def handle_c100(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #common/get_app_versions - resp = SaoCommonGetAppVersionsRequest(header.cmd +1) + resp = SaoGetAppVersionsResponse() + resp.data_list.append(AppVersionData.from_args(self.game_cfg.server.game_version, datetime.fromtimestamp(0))) return resp.make() - async def handle_c102(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c102(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #common/master_data_version_check - resp = SaoMasterDataVersionCheckResponse(header.cmd +1) - return resp.make() + req = SaoMasterDataVersionCheckRequest(header, request) + self.logger.info(f"Cab at {src_ip} checked in with master data v{req.current_data_version}") + return SaoMasterDataVersionCheckResponse(self.game_cfg.server.data_version, req.current_data_version).make() - async def handle_c10a(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/paying_play_start - resp = SaoCommonPayingPlayStartRequest(header.cmd +1) - return resp.make() + async def handle_c104(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/login + req = SaoLoginRequest(header, request) - async def handle_ca02(self, header: SaoRequestHeader, request: bytes) -> bytes: - #quest_multi_play_room/get_quest_scene_multi_play_photon_server - resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(header.cmd +1) - return resp.make() - - async def handle_c11e(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/get_auth_card_data - req = SaoGetAuthCardDataRequest(header, request) - - #Check authentication user_id = await self.data.card.get_user_id_from_card( req.access_code ) - if not user_id: - user_id = await self.data.user.create_user() #works - card_id = await self.data.card.create_card(user_id, req.access_code) + card = await self.data.card.get_card_by_idm(req.chip_id[:16]) + if card: + user_id = card['user'] + card_id = card['id'] + await self.data.card.set_access_code_by_access_code(card['access_code'], req.access_code) + + else: + user_id = await self.data.user.create_user() #works + card_id = await self.data.card.create_card(user_id, req.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 = await self.data.profile.create_profile(user_id) - await self.data.item.put_hero_log(user_id, 101000010, 1, 0, 201000000, 0, 1002, 1003, 1014, 30001, 30310) - await self.data.item.put_hero_log(user_id, 102000010, 1, 0, 202000000, 0, 3001, 3002, 3004, 30007, 3011) - await self.data.item.put_hero_log(user_id, 105000010, 1, 0, 209000000, 0, 10005, 10002, 10004, 30006, 10003) - await self.data.item.put_hero_log(user_id, 101000110, 1, 0, 201000000, 101000110, 2002, 2001, 2014, 0, 0) - await self.data.item.put_hero_party(user_id, 0, 101000010, 102000010, 105000010) - await self.data.item.put_equipment_data(user_id, 201000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 202000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 209000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 101000110, 1, 200, 0, 0, 0) - await self.data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) - - # Force the tutorial stage to be completed due to potential crash in-game + self.logger.info(f"Registered card {req.access_code} to user {user_id} from {req.serial_no}") + + if req.access_code.startswith("5"): + await self.data.card.set_idm_by_access_code(req.access_code, req.chip_id[:16]) + elif (req.access_code.startswith("010") or req.access_code.startswith("3")) and int(req.chip_id[:8], 16) != 0x04030201: + await self.data.card.set_chip_id_by_access_code(req.access_code, int(req.chip_id[:8], 16)) + + profile_data = await self.data.profile.get_profile(user_id) - self.logger.info(f"User Authenticated: { req.access_code } | { user_id }") + if not profile_data: + profile_id = await self.data.profile.create_profile(user_id) + if profile_id: + equip1 = await self.data.item.put_equipment(user_id, 101000000) + equip2 = await self.data.item.put_equipment(user_id, 102000000) + equip3 = await self.data.item.put_equipment(user_id, 109000000) + if not equip1 or not equip2 or not equip3: + self.logger.error(f"Failed to create profile for user {user_id} from {req.serial_no} (could not add equipment)") + return SaoNoopResponse(GameconnectCmd.LOGIN_RESPONSE).make() + + hero1 = await self.data.item.put_hero_log(user_id, 101000010, 1, 0, equip1, None, 1002, 1003, 1014, None, None) + hero2 = await self.data.item.put_hero_log(user_id, 102000010, 1, 0, equip2, None, 3001, 3002, 3004, None, None) + hero3 = await self.data.item.put_hero_log(user_id, 105000010, 1, 0, equip3, None, 10005, 10002, 10004, None, None) + if not hero1 or not hero2 or not hero3: + self.logger.error(f"Failed to create profile for user {user_id} from {req.serial_no} (could not add heros)") + return SaoNoopResponse(GameconnectCmd.LOGIN_RESPONSE).make() + + await self.data.item.put_hero_party(user_id, 0, hero1, hero2, hero3) + self.logger.info(f"Create profile {profile_id} for user {user_id} from {req.serial_no}") + else: + self.logger.error(f"Failed to create profile for user {user_id} from {req.serial_no}") + return SaoNoopResponse(GameconnectCmd.LOGIN_RESPONSE).make() + resp = SaoLoginResponse(user_id, True, False) + + else: + is_login_today = False + + if profile_data['last_login_date']: + last_login_time = int(profile_data["last_login_date"].timestamp()) + midnight_today_ts = int( + datetime.now() + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) + + if last_login_time > midnight_today_ts: + is_login_today = True + + if not is_login_today: + await self.data.profile.add_vp(user_id, 100) + + resp = SaoLoginResponse(user_id, profile_data['login_ct'] < 1, is_login_today) + + await self.data.profile.user_login(user_id) + return resp.make() + + async def handle_c106(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/logout + req = SaoLogoutRequest(header, request) + self.logger.info(f"User {req.user_id} Logout from {'game' if req.cabinet_type == 0 else 'terminal'} @ {src_ip} with {req.remaining_ticket_num} tickets remaining") + return SaoNoopResponse(GameconnectCmd.LOGOUT_RESPONSE).make() + + async def handle_c108(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/logout_ticket_unpurchased + req = SaoLogoutTicketUnpurchasedRequest(header, request) + self.logger.info(f"User {req.user_id} Logout from {'game' if req.cabinet_type == 0 else 'terminal'} @ {src_ip} without buying a ticket") + return SaoNoopResponse(GameconnectCmd.LOGOUT_TICKET_UNPURCHASED_RESPONSE).make() + + async def handle_c10a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/paying_play_start + req = SaoPayingPlayStartRequest(header, request) + self.logger.info(f"User {req.paying_user_id} started paying session @ {req.store_name} ({src_ip}) on cab {req.serial_no}") + resp = SaoPayingPlayStartResponse() + # TODO: session management + return resp.make() + + async def handle_c10c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/paying_play_end + req = SaoPayingPlayEndRequest(header, request) + self.logger.info(f"User {req.paying_user_id} ended paying session {req.paying_session_id} @ {req.store_name} ({src_ip}) on cab {req.serial_no} after {req.played_amount} {req.played_type} type games") + return SaoNoopResponse(GameconnectCmd.PAYING_PLAY_END_RESPONSE).make() + + async def handle_c10e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/purchase_ticket + req = SaoPurchaseTicketRequest(header, request) + self.logger.info(f"User {req.user_id} pruchased {req.purchase_num} tickets (ID {req.ticket_id}) @ {src_ip} with discout type {req.discount_type}") + return SaoNoopResponse(GameconnectCmd.PURCHASE_TICKET_RESPONSE).make() + + async def handle_c110(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/consume_ticket + req = SaoConsumeTicketRequest(header, request) + self.logger.info(f"User {req.user_id} consumed {req.consume_num} tickets (ID {req.ticket_id}) @ {src_ip} with discout type {req.discount_type} on {req.act_type}") + return SaoNoopResponse(GameconnectCmd.CONSUME_TICKET_RESPONSE).make() + + async def handle_c112(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/add_credit + req = SaoAddCreditRequest(header, request) + self.logger.info(f"User {req.user_id} added {req.add_num} credits to a {'game' if req.cabinet_type == 0 else 'terminal'} @ {src_ip}") + return SaoNoopResponse(GameconnectCmd.ADD_CREDIT_RESPONSE).make() + + async def handle_c114(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/consume_credit + req = SaoConsumeCreditRequest(header, request) + self.logger.info(f"User {req.user_id} consumed {req.consume_num} credits on a {'game' if req.cabinet_type == 0 else 'terminal'} @ {req.store_id} ({src_ip}) on {req.act_type}") + return SaoNoopResponse(GameconnectCmd.CONSUME_CREDIT_RESPONSE).make() + + async def handle_c116(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/purchase_ticket_guest + req = SaoPurchaseTicketGuestRequest(header, request) + self.logger.info(f"Guest purchased {req.purchase_num} tickets on a {'game' if req.cabinet_type == 0 else 'terminal'} @ {req.store_id} ({src_ip} | SN {req.serial_no})") + return SaoNoopResponse(GameconnectCmd.PURCHASE_TICKET_GUEST_RESPONSE).make() + + async def handle_c118(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/consume_ticket_guest + req = SaoConsumeTicketGuestRequest(header, request) + self.logger.info(f"Guest consumed {req.consume_num} tickets @ {req.store_id} ({src_ip} | SN {req.serial_no}) with discout type {req.discount_type} on {req.act_type}") + return SaoNoopResponse(GameconnectCmd.CONSUME_TICKET_GUEST_RESPONSE).make() + + async def handle_c11a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/add_credit_guest + req = SaoAddCreditGuestRequest(header, request) + self.logger.info(f"Guest added {req.add_num} credits to a {'game' if req.cabinet_type == 0 else 'terminal'} @ {req.store_id} ({src_ip} | SN {req.serial_no})") + return SaoNoopResponse(GameconnectCmd.ADD_CREDIT_GUEST_RESPONSE).make() + + async def handle_c11c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/consume_credit_guest + req = SaoConsumeCreditGuestRequest(header, request) + self.logger.info(f"Guest consumed {req.consume_num} credits on a {'game' if req.cab_type == 0 else 'terminal'} @ {req.shop_id} ({src_ip} | SN {req.serial_num}) on {req.act_type}") + return SaoNoopResponse(GameconnectCmd.CONSUME_CREDIT_GUEST_RESPONSE).make() + + async def handle_c11e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/get_auth_card_data + req = SaoGetAuthCardDataRequest(header, request) + + #Check authentication + card = await self.data.card.get_card_by_access_code( req.access_code ) + + if not card: + card = await self.data.card.get_card_by_idm(req.chip_id[:16]) + if not card: + self.logger.info(f"Unregistered card {req.access_code} authenticated from {req.serial_no}") + return SaoGetAuthCardDataResponse("NEW PLAYER", 0).make() + + await self.data.card.set_access_code_by_access_code(card['access_code'], req.access_code) + + else: + user_id = card['user'] + card_id = card['id'] + + if req.access_code.startswith("5") and not card['idm']: + await self.data.card.set_idm_by_access_code(card_id, req.chip_id[:16]) + elif (req.access_code.startswith("010") or req.access_code.startswith("3")) and not card['chip_id'] and int(req.chip_id[:8], 16) != 0x04030201: + await self.data.card.set_chip_id_by_access_code(card_id, int(req.chip_id[:8], 16)) + + self.logger.info(f"User Authenticated from {req.serial_no}: { req.access_code } | { user_id }") #Grab values from profile profile_data = await self.data.profile.get_profile(user_id) - if user_id and not profile_data: - profile_id = await self.data.profile.create_profile(user_id) - await self.data.item.put_hero_log(user_id, 101000010, 1, 0, 201000000, 0, 1002, 1003, 1014, 30001, 30310) - await self.data.item.put_hero_log(user_id, 102000010, 1, 0, 202000000, 0, 3001, 3002, 3004, 30007, 3011) - await self.data.item.put_hero_log(user_id, 105000010, 1, 0, 209000000, 0, 10005, 10002, 10004, 30006, 10003) - await self.data.item.put_hero_log(user_id, 101000110, 1, 0, 201000000, 101000110, 2002, 2001, 2014, 0, 0) - await self.data.item.put_hero_party(user_id, 0, 101000010, 102000010, 105000010) - await self.data.item.put_equipment_data(user_id, 201000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 202000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 209000000, 1, 200, 0, 0, 0) - await self.data.item.put_equipment_data(user_id, 101000110, 1, 200, 0, 0, 0) - await self.data.item.put_player_quest(user_id, 1001, True, 300, 0, 0, 1) + if not profile_data: + self.logger.info(f"Unregistered user {user_id} with card {req.access_code} authenticated from {req.serial_no}") + return SaoGetAuthCardDataResponse("NEW PLAYER", user_id).make() - # Force the tutorial stage to be completed due to potential crash in-game + return SaoGetAuthCardDataResponse(profile_data['nick_name'], user_id).make() + async def handle_c120(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/get_access_code_by_keitai + req = SaoGetAccessCodeByKeitaiRequest(header, request) + cid = req.chip_id + idm = cid[:16] + pmm = cid[16:] + card = await self.data.card.get_card_by_idm(idm) + + # If we don't have that card saved locally, check aimedb + if not card: + # Validate that we're talking to a phone + if not int(pmm[2:4], 16) in self.data.card.moble_os_codes: + self.logger.warn(f"{req.serial_no} looked up non-moble chip ID {cid}!") + return SaoGetAccessCodeByKeitaiResponse("").make() + + # TODO: Actual felica moble registration + return SaoGetAccessCodeByKeitaiResponse("").make() + #ac = await self.data.card.register_felica_moble_ac(idm, pmm) + # if we didn't get an access code, fail hard + if not ac: + self.logger.warn(f"Failed to register access code for chip ID {cid} requested by {req.serial_no}") + return SaoGetAccessCodeByKeitaiResponse("").make() + + self.logger.info(f"Successfully registered moble felica access code {ac} for chip ID {cid} requested by {req.serial_no}") + + uid = await self.data.user.create_user() + if not uid: + self.logger.error(f"Failed to create user for chip ID {cid} (access code {ac}) @ LoadAccessCode request from {req.serial_no}") + return SaoGetAccessCodeByKeitaiResponse("").make() + + cardid = await self.data.card.create_card(uid, ac) + if not cardid: + self.logger.error(f"Failed to create card for user {uid} with chip ID {cid} (access code {ac}) @ LoadAccessCode request from {req.serial_no}") + await self.data.user.delete_user(uid) + return SaoGetAccessCodeByKeitaiResponse("").make() + + self.logger.info(f"Moble Felica access code lookup for {cid} -> {ac} (user {uid}) requested by {req.serial_no}") + + else: + ac = card['access_code'] + uid = card['user'] + self.logger.info(f"Moble Felica access code for {cid} -> {ac} (user {uid}) requested by {req.serial_no}") + + return SaoGetAccessCodeByKeitaiResponse(ac).make() - profile_data = await self.data.profile.get_profile(user_id) - - resp = SaoGetAuthCardDataResponse(header.cmd +1, profile_data) + async def handle_c122(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/get_maintenance_info + resp = SaoGetMaintenanceInfoResponse() return resp.make() - async def handle_c40c(self, header: SaoRequestHeader, request: bytes) -> bytes: - #home/check_ac_login_bonus - resp = SaoHomeCheckAcLoginBonusResponse(header.cmd +1) + async def handle_c124(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/get_resource_path_info + resp = SaoGetResourcePathInfoResponse(f"https://{self.core_cfg.server.hostname}/saoresource/") return resp.make() - async def handle_c104(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/login - req = SaoCommonLoginRequest(header, request) + async def handle_c126(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/validation_error_notification + req = SaoValidationErrorNotificationRequest(header, request) + self.logger.warn(f"User {req.user_id} on {'game' if req.cabinet_type == 0 else 'terminal'} {req.serial_no} @ {req.store_name} ({src_ip} | Place ID {req.place_id}) " \ + + f"Validation error: {req.send_protocol_name} || {req.send_data_to_fraud_value} || {req.send_data_to_modification_value}") + return SaoNoopResponse(GameconnectCmd.VALIDATION_ERROR_NOTIFICATION_RESPONSE).make() - user_id = await self.data.card.get_user_id_from_card( req.access_code ) - profile_data = await self.data.profile.get_profile(user_id) + async def handle_c128(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/power_cutting_return_notification + req = SaoPowerCuttingReturnNotification(header, request) + self.logger.warn(f"User {req.user_id} on {'game' if req.cabinet_type == 0 else 'terminal'} {req.serial_no} @ {req.store_name} ({src_ip} | Place ID {req.place_id}) " \ + + f"Power outage return: Act Type {req.last_act_type} || {req.remaining_ticket_num} Remaining Tickets || {req.remaining_credit_num} Remaining Credits") + return SaoNoopResponse(GameconnectCmd.POWER_CUTTING_RETURN_NOTIFICATION_RESPONSE).make() - resp = SaoCommonLoginResponse(header.cmd +1, profile_data) + async def handle_c12a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/give_free_ticket + req = SaoGiveFreeTicketRequest(header, request) + self.logger.info(f"Give {req.give_num} free tickets (id {req.ticket_id}) to user {req.user_id}") + resp = SaoNoopResponse(GameconnectCmd.GIVE_FREE_TICKET_RESPONSE) return resp.make() - async def handle_c404(self, header: SaoRequestHeader, request: bytes) -> bytes: - #home/check_comeback_event - resp = SaoCheckComebackEventRequest(header.cmd +1) + async def handle_c12c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # common/matching_error_notification + req = SaoMatchingErrorNotificationRequest(header, request) + self.logger.warn(f"{'game' if req.cabinet_type == 0 else 'terminal'} {req.serial_no} @ {req.store_name} ({src_ip} | Place ID {req.place_id}) " \ + + f"Matching error: {req.matching_error_data_list[0]}") + return SaoNoopResponse(GameconnectCmd.MATCHING_ERROR_NOTIFICATION_RESPONSE).make() + + async def handle_c12e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #common/ac_cabinet_boot_notification + req = SaoCommonAcCabinetBootNotificationRequest(header, request) + + if req.current_version_app_id < self.game_cfg.server.game_version: + self.logger.info(f"!!OUTDATED!! {'Game' if req.cabinet_type == 0 else 'Terminal'} {req.serial_no} Booted v{req.current_version_app_id} (Master data v{req.current_master_data_version}): {req.store_name} ({src_ip} | Place/Shop ID {req.place_id}/{req.store_id})") + + if req.current_version_app_id > self.game_cfg.server.game_version: + self.logger.info(f"!!TOO NEW!! {'Game' if req.cabinet_type == 0 else 'Terminal'} {req.serial_no} Booted v{req.current_version_app_id} (Master data v{req.current_master_data_version}): {req.store_name} ({src_ip} | Place/Shop ID {req.place_id}/{req.store_id})") + + self.logger.info(f"{'Game' if req.cabinet_type == 0 else 'Terminal'} {req.serial_no} Booted v{req.current_version_app_id} (Master data v{req.current_master_data_version}): {req.store_name} ({src_ip} | Place/Shop ID {req.place_id}/{req.store_id})") + resp = SaoNoopResponse(GameconnectCmd.AC_CABINET_BOOT_NOTIFICATION_RESPONSE) return resp.make() - async def handle_c000(self, header: SaoRequestHeader, request: bytes) -> bytes: - #ticket/ticket - resp = SaoTicketResponse(header.cmd +1) + async def handle_c200(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # tutorial/first_tutorial_end + req = SaoGenericUserTicketRequest(header, request) + self.logger.info(f"User {req.user_id} (ticket {req.ticket_id}) finished first tutorial") + return SaoNoopResponse(GameconnectCmd.FIRST_TUTORIAL_END_RESPONSE).make() + + async def handle_c202(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # tutorial/various_tutorial_end + req = SaoVariousTutorialEndRequest(header, request) + await self.data.profile.add_tutorial_byte(int(req.user_id), req.tutorial_type) + return SaoNoopResponse(GameconnectCmd.VARIOUS_TUTORIAL_END_RESPONSE).make() + + async def handle_c204(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # tutorial/get_various_tutorial_data_list + req = SaoGenericUserRequest(header, request) + tuts = await self.data.profile.get_tutorial_bytes(int(req.user_id)) + resp = SaoGetVariousTutorialDataListResponse() + if tuts: + for t in tuts: + resp.end_tutorial_type_list.append(t['tutorial_byte']) + return resp.make() - async def handle_c500(self, header: SaoRequestHeader, request: bytes) -> bytes: - #user_info/get_user_basic_data + async def handle_c300(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # card/discharge_profile_card + req = SaoDischargeProfileCardRequest(header, request) + # Real cards seem to start with 10-17 as the first 2 digits, so we'll anchor ours with 2 to ensure no overlap + sn = f"2{str(randint(1, 999999999999999999)).zfill(18)}" + while await self.data.profile.get_hero_card(sn): + sn = f"2{str(randint(1, 999999999999999999)).zfill(18)}" + resp = SaoDischargeProfileCardResponse(sn) + + db_hero = await self.data.item.get_hero_log(req.user_id, req.hero_log_user_hero_log_id) + if not db_hero: + hero_statc = await self.data.static.get_hero_by_id(db_hero['hero_log_id']) + if not hero_statc: + self.logger.error(f"Failed to find hero log {db_hero['hero_log_id']}! Please run the reader") + resp.header.error_type = ProtocolErrorNum.RESOURCE_CARD_ERR1 + return resp.make() + + now_have_skills = await self.hero_default_skills(hero_statc['SkillTableSubId']) + + db_hero_id = await self.data.item.put_hero_log( + req.user_id, + db_hero['hero_log_id'], + 1, + 0, + None, + None, + now_have_skills[0], + now_have_skills[1], + now_have_skills[2], + now_have_skills[3], + now_have_skills[4] + ) + if not db_hero_id: + self.logger.error(f"Failed to give user {req.user_id} hero {db_hero['hero_log_id']}!") + resp.header.error_type = ProtocolErrorNum.RESOURCE_CARD_ERR6 + return resp.make() + + else: + db_hero_id = db_hero['id'] + + await self.data.profile.put_hero_card(req.user_id, sn, db_hero_id, req.holographic_flag) + + self.logger.info(f"User {req.user_id} printed {'holo ' if req.holographic_flag == 1 else ''}profile card {req.hero_log_user_hero_log_id} {req.execute_print_type}, code {sn}") + return resp.make() + + async def handle_c302(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # card/discharge_resource_card + req = SaoDischargeResourceCardRequest(header, request) + + for x in req.common_reward_user_data: + sn = f"2{str(randint(1, 999999999999999999)).zfill(18)}" + self.logger.info(f"User {req.user_id} printed {'holo ' if req.holographic_flag == 1 else ''}resource card {x.user_common_reward_id} {req.execute_print_type}, code {sn}") + await self.data.profile.put_resource_card(req.user_id, sn, x.common_reward_type, x.user_common_reward_id, req.holographic_flag) + + return SaoDischargeProfileCardResponse(sn).make() + + async def handle_c304(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # card/discharge_resource_card_complete + req = SaoDischargeResourceCardCompleteRequest(header, request) + self.logger.info(f"User {req.user_id} finished printing resource card {req.resource_card_code}") + return SaoNoopResponse(GameconnectCmd.DISCHARGE_RESOURCE_CARD_COMPLETE_RESPONSE).make() + + async def handle_c306(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #card/scan_qr_quest_profile_card + req = SaoScanQrQuestProfileCardRequest(header, request) + resp = SaoScanQrQuestProfileCardResponse() + + card = await self.data.profile.get_hero_card(req.profile_card_code) + if not card: + self.logger.warn(f"User {req.user_id} scanned unregistered QR code {req.profile_card_code}") + return resp.make() + + hero = await self.data.item.get_hero_log_by_id(card['user_hero_id']) + if not hero: # Shouldn't happen + self.logger.warn(f"User {req.user_id} scanned QR code {req.profile_card_code} but does not have hero entry {card['user_hero_id']}") + return resp.make() + + hero_static_data = await self.data.static.get_hero_by_id(hero['hero_log_id']) + if not hero_static_data: # Shouldn't happen + self.logger.warn(f"No entry for hero {hero['hero_log_id']}, please run read.py") + return resp.make() + + profile = await self.data.profile.get_profile(card['user']) + if not profile: # Shouldn't happen + self.logger.warn(f"No profile for user {card['user']}, something broke") + return resp.make() + + self.logger.info(f"User {req.user_id} scanned QR code {req.profile_card_code}") + card_resp = ReadProfileCard.from_args(req.profile_card_code, profile['nick_name']) + card_resp.rank_num = profile['rank_num'] + card_resp.setting_title_id = profile['setting_title_id'] + card_resp.skill_id = hero['skill_slot1_skill_id'] + card_resp.hero_log_hero_log_id = hero['hero_log_id'] + card_resp.hero_log_log_level = hero['log_level'] + #TODO: Awakening + #card_resp.hero_log_awakening_stage = hero['log_level'] + + resp.profile_card_data.append(card_resp) + return resp.make() + + async def handle_c308(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # card/scan_qr_shop_resource_card + req = SaoScanQrShopResourceCardRequest(header, request) + self.logger.info(f"User {req.user_id} scanned shop resource card {req.resource_card_code}") + resp = SaoNoopResponse(GameconnectCmd.SCAN_QR_SHOP_RESOURCE_CARD_RESPONSE) + # On official, resource cards have limited uses, but we don't track that currently (tho we should) + + card = await self.data.profile.get_resource_card(req.resource_card_code) # TODO: use count + if not card: + self.logger.warn(f"No resource card with serial {req.resource_card_code} exists!") + resp.header.err_status = 4832 # Theres a few error codes but none seem to do anything? + # Also not sure if it should be this or result + + return resp.make() + + async def handle_c30a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # card/scan_qr_quest_resource_card + req = SaoScanQrQuestResourceCardRequest(header, request) + self.logger.info(f"User {req.user_id} scanned quest resource card {req.resource_card_code}") + + card = await self.data.profile.get_resource_card(req.resource_card_code) + if not card: + resp = SaoScanQrQuestResourceCardResponse(card['common_reward_type'], card['common_reward_id'], card['holographic_flag']) + + else: + self.logger.warn(f"No resource card with serial {req.resource_card_code} exists!") + resp = SaoScanQrQuestResourceCardResponse() + resp.header.err_status = 4832 # Theres a few error codes but none seem to do anything? + # Also not sure if it should be this or result + + return resp.make() + + async def handle_c400(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #home/check_yui_medal_get_condition + req = SaoCheckYuiMedalGetConditionRequest(header, request) + profile = await self.data.profile.get_profile(req.user_id) + if profile['last_yui_medal_date']: + last_check_ts = int(profile['last_yui_medal_date'].timestamp()) + day_ts = int(datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()) + diff_ts = day_ts - last_check_ts + if diff_ts > 0: + num_days = diff_ts / 86400 + else: + num_days = 0 + else: + num_days = 1 + + if num_days > 1: + await self.data.profile.add_yui_medals(req.user_id) + + await self.data.profile.update_yui_medal_date(req.user_id) + return SaoCheckYuiMedalGetConditionResponse(num_days, 1 if num_days > 1 else 0).make() + + async def handle_c402(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #home/get_yui_medal_bonus_user_data + req = SaoGetYuiMedalBonusUserDataRequest(header, request) + resp = SaoGetYuiMedalBonusUserDataResponse() # TODO: Track yui login bonus + return resp.make() + + async def handle_c404(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # home/check_comeback_event + req = SaoGenericUserTicketRequest(header, request) + resp = SaoCheckComebackEventResponse() + #resp.get_comeback_event_id_list += [1,2,3,4] # TODO: track comeback date + return resp.make() + + async def handle_c406(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # home/change_my_store + req = SaoChangeMyStoreRequest(header, request) + self.logger.info(f"User {req.user_id} changed My Store to {req.store_id}") + shop_id = int(req.store_id[3:], 16) + await self.data.profile.set_my_shop(req.user_id, shop_id) + return SaoNoopResponse(GameconnectCmd.CHANGE_MY_STORE_RESPONSE).make() + + async def handle_c408(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # home/check_title_get_decision + req = SaoCheckTitleGetDecisionRequest(header, request) + resp = SaoCheckTitleGetDecisionResponse() # TODO: titles + return resp.make() + + async def handle_c40a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # home/check_profile_card_used_reward + req = SaoCheckProfileCardUsedRewardRequest(header, request) + resp = SaoCheckProfileCardUsedRewardResponse() # TODO: check_profile_card_used_reward + return resp.make() + + async def handle_c40c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # home/check_ac_login_bonus + req = SaoGenericUserTicketRequest(header, request) + resp = SaoCheckAcLoginBonusResponse() + #resp.get_ac_login_bonus_id_list.append(1) # TODO: track login bonus date + #resp.get_ac_login_bonus_id_list.append(2) + return resp.make() + + async def handle_c500(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # user_info/get_user_basic_data req = SaoGetUserBasicDataRequest(header, request) profile_data = await self.data.profile.get_profile(req.user_id) + player_rank_data = self.load_data_csv("PlayerRank") - resp = SaoGetUserBasicDataResponse(header.cmd +1, profile_data) - return resp.make() + resp = SaoGetUserBasicDataResponse(profile_data) + for e in player_rank_data: + if resp.user_basic_data[0].rank_num == int(e['PlayerRankId']): + resp.user_basic_data[0].rank_exp = resp.user_basic_data[0].rank_exp - int(e["TotalExp"]) + break - async def handle_c600(self, header: SaoRequestHeader, request: bytes) -> bytes: - #have_object/get_hero_log_user_data_list - req = SaoGetHeroLogUserDataListRequest(header, request) + if profile_data['my_shop']: + ac = await self.data.arcade.get_arcade(profile_data['my_shop']) + if ac: + resp.user_basic_data[0].my_store_name = ac['name'] + + return resp.make() + + async def handle_c502(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # user_info/get_vp_gasha_ticket_data_list + req = SaoGetVpGashaTicketDataListRequest(header, request) + # TODO: gasha tickets + resp = SaoGetVpGashaTicketDataListResponse() + return resp.make() + + async def handle_c504(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # user_info/get_present_box_num + req = SaoGenericUserRequest(header, request) + # TODO: presents + return SaoGetPresentBoxNumResponse().make() + + async def handle_c600(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_hero_log_user_data_list + req = SaoGenericUserRequest(header, request) + hero_level_data = self.load_data_csv("HeroLogLevel") hero_data = await self.data.item.get_hero_logs(req.user_id) - resp = SaoGetHeroLogUserDataListResponse(header.cmd +1, hero_data) + resp = SaoGetHeroLogUserDataListResponse() + for hero in hero_data: + append = HeroLogUserData.from_args(hero) + hero_static = await self.data.static.get_hero_by_id(hero['hero_log_id']) + if not hero_static: + self.logger.warn(f"No hero for id {hero['hero_log_id']}, please run reader") + resp.hero_log_user_data_list.append(append) + continue + + append.property1_property_id = hero_static['Property1PropertyId'] if hero_static['Property1PropertyId'] else 0 + append.property1_value1 = hero_static['Property1Value1'] if hero_static['Property1Value1'] else 0 + append.property1_value2 = hero_static['Property1Value2'] if hero_static['Property1Value2'] else 0 + append.property2_property_id = hero_static['Property2PropertyId'] if hero_static['Property2PropertyId'] else 0 + append.property2_value1 = hero_static['Property2Value1'] if hero_static['Property2Value1'] else 0 + append.property2_value2 = hero_static['Property2Value2'] if hero_static['Property2Value2'] else 0 + append.property3_property_id = hero_static['Property3PropertyId'] if hero_static['Property3PropertyId'] else 0 + append.property3_value1 = hero_static['Property3Value1'] if hero_static['Property3Value1'] else 0 + append.property3_value2 = hero_static['Property3Value2'] if hero_static['Property3Value2'] else 0 + append.property4_property_id = hero_static['Property4PropertyId'] if hero_static['Property4PropertyId'] else 0 + append.property4_value1 = hero_static['Property4Value1'] if hero_static['Property4Value1'] else 0 + append.property4_value2 = hero_static['Property4Value2'] if hero_static['Property4Value2'] else 0 + + for e in hero_level_data: + if hero['log_level'] == int(e['HeroLogLevelId']): + append.log_exp = hero['log_exp'] - int(e["TotalExp"]) + break + resp.hero_log_user_data_list.append(append) + return resp.make() - - async def handle_c602(self, header: SaoRequestHeader, request: bytes) -> bytes: - #have_object/get_equipment_user_data_list - req = SaoGetEquipmentUserDataListRequest(header, request) + + async def handle_c602(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_equipment_user_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetEquipmentUserDataListResponse() + equip_level_data = self.load_data_csv("EquipmentLevel") equipment_data = await self.data.item.get_user_equipments(req.user_id) + + if equipment_data: + for equipment in equipment_data: + e = EquipmentUserData.from_args(equipment) + weapon_static = await self.data.static.get_equipment_by_id(equipment['equipment_id']) + if not weapon_static: + self.logger.warn(f"No equipment for id {equipment['equipment_id']}, please run reader") + resp.equipment_user_data_list.append(e) + continue + + if not e.property1_property_id: + e.property1_property_id = weapon_static['Property1PropertyId'] if weapon_static['Property1PropertyId'] else 0 + e.property1_value1 = weapon_static['Property1Value1'] if weapon_static['Property1Value1'] else 0 + e.property1_value2 = weapon_static['Property1Value2'] if weapon_static['Property1Value2'] else 0 + + if not e.property2_property_id: + e.property2_property_id = weapon_static['Property2PropertyId'] if weapon_static['Property2PropertyId'] else 0 + e.property2_value1 = weapon_static['Property2Value1'] if weapon_static['Property2Value1'] else 0 + e.property2_value2 = weapon_static['Property2Value2'] if weapon_static['Property2Value2'] else 0 + + if e.property3_property_id: + e.property3_property_id = weapon_static['Property3PropertyId'] if weapon_static['Property3PropertyId'] else 0 + e.property3_value1 = weapon_static['Property3Value1'] if weapon_static['Property3Value1'] else 0 + e.property3_value2 = weapon_static['Property3Value2'] if weapon_static['Property3Value2'] else 0 + + if e.property4_property_id: + e.property4_property_id = weapon_static['Property4PropertyId'] if weapon_static['Property4PropertyId'] else 0 + e.property4_value1 = weapon_static['Property4Value1'] if weapon_static['Property4Value1'] else 0 + e.property4_value2 = weapon_static['Property4Value2'] if weapon_static['Property4Value2'] else 0 + + for f in equip_level_data: + if equipment['enhancement_value'] == int(f['EquipmentLevelId']): + e.enhancement_exp = equipment['enhancement_exp'] - int(f["TotalExp"]) + break + resp.equipment_user_data_list.append(e) - resp = SaoGetEquipmentUserDataListResponse(header.cmd +1, equipment_data) return resp.make() - - async def handle_c604(self, header: SaoRequestHeader, request: bytes) -> bytes: - #have_object/get_item_user_data_list - req = SaoGetItemUserDataListRequest(header, request) + + async def handle_c604(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_item_user_data_list + req = SaoGenericUserRequest(header, request) item_data = await self.data.item.get_user_items(req.user_id) - resp = SaoGetItemUserDataListResponse(header.cmd +1, item_data) + resp = SaoGetItemUserDataListResponse(item_data) return resp.make() + + async def handle_c606(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_support_log_user_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetSupportLogUserDataListResponse() + supports = self.load_data_csv("SupportLog") + # TODO: Save supports - async def handle_c606(self, header: SaoRequestHeader, request: bytes) -> bytes: - #have_object/get_support_log_user_data_list - supportIdsData = await self.data.static.get_support_log_ids(0, True) - - resp = SaoGetSupportLogUserDataListResponse(header.cmd +1, supportIdsData) + for x in range(len(supports)): + tmp = SupportLogUserData.from_args(f"{req.user_id}{x}", supports[x]['SupportLogId']) + resp.support_log_user_data_list.append(tmp) + return resp.make() + + async def handle_c608(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #have_object/get_episode_append_data_list + req = SaoGenericUserRequest(header, request) + # TODO: Appends + resp = SaoGetEpisodeAppendDataListResponse() return resp.make() - async def handle_c800(self, header: SaoRequestHeader, request: bytes) -> bytes: - #custom/get_title_user_data_list + async def handle_c60a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_event_item_data_list + req = SaoGenericUserRequest(header, request) + res = SaoGetEventItemDataListResponse() + # TODO: Event items maybe + return res.make() + + async def handle_c60c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # have_object/get_gasha_medal_user_data_list + req = SaoGenericUserRequest(header, request) + res = SaoGetGashaMedalUserDataListResponse() + # TODO: Gasha Medal data + return res.make() + + async def handle_c700(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # shop/get_shop_resource_sales_data_list + # TODO: Get user shop data + req = SaoGenericUserRequest(header, request) + resp = SaoGetShopResourceSalesDataListResponse() + return resp.make() + + async def handle_c702(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # shop/purchase_shop_resource + req = SaoPurchaseShopResourceRequest(header, request) + self.logger.infof(f"User {req.user_id} (ticket {req.ticket_id}) purchased shop resourse {req.user_shop_resource_id}") + # TODO: Shop purchases + return SaoNoopResponse(GameconnectCmd.PURCHASE_SHOP_RESOURCE_RESPONSE).make() + + async def handle_c704(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # shop/discard_shop_resource + req = SaoPurchaseShopResourceRequest(header, request) + self.logger.infof(f"User {req.user_id} (ticket {req.ticket_id}) discarded shop resourse {req.user_shop_resource_id}") + return SaoNoopResponse(GameconnectCmd.DISCARD_SHOP_RESOURCE_RESPONSE).make() + + async def handle_c800(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # custom/get_title_user_data_list + req = SaoGenericUserRequest(header, request) titleIdsData = await self.data.static.get_title_ids(0, True) + # TODO: Save titles - resp = SaoGetTitleUserDataListResponse(header.cmd +1, titleIdsData) + resp = SaoGetTitleUserDataListResponse(req.user_id, titleIdsData) return resp.make() + + async def handle_c802(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # custom/change_title + req = SaoChangeTitleRequest(header, request) + self.logger.info(f"User {req.user_id} (ticket {req.ticket_id}) changed their title to {req.user_title_id}") + await self.data.profile.set_title(req.user_id, req.user_title_id) - async def handle_c608(self, header: SaoRequestHeader, request: bytes) -> bytes: - #have_object/get_episode_append_data_list - req = SaoGetEpisodeAppendDataListRequest(header, request) + return SaoNoopResponse(GameconnectCmd.CHANGE_TITLE_RESPONSE).make() - profile_data = await self.data.profile.get_profile(req.user_id) + async def handle_c804(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # custom/get_party_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetPartyDataListResponse() + + hero_parties = await self.data.item.get_hero_party(req.user_id) + for party in hero_parties: + hero1_data = await self.data.item.get_user_hero_by_id(party['user_hero_log_id_1']) + hero2_data = await self.data.item.get_user_hero_by_id(party['user_hero_log_id_2']) + hero3_data = await self.data.item.get_user_hero_by_id(party['user_hero_log_id_3']) + + resp.party_data_list.append(PartyData.from_args(party['id'], party['user_party_team_id'], hero1_data._asdict(), hero2_data._asdict(), hero3_data._asdict())) - resp = SaoGetEpisodeAppendDataListResponse(header.cmd +1, profile_data) return resp.make() - async def handle_c804(self, header: SaoRequestHeader, request: bytes) -> bytes: - #custom/get_party_data_list - req = SaoGetPartyDataListRequest(header, request) - - hero_party = await self.data.item.get_hero_party(req.user_id, 0) - hero1_data = await self.data.item.get_hero_log(req.user_id, hero_party[3]) - hero2_data = await self.data.item.get_hero_log(req.user_id, hero_party[4]) - hero3_data = await self.data.item.get_hero_log(req.user_id, hero_party[5]) - - resp = SaoGetPartyDataListResponse(header.cmd +1, hero1_data, hero2_data, hero3_data) + async def handle_c808(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # custom/get_support_log_party_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetSupportLogPartyDataListResponse() + # TODO: Support logs return resp.make() - async def handle_c902(self, header: SaoRequestHeader, request: bytes) -> bytes: # for whatever reason, having all entries empty or filled changes nothing - #quest/get_quest_scene_prev_scan_profile_card - resp = SaoGetQuestScenePrevScanProfileCardResponse(header.cmd +1) - return resp.make() + async def handle_c812(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # custom/disposal_resource + req = SaoDisposalResourceRequest(header, request) + get_col = 0 + for disposal in req.disposal_common_reward_user_data_list: + if disposal.common_reward_type == RewardType.HeroLog: + removed_hero = await self.data.item.remove_hero_log(disposal.user_common_reward_id) + if removed_hero: + static_hero = await self.data.static.get_hero_by_id(removed_hero) + get_col += static_hero['SalePrice'] + + elif disposal.common_reward_type == RewardType.Equipment: + removed_equip = await self.data.item.remove_equipment(disposal.user_common_reward_id) + if removed_equip: + static_equip = await self.data.static.get_equipment_by_id(removed_equip) + get_col += static_equip['SalePrice'] - async def handle_c124(self, header: SaoRequestHeader, request: bytes) -> bytes: - #common/get_resource_path_info - resp = SaoGetResourcePathInfoResponse(header.cmd +1) - return resp.make() + elif disposal.common_reward_type == RewardType.Item: + removed_equip = await self.data.item.remove_item(disposal.user_common_reward_id) + if removed_equip: + static_equip = await self.data.static.get_equipment_by_id(removed_equip) + get_col += static_equip['SalePrice'] - async def handle_c900(self, header: SaoRequestHeader, request: bytes) -> bytes: - #quest/get_quest_scene_user_data_list // QuestScene.csv - req = SaoGetQuestSceneUserDataListRequest(header, request) + elif disposal.common_reward_type == RewardType.SupportLog: + continue - quest_data = await self.data.item.get_quest_logs(req.user_id) + else: + self.logger.warn(f"Unhandled disposal type {disposal.common_reward_type}") - resp = SaoGetQuestSceneUserDataListResponse(header.cmd +1, quest_data) - return resp.make() + await self.data.profile.add_col(req.user_id, get_col) + return SaoDisposalResourceResponse(get_col).make() - async def handle_c400(self, header: SaoRequestHeader, request: bytes) -> bytes: - #home/check_yui_medal_get_condition - resp = SaoCheckYuiMedalGetConditionResponse(header.cmd +1) - return resp.make() - - async def handle_c402(self, header: SaoRequestHeader, request: bytes) -> bytes: - #home/get_yui_medal_bonus_user_data - resp = SaoGetYuiMedalBonusUserDataResponse(header.cmd +1) - return resp.make() - - async def handle_c40a(self, header: SaoRequestHeader, request: bytes) -> bytes: - #home/check_profile_card_used_reward - resp = SaoCheckProfileCardUsedRewardResponse(header.cmd +1) - return resp.make() - - async def handle_c814(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c814(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #custom/synthesize_enhancement_hero_log req = SaoSynthesizeEnhancementHeroLogRequest(header, request) - - synthesize_hero_log_data = await self.data.item.get_hero_log(req.user_id, req.origin_user_hero_log_id) + resp = SaoSynthesizeEnhancementHeroLogResponse() + hero_level_data = self.load_data_csv("HeroLogLevel") + equip_level_data = self.load_data_csv("EquipmentLevel") + hero_exp = 0 + col_cost = 0 for x in req.material_common_reward_user_data_list: - hero_exp = 0 - itemList = await self.data.static.get_item_id(x.user_common_reward_id) - heroList = await self.data.static.get_hero_id(x.user_common_reward_id) - equipmentList = await self.data.static.get_equipment_id(x.user_common_reward_id) + if x.common_reward_type == RewardType.Item: + user_item = await self.data.item.get_user_item_by_id(x.user_common_reward_id) + static_item = await self.data.static.get_item_id(user_item['item_id']) + + if int(static_item['ItemTypeId']) == 7: + hero_exp += int(static_item['Value']) + self.logger.info(f"Remove item {x.user_common_reward_id} and add {int(static_item['Value'])} XP (running {hero_exp})") + await self.data.item.remove_item(x.user_common_reward_id) - if itemList: - hero_exp = 2000 + int(synthesize_hero_log_data["log_exp"]) - await self.data.item.remove_item(req.user_id, x.user_common_reward_id) - - if equipmentList: - equipment_data = await self.data.item.get_user_equipment(req.user_id, x.user_common_reward_id) + elif x.common_reward_type == RewardType.Equipment: + equipment_data = await self.data.item.get_user_equipment_by_id(x.user_common_reward_id) if equipment_data is None: self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req.user_id}!") continue - hero_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_hero_log_data["log_exp"]) - await self.data.item.remove_equipment(req.user_id, x.user_common_reward_id) + req_exp = 0 + for e in range(len(equip_level_data)): + if equipment_data['enhancement_value'] == int(equip_level_data[e]['EquipmentLevelId']): + req_exp = equip_level_data[e + 1]['RequireExp'] + break + + static_equip_data = await self.data.static.get_equipment_by_id(equipment_data['equipment_id']) + + hero_exp += int(static_equip_data['CompositionExp']) + req_exp + self.logger.info(f"Remove equipment {x.user_common_reward_id} and add {int(static_equip_data['CompositionExp']) + req_exp} XP (running {hero_exp})") + await self.data.item.remove_equipment(x.user_common_reward_id) - if heroList: - hero_data = await self.data.item.get_hero_log(req.user_id, x.user_common_reward_id) + elif x.common_reward_type == RewardType.HeroLog: + hero_data = await self.data.item.get_hero_log_by_id(x.user_common_reward_id) if hero_data is None: self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req.user_id}!") continue + + req_exp = 0 + for e in range(len(hero_level_data)): + if hero_data['log_level'] == int(hero_level_data[e]['HeroLogLevelId']): + req_exp = hero_level_data[e + 1]['RequireExp'] + break - hero_exp = int(hero_data["log_exp"]) + int(synthesize_hero_log_data["log_exp"]) - await self.data.item.remove_hero_log(req.user_id, x.user_common_reward_id) + static_hero_data = await self.data.static.get_hero_by_id(hero_data['hero_log_id']) + + hero_exp += int(static_hero_data['CompositionExp']) + req_exp + self.logger.info(f"Remove hero {x.user_common_reward_id} and add {int(static_hero_data['CompositionExp']) + req_exp} XP (running {hero_exp})") + await self.data.item.remove_hero_log(x.user_common_reward_id) - if hero_exp == 0: - self.logger.warn(f"Hero {x.user_common_reward_id} (type {x.common_reward_type}) not found!") + else: + self.logger.warn(f"Unhandled ype {x.common_reward_type}! (running {hero_exp})") + + hero_exp = int(hero_exp * 1.5) + await self.data.item.add_hero_xp(req.origin_user_hero_log_id, hero_exp) + log_exp = await self.data.item.get_hero_xp(req.origin_user_hero_log_id) + pre_synth_xp = log_exp - hero_exp + pre_synth_level = 1 + + for e in range(len(hero_level_data)): + if log_exp>=int(hero_level_data[e]["TotalExp"]) and log_exp=int(hero_level_data[e]["TotalExp"]) and pre_synth_xp= 100: + col_cost = col_cost * 10 + + col_cost = col_cost * 1000 + col_cost = max(100, int(col_cost / 1000)) + + self.logger.info(f"Synthesize {hero_exp} exp for hero {req.origin_user_hero_log_id}, costing {col_cost} col") + + await self.data.profile.add_col(req.user_id, -col_cost) + if synthesize_hero_log_data is not None: + resp.after_hero_log_user_data.append(HeroLogUserData.from_args(synthesize_hero_log_data)) + resp.after_hero_log_user_data[0].log_exp = remain_exp return resp.make() - async def handle_c816(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c816(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #custom/synthesize_enhancement_equipment req_data = SaoSynthesizeEnhancementEquipmentRequest(header, request) - synthesize_equipment_data = await self.data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + resp = SaoSynthesizeEnhancementEquipmentResponse() + hero_level_data = self.load_data_csv("HeroLogLevel") + equip_level_data = self.load_data_csv("EquipmentLevel") + equipment_exp = 0 + col_cost = 0 for x in req_data.material_common_reward_user_data_list: - equipment_exp = 0 - itemList = await self.data.static.get_item_id(x.user_common_reward_id) - heroList = await self.data.static.get_hero_id(x.user_common_reward_id) - equipmentList = await self.data.static.get_equipment_id(x.user_common_reward_id) + if x.common_reward_type == RewardType.Item: + user_item = await self.data.item.get_user_item_by_id(x.user_common_reward_id) + static_item = await self.data.static.get_item_id(user_item['item_id']) + + if int(static_item['ItemTypeId']) == 7: + equipment_exp += int(static_item['Value']) + self.logger.info(f"Remove item {x.user_common_reward_id} and add {int(static_item['Value'])} XP (running {equipment_exp})") + await self.data.item.remove_item(x.user_common_reward_id) - if itemList: - equipment_exp = 2000 + int(synthesize_equipment_data["enhancement_exp"]) - await self.data.item.remove_item(req_data.user_id, x.user_common_reward_id) - - if equipmentList: - equipment_data = await self.data.item.get_user_equipment(req_data.user_id, x.user_common_reward_id) + elif x.common_reward_type == RewardType.Equipment: + equipment_data = await self.data.item.get_user_equipment_by_id(x.user_common_reward_id) if equipment_data is None: self.logger.error(f"Failed to find equipment {x.user_common_reward_id} for user {req_data.user_id}!") continue + + req_exp = 0 + for e in range(len(equip_level_data)): + if equipment_data['enhancement_value'] == int(equip_level_data[e]['EquipmentLevelId']): + req_exp = equip_level_data[e + 1]['RequireExp'] + break + + static_equip_data = await self.data.static.get_equipment_by_id(equipment_data['equipment_id']) + + equipment_exp += int(static_equip_data['CompositionExp']) + req_exp + self.logger.info(f"Remove equipment {x.user_common_reward_id} and add {int(static_equip_data['CompositionExp']) + req_exp} XP (running {equipment_exp})") + await self.data.item.remove_equipment(x.user_common_reward_id) - equipment_exp = int(equipment_data["enhancement_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - await self.data.item.remove_equipment(req_data.user_id, x.user_common_reward_id) - - if heroList: - hero_data = await self.data.item.get_hero_log(req_data.user_id, x.user_common_reward_id) + elif x.common_reward_type == RewardType.HeroLog: + hero_data = await self.data.item.get_hero_log_by_id(x.user_common_reward_id) if hero_data is None: self.logger.error(f"Failed to find hero {x.user_common_reward_id} for user {req_data.user_id}!") continue - equipment_exp = int(hero_data["log_exp"]) + int(synthesize_equipment_data["enhancement_exp"]) - await self.data.item.remove_hero_log(req_data.user_id, x.user_common_reward_id) - - if equipment_exp == 0: - self.logger.warn(f"Common reward {x.user_common_reward_id} (type {x.common_reward_type}) not found!") - continue + req_exp = 0 + for e in range(len(hero_level_data)): + if hero_data['log_level'] == int(hero_level_data[e]['HeroLogLevelId']): + req_exp = hero_level_data[e + 1]['RequireExp'] + break + + static_hero_data = await self.data.static.get_hero_by_id(hero_data['hero_log_id']) + + equipment_exp += int(static_hero_data['CompositionExp']) + req_exp + self.logger.info(f"Remove hero {x.user_common_reward_id} and add {int(static_hero_data['CompositionExp']) + req_exp} XP (running {equipment_exp})") + await self.data.item.remove_hero_log(x.user_common_reward_id) - await self.data.item.put_equipment_data(req_data.user_id, int(req_data.origin_user_equipment_id), synthesize_equipment_data["enhancement_value"], equipment_exp, 0, 0, 0) - - profile = await self.data.profile.get_profile(req_data.user_id) - new_col = int(profile["own_col"]) - 100 - - # Update profile - - await self.data.profile.put_profile( - req_data.user_id, - profile["user_type"], - profile["nick_name"], - profile["rank_num"], - profile["rank_exp"], - new_col, - profile["own_vp"], - profile["own_yui_medal"], - profile["setting_title_id"] - ) + else: + self.logger.warn(f"Unhandled ype {x.common_reward_type}! (running {equipment_exp})") + + equipment_exp = int(equipment_exp * 1.5) + await self.data.item.add_equipment_enhancement_exp(req_data.origin_user_equipment_id, equipment_exp) + synthesize_equipment_data = await self.data.item.get_user_equipment(req_data.user_id, req_data.origin_user_equipment_id) + equip_exp_new = synthesize_equipment_data['enhancement_exp'] + pre_synth_level = synthesize_equipment_data['enhancement_value'] + new_synth_level = 1 + + for e in range(len(equip_level_data)): + if equip_exp_new>=int(equip_level_data[e]["TotalExp"]) and equip_exp_new= 100: + col_cost = col_cost * 10 + + col_cost = col_cost * 1000 + col_cost = max(100, int(col_cost / 1000)) + + self.logger.info(f"Synthesize {equipment_exp} exp for equipment {req_data.origin_user_equipment_id}, costing {col_cost} col") + + await self.data.profile.add_col(req_data.user_id, -col_cost) + if synthesize_equipment_data is not None: + resp.after_equipment_user_data.append(EquipmentUserData.from_args(synthesize_equipment_data)) + resp.after_equipment_user_data[0].enhancement_exp = remain_exp + resp.after_equipment_user_data[0].enhancement_value = new_synth_level return resp.make() - async def handle_c806(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c806(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #custom/change_party req_data = SaoChangePartyRequest(header, request) - party_hero_list = [] - for party_team in req_data.party_data_list[0].party_team_data_list: - hero_data = await self.data.item.get_hero_log(req_data.user_id, party_team.user_hero_log_id) - hero_level = 1 - hero_exp = 0 + for party_team in req_data.party_data_list: + for hero in party_team.party_team_data_list: + await self.data.item.set_user_hero_weapons( + int(hero.user_hero_log_id), + int(hero.main_weapon_user_equipment_id) if hero.main_weapon_user_equipment_id and int(hero.main_weapon_user_equipment_id) > 0 else None, + int(hero.sub_equipment_user_equipment_id) if hero.sub_equipment_user_equipment_id and int(hero.sub_equipment_user_equipment_id) > 0 else None + ) + await self.data.item.set_user_hero_skills( + int(hero.user_hero_log_id), + hero.skill_slot1_skill_id if hero.skill_slot1_skill_id > 0 else None, + hero.skill_slot2_skill_id if hero.skill_slot2_skill_id > 0 else None, + hero.skill_slot3_skill_id if hero.skill_slot3_skill_id > 0 else None, + hero.skill_slot4_skill_id if hero.skill_slot4_skill_id > 0 else None, + hero.skill_slot5_skill_id if hero.skill_slot5_skill_id > 0 else None + ) - if hero_data: - hero_level = hero_data["log_level"] - hero_exp = hero_data["log_exp"] - await self.data.item.put_hero_log( + await self.data.item.put_hero_party( req_data.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 + party_team.team_no, + party_team.party_team_data_list[0].user_hero_log_id, + party_team.party_team_data_list[1].user_hero_log_id, + party_team.party_team_data_list[2].user_hero_log_id, ) - party_hero_list.append(party_team.user_hero_log_id) - - await self.data.item.put_hero_party(req_data.user_id, req_data.party_data_list[0].party_team_data_list[0].user_party_team_id, party_hero_list[0], party_hero_list[1], party_hero_list[2]) - - resp = SaoNoopResponse(header.cmd +1) + resp = SaoNoopResponse(GameconnectCmd.CHANGE_PARTY_RESPONSE) return resp.make() - async def handle_c904(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c900(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #quest/get_quest_scene_user_data_list + req = SaoGenericUserRequest(header, request) + + quest_data = await self.data.item.get_quest_logs(req.user_id) + + resp = SaoGetQuestSceneUserDataListResponse() + for quest in quest_data: + ex_bonus_data = await self.data.item.get_player_ex_bonus_by_quest(req.user_id, quest["quest_scene_id"]) + tmp = QuestSceneUserData.from_args(quest) + + for ex_bonus in ex_bonus_data: + tmp.quest_scene_ex_bonus_user_data_list.append(QuestSceneExBonusUserData.from_args(ex_bonus['ex_bonus_table_id'], ex_bonus['quest_clear_flag'])) + + resp.quest_scene_user_data_list.append(tmp) + return resp.make() + + async def handle_c902(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # for whatever reason, having all entries empty or filled changes nothing + #quest/get_quest_scene_prev_scan_profile_card + resp = SaoGetQuestScenePrevScanProfileCardResponse() + resp.profile_card_data.append(ReadProfileCardData.from_args({}, {})) + return resp.make() + + async def handle_c904(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #quest/episode_play_start req_data = SaoEpisodePlayStartRequest(header, request) user_id = req_data.user_id profile_data = await self.data.profile.get_profile(user_id) - await self.data.item.create_session( + sesh_id = await self.data.item.create_session( user_id, int(req_data.play_start_request_data[0].user_party_id), req_data.episode_id, @@ -446,178 +1175,228 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) + resp = SaoEpisodePlayStartResponse() + resp.play_start_response_data.append(QuestScenePlayStartResponseData.from_args(sesh_id, profile_data['nick_name'])) + resp.multi_play_start_response_data.append(QuestSceneMultiPlayStartResponseData.from_args()) return resp.make() - async def handle_c908(self, header: SaoRequestHeader, request: bytes) -> bytes: # Level calculation missing for the profile and heroes + async def handle_c908(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # Level calculation missing for the profile and heroes #quest/episode_play_end - req_data = SaoEpisodePlayEndRequest(header, request) + resp = SaoEpisodePlayEndResponse() - # Add stage progression to database user_id = req_data.user_id episode_id = req_data.episode_id - quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) - clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time - combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num - total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage - concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num + play_end = req_data.play_end_request_data[0] + base_get_data = play_end.base_get_data[0] + score_data = play_end.score_data[0] + exp = 0 + cleared_mission_ct = 0 + highest_mission_diff_cleared = 0 + num_monsters_defeated = 0 + monsters_defeated_data = {} + json_data = {"data": []} + + ep_data = await self.data.static.get_episode_by_id(req_data.episode_id) + quest_scene = await self.data.static.get_quest_by_id(int(ep_data['QuestSceneId'])) + reward_table = await self.data.static.get_rewards_by_subtable(int(quest_scene['RewardTableSubId'])) + ex_bonus_table = await self.data.static.get_ex_bonuses_by_subtable(int(ep_data['ExBonusTableSubId'])) - profile = await self.data.profile.get_profile(user_id) - vp = int(profile["own_vp"]) - exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) + await self.data.profile.add_col(user_id, base_get_data.get_col) + quest_clear_flag = score_data.clear_time > 0 + clear_time = score_data.clear_time + combo_num = score_data.combo_num + total_damage = score_data.total_damage + concurrent_destroying_num = score_data.concurrent_destroying_num + if quest_clear_flag is True: - # Save stage progression - to be revised to avoid saving worse score - - # Reference Episode.csv but Chapter 2,3,4 and 5 reports id -1, match using /10 + last digits - if episode_id > 10000 and episode_id < 11000: - # Starts at 1001 - episode_id = episode_id - 9000 - elif episode_id > 20000: - # Starts at 2001 - stage_id = str(episode_id)[-2:] - episode_id = episode_id / 10 - episode_id = int(episode_id) + int(stage_id) - - # Match episode_id with the questSceneId saved in the DB through sortNo - questId = await self.data.static.get_quests_id(episode_id) - episode_id = questId[2] - - await self.data.item.put_player_quest(user_id, episode_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) - - vp = int(profile["own_vp"]) + 10 #always 10 VP per cleared stage - - + await self.data.profile.add_vp(user_id, quest_scene['SingleRewardVp']) + await self.data.profile.add_exp(user_id, quest_scene['SuccessPlayerExp']) + exp = await self.data.profile.get_exp(user_id) + + else: + await self.data.profile.add_exp(user_id, quest_scene['FailedPlayerExp']) + exp = await self.data.profile.get_exp(user_id) + # 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) + player_level_data = self.load_data_csv("PlayerRank") - for i in range(0,len(data)): - if exp>=int(data[i][1]) and exp=int(player_level_data[i]["TotalExp"]) and exp highest_mission_diff_cleared: + highest_mission_diff_cleared = mission.mission_difficulty_id + + for monster_data in play_end.discovery_enemy_data_list: + num_monsters_defeated += monster_data.destroy_num + monsters_defeated_data[monster_data.enemy_kind_id] = monster_data.destroy_num + + for bonus in ex_bonus_table: + table_id = int(bonus['ExBonusTableId']) + ach_thing = 0 + condition = int(bonus['ExBonusConditionId']) + val1 = int(bonus['ConditionValue1']) + val2 = int(bonus['ConditionValue2']) + if condition == ExBonusCondition.CLEAR_UNDER_X_SECS: + if quest_clear_flag and int(score_data.clear_time / 1000) < val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.DEFEAT_X_MONSTER_Y_TIMES: + if monsters_defeated_data.get(val1, 0) >= val2: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.DEFEAT_X_MONSTERS: + if num_monsters_defeated >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_X_MISSIONS: + if cleared_mission_ct >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_MISSION_DIFFICULTY_X: + if highest_mission_diff_cleared >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.COLLECT_X_LOGS: + if len(play_end.get_unanalyzed_log_tmp_reward_data_list) >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_SKILL_LEVEL_X: + if score_data.reaching_skill_level >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.NO_LOSSES: + if score_data.total_loss_num == 0: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.ACCEL_X_TIMES: + if score_data.acceleration_invocation_num >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.MAX_COMBO_X: + if score_data.combo_num >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.MULTIPLAYER_CLEAR_X: + # TODO + pass + else: + self.logger.warn(f"Unhandled EX Bonus condition {condition}") + + resp.play_end_response_data[0].ex_bonus_data_list.append(QuestScenePlayEndExBonusData.from_args(table_id, ach_thing)) + + self.logger.info(f"User {user_id} {'cleared' if quest_clear_flag else 'ended'} episode {episode_id}") + + for rare_drop in play_end.get_rare_drop_data_list: + rewardList = self.load_data_csv("QuestRareDrop") + for drop in rewardList: + if int(drop['QuestRareDropId']) == rare_drop.quest_rare_drop_id: + await self.add_reward(drop, user_id) + break + + for unanalyzed_log in play_end.get_unanalyzed_log_tmp_reward_data_list: + able_rewards: List[Row] = [] + for reward in reward_table: + if int(reward['UnanalyzedLogGradeId']) == unanalyzed_log.unanalyzed_log_grade_id: + able_rewards.append(reward) + randomized_unanalyzed_id = choice(able_rewards) + + await self.add_reward(randomized_unanalyzed_id._asdict(), user_id) + json_data["data"].append(randomized_unanalyzed_id._asdict()) + + + trace_table = await self.data.static.get_player_trace_by_subid(quest_scene['PlayerTraceTableSubId']) + + for trace in play_end.get_player_trace_data_list: + self.logger.info(f"User {user_id} obtained trace {trace.user_quest_scene_player_trace_id}") + resp.play_end_response_data[0].play_end_player_trace_reward_data_list.append(QuestScenePlayEndPlayerTraceRewardData.from_args(choice(trace_table)._asdict())) + + await self.data.item.create_end_session(user_id, ep_data['QuestSceneId'], play_end.play_result_flag, json_data["data"]) # Update heroes from the used party play_session = await self.data.item.get_session(user_id) - session_party = await self.data.item.get_hero_party(user_id, play_session["user_party_team_id"]) + session_party = await self.data.item.get_hero_party_by_id(play_session["user_party_team_id"]) + if session_party: + hero_level_data = self.load_data_csv("HeroLogLevel") + hero_list = [] + hero_list.append(session_party["user_hero_log_id_1"]) + hero_list.append(session_party["user_hero_log_id_2"]) + hero_list.append(session_party["user_hero_log_id_3"]) - hero_list = [] - hero_list.append(session_party["user_hero_log_id_1"]) - hero_list.append(session_party["user_hero_log_id_2"]) - hero_list.append(session_party["user_hero_log_id_3"]) + for i in range(0,3): + await self.data.item.add_hero_xp(hero_list[i], base_get_data.get_hero_log_exp) + log_exp = await self.data.item.get_hero_xp(hero_list[i]) - for i in range(0,len(hero_list)): - hero_data = await self.data.item.get_hero_log(user_id, hero_list[i]) + # Calculate hero level based off experience and the CSV list + if log_exp: + for e in range(0,len(hero_level_data)): + if log_exp>=int(hero_level_data[e]["TotalExp"]) and log_exp=int(data[e][1]) and log_exp bytes: + async def handle_c90a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #quest/episode_play_end_unanalyzed_log_fixed + req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) + resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse() + + end_session_data = await self.data.item.get_end_session(req.user_id) + for data in end_session_data['reward_data']: + resp.play_end_unanalyzed_log_reward_data_list.append(QuestScenePlayEndUnanalyzedLogRewardData.from_args(data['UnanalyzedLogGradeId'], data)) + + return resp.make() + + async def handle_c914(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #quest/trial_tower_play_start req_data = SaoTrialTowerPlayStartRequest(header, request) user_id = req_data.user_id - floor_id = req_data.trial_tower_id profile_data = await self.data.profile.get_profile(user_id) - await self.data.item.create_session( + sesh_id = await self.data.item.create_session( user_id, int(req_data.play_start_request_data[0].user_party_id), req_data.trial_tower_id, @@ -625,374 +1404,1222 @@ class SaoBase: req_data.play_start_request_data[0].quest_drop_boost_apply_flag ) - resp = SaoEpisodePlayStartResponse(header.cmd +1, profile_data) + self.logger.info(f"User {req_data.user_id} started trial tower on floor {req_data.trial_tower_id}") + resp = SaoTrialTowerPlayStartResponse(sesh_id, profile_data['nick_name']) return resp.make() - async def handle_c918(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c918(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #quest/trial_tower_play_end req_data = SaoTrialTowerPlayEndRequest(header, request) + resp = SaoTrialTowerPlayEndResponse() - # Add tower progression to database user_id = req_data.user_id trial_tower_id = req_data.trial_tower_id - next_tower_id = 0 - quest_clear_flag = bool(req_data.play_end_request_data_list[0].score_data_list[0].boss_destroying_num) - clear_time = req_data.play_end_request_data_list[0].score_data_list[0].clear_time - combo_num = req_data.play_end_request_data_list[0].score_data_list[0].combo_num - total_damage = req_data.play_end_request_data_list[0].score_data_list[0].total_damage - concurrent_destroying_num = req_data.play_end_request_data_list[0].score_data_list[0].concurrent_destroying_num - - if quest_clear_flag is True: - # Save tower progression - to be revised to avoid saving worse score - if trial_tower_id == 9: - next_tower_id = 10001 - elif trial_tower_id == 10: - trial_tower_id = 10001 - next_tower_id = 3011 - elif trial_tower_id == 19: - next_tower_id = 10002 - elif trial_tower_id == 20: - trial_tower_id = 10002 - next_tower_id = 3021 - elif trial_tower_id == 29: - next_tower_id = 10003 - elif trial_tower_id == 30: - trial_tower_id = 10003 - next_tower_id = 3031 - elif trial_tower_id == 39: - next_tower_id = 10004 - elif trial_tower_id == 40: - trial_tower_id = 10004 - next_tower_id = 3041 - elif trial_tower_id == 49: - next_tower_id = 10005 - elif trial_tower_id == 50: - trial_tower_id = 10005 - next_tower_id = 3051 - else: - trial_tower_id = trial_tower_id + 3000 - next_tower_id = trial_tower_id + 1 - - await self.data.item.put_player_quest(user_id, trial_tower_id, quest_clear_flag, clear_time, combo_num, total_damage, concurrent_destroying_num) - - # Check if next stage is already done - checkQuest = await self.data.item.get_quest_log(user_id, next_tower_id) - if not checkQuest: - if next_tower_id != 3101: - await self.data.item.put_player_quest(user_id, next_tower_id, 0, 0, 0, 0, 0) - - # Update the profile - profile = await self.data.profile.get_profile(user_id) + play_end = req_data.play_end_request_data[0] + base_get_data = play_end.base_get_data[0] + score_data = play_end.score_data[0] + exp = 0 + cleared_mission_ct = 0 + highest_mission_diff_cleared = 0 + num_monsters_defeated = 0 + monsters_defeated_data = {} + json_data = {"data": []} - exp = int(profile["rank_exp"]) + 100 #always 100 extra exp for some reason - col = int(profile["own_col"]) + int(req_data.play_end_request_data_list[0].base_get_data_list[0].get_col) + ep_data = await self.data.static.get_tower_by_id(trial_tower_id) + quest_scene = await self.data.static.get_quest_by_id(int(ep_data['QuestSceneId'])) + reward_table = await self.data.static.get_rewards_by_subtable(int(quest_scene['RewardTableSubId'])) + ex_bonus_table = await self.data.static.get_ex_bonuses_by_subtable(int(ep_data['ExBonusTableSubId'])) + await self.data.profile.add_col(user_id, base_get_data.get_col) + + quest_clear_flag = score_data.clear_time > 0 + clear_time = score_data.clear_time + combo_num = score_data.combo_num + total_damage = score_data.total_damage + concurrent_destroying_num = score_data.concurrent_destroying_num + + if quest_clear_flag is True: + await self.data.profile.add_vp(user_id, quest_scene['SingleRewardVp']) + await self.data.profile.add_exp(user_id, quest_scene['SuccessPlayerExp']) + exp = await self.data.profile.get_exp(user_id) + + else: + await self.data.profile.add_exp(user_id, quest_scene['FailedPlayerExp']) + exp = await self.data.profile.get_exp(user_id) + # 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) + player_level_data = self.load_data_csv("PlayerRank") - for i in range(0,len(data)): - if exp>=int(data[i][1]) and exp=int(player_level_data[i]["TotalExp"]) and exp highest_mission_diff_cleared: + highest_mission_diff_cleared = mission.mission_difficulty_id + + for monster_data in play_end.discovery_enemy_data_list: + num_monsters_defeated += monster_data.destroy_num + monsters_defeated_data[monster_data.enemy_kind_id] = monster_data.destroy_num + + for bonus in ex_bonus_table: + table_id = int(bonus['ExBonusTableId']) + ach_thing = 0 + condition = int(bonus['ExBonusConditionId']) + val1 = int(bonus['ConditionValue1']) + val2 = int(bonus['ConditionValue2']) + if condition == ExBonusCondition.CLEAR_UNDER_X_SECS: + if quest_clear_flag and int(score_data.clear_time / 1000) < val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.DEFEAT_X_MONSTER_Y_TIMES: + if monsters_defeated_data.get(val1, 0) >= val2: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.DEFEAT_X_MONSTERS: + if num_monsters_defeated >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_X_MISSIONS: + if cleared_mission_ct >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_MISSION_DIFFICULTY_X: + if highest_mission_diff_cleared >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.COLLECT_X_LOGS: + if len(play_end.get_unanalyzed_log_tmp_reward_data_list) >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.CLEAR_SKILL_LEVEL_X: + if score_data.reaching_skill_level >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.NO_LOSSES: + if score_data.total_loss_num == 0: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.ACCEL_X_TIMES: + if score_data.acceleration_invocation_num >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.MAX_COMBO_X: + if score_data.combo_num >= val1: + await self.add_reward(bonus._asdict(), user_id) + await self.data.item.put_ex_bonus(user_id, int(ep_data['QuestSceneId']), table_id, True) + ach_thing = 2 + elif condition == ExBonusCondition.MULTIPLAYER_CLEAR_X: + # TODO + pass + else: + self.logger.warn(f"Unhandled EX Bonus condition {condition}") + + resp.play_end_response_data[0].ex_bonus_data_list.append(QuestScenePlayEndExBonusData.from_args(table_id, ach_thing)) + + self.logger.info(f"User {user_id} {'cleared' if quest_clear_flag else 'ended'} trial tower {trial_tower_id}") + + for rare_drop in play_end.get_rare_drop_data_list: + rewardList = self.load_data_csv("QuestRareDrop") + for drop in rewardList: + if int(drop['QuestRareDropId']) == rare_drop.quest_rare_drop_id: + await self.add_reward(drop, user_id) + break + + for unanalyzed_log in play_end.get_unanalyzed_log_tmp_reward_data_list: + able_rewards: List[Row] = [] + for reward in reward_table: + if int(reward['UnanalyzedLogGradeId']) == unanalyzed_log.unanalyzed_log_grade_id: + able_rewards.append(reward) + randomized_unanalyzed_id = choice(able_rewards) + + await self.add_reward(randomized_unanalyzed_id._asdict(), user_id) + json_data["data"].append(randomized_unanalyzed_id._asdict()) + + + trace_table = await self.data.static.get_player_trace_by_subid(quest_scene['PlayerTraceTableSubId']) + + for trace in play_end.get_player_trace_data_list: + self.logger.info(f"User {user_id} obtained trace {trace.user_quest_scene_player_trace_id}") + resp.play_end_response_data[0].play_end_player_trace_reward_data_list.append(QuestScenePlayEndPlayerTraceRewardData.from_args(choice(trace_table)._asdict())) + + await self.data.item.create_end_session(user_id, ep_data['QuestSceneId'], play_end.play_result_flag, json_data["data"]) # Update heroes from the used party play_session = await self.data.item.get_session(user_id) - session_party = await self.data.item.get_hero_party(user_id, play_session["user_party_team_id"]) + session_party = await self.data.item.get_hero_party_by_id(play_session["user_party_team_id"]) + if session_party: + hero_level_data = self.load_data_csv("HeroLogLevel") + hero_list = [] + hero_list.append(session_party["user_hero_log_id_1"]) + hero_list.append(session_party["user_hero_log_id_2"]) + hero_list.append(session_party["user_hero_log_id_3"]) - hero_list = [] - hero_list.append(session_party["user_hero_log_id_1"]) - hero_list.append(session_party["user_hero_log_id_2"]) - hero_list.append(session_party["user_hero_log_id_3"]) + for i in range(0,3): + self.logger.info(f"Give hero {hero_list[i]} {base_get_data.get_hero_log_exp}") + await self.data.item.add_hero_xp(hero_list[i], base_get_data.get_hero_log_exp) + log_exp = await self.data.item.get_hero_xp(hero_list[i]) - for i in range(0,len(hero_list)): - hero_data = await self.data.item.get_hero_log(user_id, hero_list[i]) + # Calculate hero level based off experience and the CSV list + if log_exp: + for e in range(0,len(hero_level_data)): + if log_exp>=int(hero_level_data[e]["TotalExp"]) and log_exp=int(data[e][1]) and log_exp bytes: - #quest/episode_play_end_unanalyzed_log_fixed - - req = SaoEpisodePlayEndUnanalyzedLogFixedRequest(header, request) - - end_session_data = await self.data.item.get_end_session(req.user_id) - - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) - return resp.make() - - async def handle_c91a(self, header: SaoRequestHeader, request: bytes) -> bytes: # handler is identical to the episode + async def handle_c91a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # handler is identical to the episode #quest/trial_tower_play_end_unanalyzed_log_fixed - req = TrialTowerPlayEndUnanalyzedLogFixed(header, request) - + req = SaoTrialTowerPlayEndUnanalyzedLogFixedRequest(header, request) + resp = SaoTrialTowerPlayEndUnanalyzedLogFixedResponse() + end_session_data = await self.data.item.get_end_session(req.user_id) + for data in end_session_data['reward_data']: + resp.play_end_unanalyzed_log_reward_data_list.append(QuestScenePlayEndUnanalyzedLogRewardData.from_args(data['UnanalyzedLogGradeId'], data)) - resp = SaoEpisodePlayEndUnanalyzedLogFixedResponse(header.cmd +1, end_session_data[4]) return resp.make() - async def handle_cd00(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_c930(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # quest/get_chat_side_story_user_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetChatSideStoryUserDataListResponse() + + return resp.make() + + async def handle_ca02(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + #quest_multi_play_room/get_quest_scene_multi_play_photon_server + resp = SaoGetQuestSceneMultiPlayPhotonServerResponse(self.game_cfg.server.photon_app_id) + return resp.make() + + async def handle_cb02(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # quest_ranking/get_quest_hierarchy_progress_degrees_ranking_list + req = SaoGetQuestHierarchyProgressDegreesRankingListRequest(header, request) + return SaoGetQuestHierarchyProgressDegreesRankingListResponse(GameconnectCmd.GET_QUEST_HIERARCHY_PROGRESS_DEGREES_RANKING_LIST_RESPONSE).make() + + async def handle_cb04(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # quest_ranking/get_quest_popular_hero_log_ranking_list + req = SaoGetQuestPopularHeroLogRankingListRequest(header, request) + return SaoGetQuestPopularHeroLogRankingListResponse(GameconnectCmd.GET_QUEST_POPULAR_HERO_LOG_RANKING_LIST_RESPONSE).make() + + async def handle_cd00(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #defrag_match/get_defrag_match_basic_data - resp = SaoGetDefragMatchBasicDataResponse(header.cmd +1) + resp = SaoGetDefragMatchBasicDataResponse() + data = DefragMatchBasicUserData.from_args() return resp.make() - async def handle_cd02(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd02(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #defrag_match/get_defrag_match_ranking_user_data - resp = SaoGetDefragMatchRankingUserDataResponse(header.cmd +1) + # TODO: League points + req = SaoGetDefragMatchRankingUserDataRequest(header, request) + profile = await self.data.profile.get_profile(req.user_id) + resp = SaoGetDefragMatchRankingUserDataResponse(profile._asdict()) return resp.make() - async def handle_cd04(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd04(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #defrag_match/get_defrag_match_league_point_ranking_list - resp = SaoGetDefragMatchLeaguePointRankingListResponse(header.cmd +1) + resp = SaoGetDefragMatchLeaguePointRankingListResponse() return resp.make() - async def handle_cd06(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cd06(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: #defrag_match/get_defrag_match_league_score_ranking_list - resp = SaoGetDefragMatchLeagueScoreRankingListResponse(header.cmd +1) + resp = SaoGetDefragMatchLeagueScoreRankingListResponse() return resp.make() - async def handle_d404(self, header: SaoRequestHeader, request: bytes) -> bytes: - #other/bnid_serial_code_check - resp = SaoBnidSerialCodeCheckResponse(header.cmd +1) - return resp.make() - - async def handle_c306(self, header: SaoRequestHeader, request: bytes) -> bytes: - #card/scan_qr_quest_profile_card - resp = SaoScanQrQuestProfileCardResponse(header.cmd +1) - return resp.make() - - async def handle_c700(self, header: SaoRequestHeader, request: bytes) -> bytes: - # shop/get_shop_resource_sales_data_list - # TODO: Get user shop data - req = GetShopResourceSalesDataListRequest(header, request) - resp = GetShopResourceSalesDataListResponse(header.cmd + 1) - return resp.make() - - async def handle_d100(self, header: SaoRequestHeader, request: bytes) -> bytes: - # shop/get_yui_medal_shop_user_data_list - # TODO: Get user shop data - req = GetYuiMedalShopUserDataListRequest(header, request) - resp = GetYuiMedalShopUserDataListResponse(header.cmd + 1) - return resp.make() - - async def handle_cf0e(self, header: SaoRequestHeader, request: bytes) -> bytes: + async def handle_cf0e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # gasha/get_gasha_medal_shop_user_data_list # TODO: Get user shop data req = GetGashaMedalShopUserDataListRequest(header, request) - resp = GetGashaMedalShopUserDataListResponse(header.cmd + 1) + resp = GetGashaMedalShopUserDataListResponse(GameconnectCmd.GET_GASHA_MEDAL_SHOP_USER_DATA_LIST_RESPONSE) return resp.make() - - async def handle_d5da(self, header: SaoRequestHeader, request: bytes) -> bytes: + + async def handle_d000(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + req = SaoGenericUserRequest(header, request) + resp = SaoGetAdventureExecUserDataResponse() + return resp.make() + + async def handle_d100(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # shop/get_yui_medal_shop_user_data_list + # TODO: Get user shop data + req = GetYuiMedalShopUserDataListRequest(header, request) + resp = GetYuiMedalShopUserDataListResponse(GameconnectCmd.GET_YUI_MEDAL_SHOP_USER_DATA_LIST_RESPONSE) + return resp.make() + + async def handle_d200(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # mission/get_beginner_mission_user_data + req = SaoGetBeginnerMissionUserDataRequest(header, request) + resp = SaoGetBeginnerMissionUserDataResponse() + profile = await self.data.profile.get_profile(req.user_id) + if profile: + if profile['ad_confirm_date']: + resp.data[0].ad_confirm_date = profile['ad_confirm_date'] + resp.data[0].ad_confirm_flag = 1 + + return resp.make() + + async def handle_d202(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # mission/get_beginner_mission_progresses_user_data_list + req = SaoGetBeginnerMissionProgressesUserDataListRequest(header, request) + resp = SaoGetBeginnerMissionProgressesUserDataListResponse() + + return resp.make() + + async def handle_d204(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # mission/get_beginner_mission_seat_progresses_user_data_list + req = SaoGetBeginnerMissionSeatProgressesUserDataListRequest(header, request) + resp = SaoGetBeginnerMissionSeatProgressesUserDataListResponse() + + return resp.make() + + async def handle_d206(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # mission/beginner_mission_ad_confirm_notification + req = SaoBeginnerMissionAdConfirmNotificationRequest(header, request) + self.logger.info(f"User {req.user_id} confirmed ad for beginner mission {req.beginner_mission_id}") + await self.data.profile.update_beginner_mission_date(req.user_id) + + return SaoNoopResponse(GameconnectCmd.BEGINNER_MISSION_AD_CONFIRM_NOTIFICATION_RESPONSE).make() + + async def handle_d312(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # campaign/get_linked_site_reg_campaign_user_data + req = SaoGetLinkedSiteRegCampaignUserDataRequest(header, request) + resp = SaoGetLinkedSiteRegCampaignUserDataResponse() + + return resp.make() + + async def handle_d400(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # other/get_hero_log_unit_user_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetHeroLogUnitUserDataListResponse() + + return resp.make() + + async def handle_d402(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # other/get_chara_unit_user_data_list + req = SaoGenericUserRequest(header, request) + resp = SaoGetCharaUnitUserDataListResponse() + + return resp.make() + + async def handle_d404(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # other/bnid_serial_code_check + req = SaoBnidSerialCodeCheckRequest() + resp = SaoBnidSerialCodeCheckResponse() + return resp.make() + + async def handle_d404(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # other/bnid_serial_code_entry_by_appendix_card + req = SaoBnidSerialCodeEntryByAppendixCardRequest() + resp = SaoBnidSerialCodeEntryByAppendixCardResponse() + return resp.make() + + async def handle_d500(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_player_ranks + tbl = self.load_data_csv('PlayerRank') + resp = SaoGetMPlayerRanksResponse(tbl) + return resp.make() + + async def handle_d502(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_titles + tbl = self.load_data_csv('Title') + resp = SaoGetMTitlesResponse(tbl) + return resp.make() + + async def handle_d504(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_fragments + tbl = self.load_data_csv('Fragment') + resp = SaoGetMFragmentsResponse(tbl) + return resp.make() + + async def handle_d506(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_reward_tables + tbl = self.load_data_csv('RewardTable') + resp = SaoGetMRewardTablesResponse(tbl) + return resp.make() + + async def handle_d508(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_reward_sets + tbl = self.load_data_csv('RewardSet') + resp = SaoGetMRewardSetsResponse(tbl) + return resp.make() + + async def handle_d50a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_unanalyzed_log_grades + tbl = self.load_data_csv('UnanalyzedLogGrade') + resp = SaoGetMUnanalyzedLogGradesResponse(tbl) + return resp.make() + + async def handle_d50c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_appoint_leader_params + tbl = self.load_data_csv('AppointLeaderParam') + resp = SaoGetMAppointLeaderParamsResponse(tbl) + return resp.make() + + async def handle_d50e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_appoint_leader_effects + tbl = self.load_data_csv('AppointLeaderEffect') + resp = SaoGetMAppointLeaderEffectsResponse(tbl) + return resp.make() + + async def handle_d510(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_appoint_leader_effect_types + tbl = self.load_data_csv('AppointLeaderEffectType') + resp = SaoGetMAppointLeaderEffectTypesResponse(tbl) + return resp.make() + + async def handle_d512(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_rarities + tbl = self.load_data_csv('Rarity') + resp = SaoGetMRaritiesResponse(tbl) + return resp.make() + + async def handle_d514(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_composition_events + tbl = self.load_data_csv('CompositionEvent') + resp = SaoGetMCompositionEventsResponse(tbl) + return resp.make() + + async def handle_d516(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_composition_params + tbl = self.load_data_csv('CompositionParam') + resp = SaoGetMCompositionParamsResponse(tbl) + return resp.make() + + async def handle_d518(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_game_play_prices + tbl = self.load_data_csv('GamePlayPrice') + resp = SaoGetMGamePlayPricesResponse(tbl) + return resp.make() + + async def handle_d51a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_buy_tickets + tbl = self.load_data_csv('BuyTicket') + resp = SaoGetMBuyTicketsResponse(tbl) + return resp.make() + + async def handle_d51c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_tips + tbl = self.load_data_csv('Tips') + resp = SaoGetMTipsResponse(tbl) + return resp.make() + + async def handle_d51e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_caps + tbl = self.load_data_csv('Cap') + resp = SaoGetMCapsResponse(tbl) + return resp.make() + + async def handle_d520(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_hero_log + tbl = self.load_data_csv('HeroLog') + resp = SaoGetMHeroLogResponse(tbl) + return resp.make() + + async def handle_d522(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_hero_log_levels + tbl = self.load_data_csv('HeroLogLevel') + resp = SaoGetMHeroLogLevelsResponse(tbl) + return resp.make() + + async def handle_d524(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_hero_log_roles + tbl = self.load_data_csv('HeroLogRole') + resp = SaoGetMHeroLogRolesResponse(tbl) + return resp.make() + + async def handle_d526(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_hero_log_trust_ranks + tbl = self.load_data_csv('HeroLogTrustRank') + resp = SaoGetMHeroLogTrustRanksResponse(tbl) + return resp.make() + + async def handle_d528(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_charas + tbl = self.load_data_csv('Chara') + resp = SaoGetMCharasResponse(tbl) + return resp.make() + + async def handle_d52a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_chara_friendly_ranks + tbl = self.load_data_csv('CharaFriendlyRank') + resp = SaoGetMCharaFriendlyRanksResponse(tbl) + return resp.make() + + async def handle_d52c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_equipments + tbl = self.load_data_csv('Equipment') + resp = SaoGetMEquipmentsResponse(tbl) + return resp.make() + + async def handle_d52e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_equipment_levels + tbl = self.load_data_csv('EquipmentLevel') + resp = SaoGetMEquipmentLevelsResponse(tbl) + return resp.make() + + async def handle_d530(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_weapon_types + tbl = self.load_data_csv('WeaponType') + resp = SaoGetMWeaponTypesResponse(tbl) + return resp.make() + + async def handle_d532(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_items + tbl = self.load_data_csv('Item') + resp = SaoGetMItemsResponse(tbl) + return resp.make() + + async def handle_d534(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_item_types + tbl = self.load_data_csv('ItemType') + resp = SaoGetMItemTypesResponse(tbl) + return resp.make() + + async def handle_d536(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_buff_items + tbl = self.load_data_csv('BuffItem') + resp = SaoGetMBuffItemsResponse(tbl) + return resp.make() + + async def handle_d538(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_enemies + tbl = self.load_data_csv('Enemy') + resp = SaoGetMEnemiesResponse(tbl) + return resp.make() + + async def handle_d53a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_enemy_sets + tbl = self.load_data_csv('EnemySet') + resp = SaoGetMEnemySetsResponse(tbl) + return resp.make() + + async def handle_d53c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_enemy_kinds + tbl = self.load_data_csv('EnemyKind') + resp = SaoGetMEnemyKindsResponse(tbl) + return resp.make() + + async def handle_d53e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_enemy_categories + tbl = self.load_data_csv('EnemyCategory') + resp = SaoGetMEnemyCategoriesResponse(tbl) + return resp.make() + + async def handle_d540(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_units + tbl = self.load_data_csv('Unit') + resp = SaoGetMUnitsResponse(tbl) + return resp.make() + + async def handle_d542(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_unit_gimmicks + tbl = self.load_data_csv('UnitGimmick') + resp = SaoGetMUnitGimmicksResponse(tbl) + return resp.make() + + async def handle_d544(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_unit_collisions + tbl = self.load_data_csv('UnitCollision') + resp = SaoGetMUnitCollisionsResponse(tbl) + return resp.make() + + async def handle_d546(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_unit_powers + tbl = self.load_data_csv('UnitPower') + resp = SaoGetMUnitPowersResponse(tbl) + return resp.make() + + async def handle_d548(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gimmick_attacks + tbl = self.load_data_csv('GimmickAttack') + resp = SaoGetMGimmickAttacksResponse(tbl) + return resp.make() + + async def handle_d54a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_chara_attacks + tbl = self.load_data_csv('CharaAttack') + resp = SaoGetMCharaAttacksResponse(tbl) + return resp.make() + + async def handle_d54c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_boss_attacks + tbl = self.load_data_csv('BossAttack') + resp = SaoGetMBossAttacksResponse(tbl) + return resp.make() + + async def handle_d54e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_monster_attacks + tbl = self.load_data_csv('MonsterAttack') + resp = SaoGetMMonsterAttacksResponse(tbl) + return resp.make() + + async def handle_d550(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_monster_actions + tbl = self.load_data_csv('MonsterAction') + resp = SaoGetMMonsterActionsResponse(tbl) + return resp.make() + + async def handle_d552(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_properties + tbl = self.load_data_csv('Property') + resp = SaoGetMPropertiesResponse(tbl) + return resp.make() + + async def handle_d554(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_property_tables + tbl = self.load_data_csv('PropertyTable') + resp = SaoGetMPropertyTablesResponse(tbl) + return resp.make() + + async def handle_d556(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_property_types + tbl = self.load_data_csv('PropertyType') + resp = SaoGetMPropertyTypesResponse(tbl) + return resp.make() + + async def handle_d558(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_skills + tbl = self.load_data_csv('Skill') + resp = SaoGetMSkillsResponse(tbl) + return resp.make() + + async def handle_d55a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_skill_tables + tbl = self.load_data_csv('SkillTable') + resp = SaoGetMSkillTablesResponse(tbl) + return resp.make() + + async def handle_d55c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_skill_levels + tbl = self.load_data_csv('SkillLevel') + resp = SaoGetMSkillLevelsResponse(tbl) + return resp.make() + + async def handle_d55e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_awakenings + tbl = self.load_data_csv('Awakening') + resp = SaoGetMAwakeningsResponse(tbl) + return resp.make() + + async def handle_d560(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_synchro_skills + tbl = self.load_data_csv('SynchroSkill') + resp = SaoGetMSynchroSkillsResponse(tbl) + return resp.make() + + async def handle_d562(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_sound_skill_cut_in_voices + tbl = self.load_data_csv('Sound_SkillCutInVoice') + resp = SaoGetMSoundSkillCutInVoicesResponse(tbl) + return resp.make() + + async def handle_d564(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_scenes + tbl = self.load_data_csv('QuestScene') + resp = SaoGetMQuestScenesResponse(tbl) + return resp.make() + + async def handle_d566(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_exist_units + tbl = self.load_data_csv('QuestExistUnit') + resp = SaoGetMQuestExistUnitsResponse(tbl) + return resp.make() + + async def handle_d568(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_episode_append_rewards + tbl = self.load_data_csv('QuestEpisodeAppendRewards') + resp = SaoGetMQuestEpisodeAppendRewardsResponse(tbl) + return resp.make() + + async def handle_d56a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_side_quests + tbl = self.load_data_csv('SideQuest') + resp = SaoGetMSideQuestsResponse(tbl) + return resp.make() + + async def handle_d56c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_episodes + tbl = self.load_data_csv('Episode') + resp = SaoGetMEpisodesResponse(tbl) + return resp.make() + + async def handle_d56e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_episode_chapters + tbl = self.load_data_csv('EpisodeChapter') + resp = SaoGetMEpisodeChaptersResponse(tbl) + return resp.make() + + async def handle_d570(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_episode_parts + tbl = self.load_data_csv('EpisodePart') + resp = SaoGetMEpisodePartsResponse(tbl) + return resp.make() + + async def handle_d572(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_trial_towers + tbl = self.load_data_csv('TrialTower') + resp = SaoGetMTrialTowersResponse(tbl) + return resp.make() + + async def handle_d574(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_ex_towers + req = SaoGetMExTowersRequest(header, request) + tbl = self.load_data_csv('ExTowers') + resp = SaoGetMExTowersResponse(tbl) + return resp.make() + + async def handle_d576(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_ex_tower_quests + req = SaoGetMExTowerQuestsRequest(header, request) + tbl = self.load_data_csv('ExTowerQuests') + resp = SaoGetMExTowerQuestsResponse(tbl) + return resp.make() + + async def handle_d578(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_menu_display_enemies + tbl = self.load_data_csv('MenuDisplayEnemy') + resp = SaoGetMMenuDisplayEnemiesResponse(tbl) + return resp.make() + + async def handle_d57a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_missions + tbl = self.load_data_csv('Mission') + resp = SaoGetMMissionsResponse(tbl) + return resp.make() + + async def handle_d57c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_mission_tables + tbl = self.load_data_csv('MissionTable') + resp = SaoGetMMissionTablesResponse(tbl) + return resp.make() + + async def handle_d57e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_mission_difficulties + tbl = self.load_data_csv('MissionDifficulty') + resp = SaoGetMMissionDifficultiesResponse(tbl) + return resp.make() + + async def handle_d580(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_battle_cameras + tbl = self.load_data_csv('BattleCamera') + resp = SaoGetMBattleCamerasResponse(tbl) + return resp.make() + + async def handle_d582(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_chat_main_stories + tbl = self.load_data_csv('ChatMainStory') + resp = SaoGetMChatMainStoriesResponse(tbl) + return resp.make() + + async def handle_d584(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_chat_side_stories + tbl = self.load_data_csv('ChatSideStory') + resp = SaoGetMChatSideStoriesResponse(tbl) + return resp.make() + + async def handle_d586(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_chat_event_stories + req = SaoGetMChatEventStoriesRequest(header, request) + tbl = self.load_data_csv('ChatEventStory') + resp = SaoGetMChatEventStoriesResponse(tbl) + return resp.make() + + async def handle_d588(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_navigator_charas + tbl = self.load_data_csv('NavigatorChara') + resp = SaoGetMNavigatorCharasResponse(tbl) + return resp.make() + + async def handle_d58a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_navigator_comments + tbl = self.load_data_csv('NavigatorComment') + resp = SaoGetMNavigatorCommentsResponse(tbl) + return resp.make() + + async def handle_d58c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_ex_bonus_tables + tbl = self.load_data_csv('ExBonusTable') + resp = SaoGetMExBonusTablesResponse(tbl) + return resp.make() + + async def handle_d58e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_ex_bonus_conditions + tbl = self.load_data_csv('ExBonusCondition') + resp = SaoGetMExBonusConditionsResponse(tbl) + return resp.make() + + async def handle_d590(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_rare_drops + tbl = self.load_data_csv('QuestRareDrop') + resp = SaoGetMQuestRareDropsResponse(tbl) + return resp.make() + + async def handle_d592(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_special_rare_drop_settings + tbl = self.load_data_csv('QuestSpecialRareDropSettings') + resp = SaoGetMQuestSpecialRareDropSettingsResponse(tbl) + return resp.make() + + async def handle_d594(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_special_rare_drops + tbl = self.load_data_csv('QuestSpecialRareDrops') + resp = SaoGetMQuestSpecialRareDropsResponse(tbl) + return resp.make() + + async def handle_d596(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_tutorials + tbl = self.load_data_csv('QuestTutorial') + resp = SaoGetMQuestTutorialsResponse(tbl) + return resp.make() + + async def handle_d598(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_player_trace_tables + tbl = self.load_data_csv('PlayerTraceTable') + resp = SaoGetMQuestPlayerTraceTablesResponse(tbl) + return resp.make() + + async def handle_d59a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_stills + tbl = self.load_data_csv('QuestStill') + resp = SaoGetMQuestStillsResponse(tbl) + return resp.make() + + async def handle_d59c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gashas + tbl = self.load_data_csv('Gasha') + resp = SaoGetMGashasResponse(tbl) + return resp.make() + + async def handle_d59e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_headers + tbl = self.load_data_csv('GashaHeader') + resp = SaoGetMGashaHeadersResponse(tbl) + return resp.make() + + async def handle_d5a0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_lottery_rarities + tbl = self.load_data_csv('GashaLotteryRarity') + resp = SaoGetMGashaLotteryRaritiesResponse(tbl) + return resp.make() + + async def handle_d5a2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_prizes + tbl = self.load_data_csv('GashaPrize') + resp = SaoGetMGashaPrizesResponse(tbl) + return resp.make() + + async def handle_d5a4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_comeback_events + tbl = self.load_data_csv('ComebackEvent') + resp = SaoGetMComebackEventsResponse(tbl) + return resp.make() + + async def handle_d5a6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_ad_banners + tbl = self.load_data_csv('AdBanners') + resp = SaoGetMAdBannersResponse(tbl) + return resp.make() + + async def handle_d5a8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_events + tbl = self.load_data_csv('Event') + resp = SaoGetMEventsResponse(tbl) + return resp.make() + + async def handle_d5aa(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunts + req = SaoGetMTreasureHuntsRequest(header, request) + tbl = self.load_data_csv('TreasureHunt') + resp = SaoGetMTreasureHuntsResponse(tbl) + return resp.make() + + async def handle_d5ac(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_whole_tasks + req = SaoGetMTreasureHuntWholeTasksRequest(header, request) + tbl = self.load_data_csv('TreasureHuntWholeTask') + resp = SaoGetMTreasureHuntWholeTasksResponse(tbl) + return resp.make() + + async def handle_d5ae(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_individual_tasks + req = SaoGetMTreasureHuntIndividualTasksRequest(header, request) + tbl = self.load_data_csv('TreasureHuntIndividualTask') + resp = SaoGetMTreasureHuntIndividualTasksResponse(tbl) + return resp.make() + + async def handle_d5b0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_special_effects + req = SaoGetMTreasureHuntSpecialEffectsRequest(header, request) + tbl = self.load_data_csv('TreasureHuntSpecialEffect') + resp = SaoGetMTreasureHuntSpecialEffectsResponse(tbl) + return resp.make() + + async def handle_d5b2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_event_point_reward_common_rewards + req = SaoGetMTreasureHuntEventPointRewardCommonRewardsRequest(header, request) + tbl = self.load_data_csv('TreasureHuntEventPointRewardCommonReward') + resp = SaoGetMTreasureHuntEventPointRewardCommonRewardsResponse(tbl) + return resp.make() + + async def handle_d5b4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_event_point_reward_titles + req = SaoGetMTreasureHuntEventPointRewardTitlesRequest(header, request) + tbl = self.load_data_csv('TreasureHuntEventPointRewardTitle') + resp = SaoGetMTreasureHuntEventPointRewardTitlesResponse(tbl) + return resp.make() + + async def handle_d5b6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_treasure_hunt_task_texts + tbl = self.load_data_csv('TreasureHuntTaskText') + resp = SaoGetMTreasureHuntTaskTextsResponse(tbl) + return resp.make() + + async def handle_d5b8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_bnid_serial_codes + tbl = self.load_data_csv('BnidSerialCodes') + resp = SaoGetMBnidSerialCodesResponse(tbl) + return resp.make() + + async def handle_d5ba(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_bnid_serial_code_rewards + tbl = self.load_data_csv('BnidSerialCodeRewards') + resp = SaoGetMBnidSerialCodeRewardsResponse(tbl) + return resp.make() + + async def handle_d5bc(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_support_log + tbl = self.load_data_csv('SupportLog') + resp = SaoGetMSupportLogResponse(tbl) + return resp.make() + + async def handle_d5be(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_support_log_types + tbl = self.load_data_csv('SupportLogType') + resp = SaoGetMSupportLogTypesResponse(tbl) + return resp.make() + + async def handle_d5c0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_episode_appends + tbl = self.load_data_csv('EpisodeAppends') + resp = SaoGetMEpisodeAppendsResponse(tbl) + return resp.make() + + async def handle_d5c2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_defrag_match_quests + tbl = self.load_data_csv('DefragMatchQuest') + resp = SaoGetMQuestDefragMatchQuestsResponse(tbl) + return resp.make() + + async def handle_d5c4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_quest_defrag_match_quest_boss_tables + tbl = self.load_data_csv('DefragMatchBossTable') + resp = SaoGetMQuestDefragMatchQuestBossTablesResponse(tbl) + return resp.make() + + async def handle_d5c6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_matches + req = SaoGetMDefragMatchesRequest(header, request) + tbl = self.load_data_csv('DefragMatchs') + resp = SaoGetMDefragMatchesResponse(tbl) + return resp.make() + + async def handle_d5c8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_seed + req = SaoGetMDefragMatchSeedRequest(header, request) + tbl = self.load_data_csv('DefragMatchSeed') + resp = SaoGetMDefragMatchSeedResponse(tbl) + return resp.make() + + async def handle_d5ca(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_special_effects + req = SaoGetMDefragMatchSpecialEffectsRequest(header, request) + tbl = self.load_data_csv('DefragMatchSpecialEffects') + resp = SaoGetMDefragMatchSpecialEffectsResponse(tbl) + return resp.make() + + async def handle_d5cc(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_grades + req = SaoGetMDefragMatchGradesRequest(header, request) + tbl = self.load_data_csv('DefragMatchGrade') + resp = SaoGetMDefragMatchGradesResponse(tbl) + return resp.make() + + async def handle_d5ce(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_cpu_units + tbl = self.load_data_csv('DefragMatchCpuUnits') + resp = SaoGetMDefragMatchCpuUnitsResponse(tbl) + return resp.make() + + async def handle_d5d0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_cpu_support_logs + tbl = self.load_data_csv('DefragMatchCpuSupportLogs') + resp = SaoGetMDefragMatchCpuSupportLogsResponse(tbl) + return resp.make() + + async def handle_d5d2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_period_bonuses + req = SaoGetMDefragMatchPeriodBonusesRequest(header, request) + tbl = self.load_data_csv('DefragMatchPeriodBonuses') + resp = SaoGetMDefragMatchPeriodBonusesResponse(tbl) + return resp.make() + + async def handle_d5d4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_random_bonus_tables + req = SaoGetMDefragMatchRandomBonusTablesRequest(header, request) + tbl = self.load_data_csv('DefragMatchRandomBonusTables') + resp = SaoGetMDefragMatchRandomBonusTablesResponse(tbl) + return resp.make() + + async def handle_d5d6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_random_bonus_conditions + tbl = self.load_data_csv('DefragMatchRandomBonusConditions') + resp = SaoGetMDefragMatchRandomBonusConditionsResponse(tbl) + return resp.make() + + async def handle_d5d8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_defrag_match_rare_drops + req = SaoGetMDefragMatchRareDropsRequest(header, request) + tbl = self.load_data_csv('DefragMatchRareDrops') + resp = SaoGetMDefragMatchRareDropsResponse(tbl) + return resp.make() + + async def handle_d5da(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # master_data/get_m_yui_medal_shops - req = GetMYuiMedalShopDataRequest(header, request) - resp = GetMYuiMedalShopDataResponse(header.cmd + 1) - - shops = self.load_data_csv("YuiMedalShops") - for shop in shops: - tmp = YuiMedalShopData.from_args(int(shop['YuiMedalShopId']), shop['Name'], shop['Description']) - tmp.selling_yui_medal = int(shop['SellingYuiMedal']) - tmp.selling_col = int(shop['SellingCol']) - tmp.selling_event_item_id = int(shop['SellingEventItemId']) - tmp.selling_event_item_num = int(shop['SellingEventItemNum']) - tmp.selling_ticket_num = int(shop['SellingTicketNum']) - tmp.purchase_limit = int(shop['PurchaseLimit']) - tmp.pick_up_flag = 1 if shop['PickUpFlag'] == "True" else 0 - tmp.product_category = int(shop['ProductCategory']) - tmp.sales_type = int(shop['SalesType']) - tmp.target_days = int(shop['TargetDays']) - tmp.target_hour = int(shop['TargetHour']) - tmp.interval_hour = int(shop['IntervalHour']) - tmp.sort = int(shop['Sort']) - - tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open - - resp.data_list.append(tmp) - self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shops") + tbl = self.load_data_csv('YuiMedalShops') + resp = SaoGetMYuiMedalShopsResponse(tbl) return resp.make() - - async def handle_d5dc(self, header: SaoRequestHeader, request: bytes) -> bytes: + + async def handle_d5dc(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # master_data/get_m_yui_medal_shop_items - req = GetMYuiMedalShopItemsRequest(header, request) - resp = GetMYuiMedalShopItemsResponse(header.cmd + 1) - - shops = self.load_data_csv("YuiMedalShopItems") - for shop in shops: - tmp = YuiMedalShopItemData.from_args(int(shop['YuiMedalShopItemId']), int(shop['YuiMedalShopId']), int(shop['CommonRewardType']), int(shop['CommonRewardId']), int(shop['CommonRewardNum']), int(shop['Strength'])) - - tmp.property1_property_id = int(shop['Property1PropertyId']) - tmp.property1_value1 = int(shop['Property1Value1']) - tmp.property1_value2 = int(shop['Property1Value2']) - - tmp.property2_property_id = int(shop['Property2PropertyId']) - tmp.property2_value1 = int(shop['Property2Value1']) - tmp.property2_value2 = int(shop['Property2Value2']) - - tmp.property3_property_id = int(shop['Property3PropertyId']) - tmp.property3_value1 = int(shop['Property3Value1']) - tmp.property3_value2 = int(shop['Property3Value2']) - - tmp.property4_property_id = int(shop['Property4PropertyId']) - tmp.property4_value1 = int(shop['Property4Value1']) - tmp.property4_value2 = int(shop['Property4Value2']) - - resp.data_list.append(tmp) - - self.logger.debug(f"Load {len(resp.data_list)} Yui Medal Shop Items") + tbl = self.load_data_csv('YuiMedalShopItems') + resp = SaoGetMYuiMedalShopItemsResponse(tbl) return resp.make() - - async def handle_d5fc(self, header: SaoRequestHeader, request: bytes) -> bytes: + + async def handle_d5de(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_event_scenes + req = SaoGetMEventScenesRequest(header, request) + tbl = self.load_data_csv('EventScenes') + resp = SaoGetMEventScenesResponse(tbl) + return resp.make() + + async def handle_d5e0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_generic_campaign_periods + tbl = self.load_data_csv('GenericCampaignPeriods') + resp = SaoGetMGenericCampaignPeriodsResponse(tbl) + return resp.make() + + async def handle_d5e2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_beginner_missions + tbl = self.load_data_csv('BeginnerMissions') + resp = SaoGetMBeginnerMissionsResponse(tbl) + return resp.make() + + async def handle_d5e4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_beginner_mission_conditions + req = SaoGetMBeginnerMissionConditionsRequest(header, request) + tbl = self.load_data_csv('BeginnerMissionConditions') + resp = SaoGetMBeginnerMissionConditionsResponse(tbl) + return resp.make() + + async def handle_d5e6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_beginner_mission_rewards + req = SaoGetMBeginnerMissionRewardsRequest(header, request) + tbl = self.load_data_csv('BeginnerMissionRewards') + resp = SaoGetMBeginnerMissionRewardsResponse(tbl) + return resp.make() + + async def handle_d5e8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_beginner_mission_seat_conditions + req = SaoGetMBeginnerMissionSeatConditionsRequest(header, request) + tbl = self.load_data_csv('BeginnerMissionSeatConditions') + resp = SaoGetMBeginnerMissionSeatConditionsResponse(tbl) + return resp.make() + + async def handle_d5ea(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_beginner_mission_seat_rewards + req = SaoGetMBeginnerMissionSeatRewardsRequest(header, request) + tbl = self.load_data_csv('BeginnerMissionSeatRewards') + resp = SaoGetMBeginnerMissionSeatRewardsResponse(tbl) + return resp.make() + + async def handle_d5ec(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_event_items + tbl = self.load_data_csv('EventItems') + resp = SaoGetMEventItemsResponse(tbl) + return resp.make() + + async def handle_d5ee(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_event_monsters + req = SaoGetMEventMonstersRequest(header, request) + tbl = self.load_data_csv('EventMonsters') + resp = SaoGetMEventMonstersResponse(tbl) + return resp.make() + + async def handle_d5f0(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_yui_medal_bonuses + tbl = self.load_data_csv('YuiMedalBonus') + resp = SaoGetMYuiMedalBonusesResponse(tbl) + return resp.make() + + async def handle_d5f2(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_yui_medal_bonus_conditions + tbl = self.load_data_csv('YuiMedalBonusCondition') + resp = SaoGetMYuiMedalBonusConditionsResponse(tbl) + return resp.make() + + async def handle_d5f4(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_medals + tbl = self.load_data_csv('GashaMedals') + resp = SaoGetMGashaMedalsResponse(tbl) + return resp.make() + + async def handle_d5f6(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_medal_types + tbl = self.load_data_csv('GashaMedalTypes') + resp = SaoGetMGashaMedalTypesResponse(tbl) + return resp.make() + + async def handle_d5f8(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_medal_settings + req = SaoGetMGashaMedalSettingsRequest(header, request) + tbl = self.load_data_csv('GashaMedalSettings') + resp = SaoGetMGashaMedalSettingsResponse(tbl) + return resp.make() + + async def handle_d5fa(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_medal_bonuses + tbl = self.load_data_csv('GashaMedalBonuses') + resp = SaoGetMGashaMedalBonusesResponse(tbl) + return resp.make() + + async def handle_d5fc(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: # master_data/get_m_gasha_medal_shops - req = GetMGashaMedalShopsRequest(header, request) - resp = GetMGashaMedalShopsResponse(header.cmd + 1) - - shops = self.load_data_csv("GashaMedalShops") - for shop in shops: - tmp = GashaMedalShop.from_args(int(shop['GashaMedalShopId']), shop['Name'], int(shop['GashaMedalId']), int(shop['UseGashaMedalNum']), int(shop['PurchaseLimit'])) - tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open - - resp.data_list.append(tmp) - - self.logger.debug(f"Load {len(resp.data_list)} Gasha Medal Shops") + tbl = self.load_data_csv('GashaMedalShops') + resp = SaoGetMGashaMedalShopsResponse(tbl) return resp.make() - - async def handle_d5fe(self, header: SaoRequestHeader, request: bytes) -> bytes: - # master_data/get_m_gasha_medal_shop_items - return SaoNoopResponse(header.cmd + 1).make() - - async def handle_d604(self, header: SaoRequestHeader, request: bytes) -> bytes: - # master_data_2/get_m_res_earn_campaign_shops - req = GetMResEarnCampaignShopsRequest(header, request) - resp = GetMResEarnCampaignShopsResponse(header.cmd + 1) - - shops = self.load_data_csv("ResEarnCampaignShops") - for shop in shops: - tmp = ResEarnCampaignShop.from_args(int(shop['ResEarnCampaignShopId']), int(shop['ResEarnCampaignApplicationId']), shop['Name']) - tmp.selling_yui_medal = int(shop['SellingYuiMedal']) - tmp.selling_col = int(shop['SellingCol']) - tmp.selling_event_item_id = int(shop['SellingEventItemId']) - tmp.selling_event_item_num = int(shop['SellingEventItemNum']) - tmp.purchase_limit = int(shop['PurchaseLimit']) - tmp.get_application_point = int(shop['GetApplicationPoint']) - - tmp.sales_end_date = datetime(2121, 1, 1, 0, 0, 0, 0) # always open - - resp.data_list.append(tmp) - #self.logger.debug(f"Load {len(resp.data_list)} Res Earn Campaign Shops") - return SaoNoopResponse(header.cmd + 1).make() - - async def handle_d606(self, header: SaoRequestHeader, request: bytes) -> bytes: - # master_data_2/get_m_res_earn_campaign_shop_items - return SaoNoopResponse(header.cmd + 1).make() - - async def handle_c108(self, header: SaoRequestHeader, request: bytes) -> bytes: - # common/logout_ticket_unpurchased - req = SaoLogoutTicketUnpurchasedRequest(header, request) - return SaoLogoutTicketUnpurchasedResponse(header.cmd + 1).make() - - async def handle_cb02(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest_ranking/get_quest_hierarchy_progress_degrees_ranking_list - req = SaoGetQuestHierarchyProgressDegreesRankingListRequest(header, request) - return SaoGetQuestHierarchyProgressDegreesRankingListResponse(header.cmd + 1).make() - - async def handle_cb04(self, header: SaoRequestHeader, request: bytes) -> bytes: - # quest_ranking/get_quest_popular_hero_log_ranking_list - req = SaoGetQuestPopularHeroLogRankingListRequest(header, request) - return SaoGetQuestPopularHeroLogRankingListResponse(header.cmd + 1).make() + async def handle_d5fe(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data/get_m_gasha_medal_shop_items + req = SaoGetMGashaMedalShopItemsRequest(header, request) + tbl = self.load_data_csv('GashaMedalShopItems') + resp = SaoGetMGashaMedalShopItemsResponse(tbl) + return resp.make() + + async def handle_d600(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mres_earn_campaign_applications + tbl = self.load_data_csv('ResEarnCampaignApplications') + resp = SaoGetMResEarnCampaignApplicationsResponse(tbl) + return resp.make() + + async def handle_d602(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mres_earn_campaign_application_products + req = SaoGetMResEarnCampaignApplicationProductsRequest(header, request) + tbl = self.load_data_csv('ResEarnCampaignApplicationProducts') + resp = SaoGetMResEarnCampaignApplicationProductsResponse(tbl) + return resp.make() + + async def handle_d604(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mres_earn_campaign_shops + tbl = self.load_data_csv('ResEarnCampaignShops') + resp = SaoGetMResEarnCampaignShopsResponse(tbl) + return resp.make() + + async def handle_d606(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mres_earn_campaign_shop_items + req = SaoGetMResEarnCampaignShopItemsRequest(header, request) + tbl = self.load_data_csv('ResEarnCampaignShopItems') + resp = SaoGetMResEarnCampaignShopItemsResponse(tbl) + return resp.make() + + async def handle_d608(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mpaying_yui_medal_bonuses + tbl = self.load_data_csv('PayingYuiMedalBonuses') + resp = SaoGetMPayingYuiMedalBonusesResponse(tbl) + return resp.make() + + async def handle_d60a(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mac_login_bonuses + tbl = self.load_data_csv('AcLoginBonuses') + resp = SaoGetMAcLoginBonusesResponse(tbl) + return resp.make() + + async def handle_d60c(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mplay_campaigns + tbl = self.load_data_csv('PlayCampaigns') + resp = SaoGetMPlayCampaignsResponse(tbl) + return resp.make() + + async def handle_d60e(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mplay_campaign_rewards + req = SaoGetMPlayCampaignRewardsRequest(header, request) + tbl = self.load_data_csv('PlayCampaignRewards') + resp = SaoGetMPlayCampaignRewardsResponse(tbl) + return resp.make() + + async def handle_d610(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mgasha_free_campaigns + tbl = self.load_data_csv('GashaFreeCampaigns') + resp = SaoGetMGashaFreeCampaignsResponse(tbl) + return resp.make() + + async def handle_d612(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mquest_drop_boost_campaigns + tbl = self.load_data_csv('QuestDropBoostCampaigns') + resp = SaoGetMQuestDropBoostCampaignsResponse(tbl) + return resp.make() + + async def handle_d614(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mfirst_ticket_purchase_campaigns + tbl = self.load_data_csv('FirstTicketPurchaseCampaigns') + resp = SaoGetMFirstTicketPurchaseCampaignsResponse(tbl) + return resp.make() + + async def handle_d616(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mlinked_site_reg_campaigns + tbl = self.load_data_csv('LinkedSiteRegCampaigns') + resp = SaoGetMLinkedSiteRegCampaignsResponse(tbl) + return resp.make() + + async def handle_d618(self, header: SaoRequestHeader, request: bytes, src_ip: str) -> bytes: + # master_data2/get_mlinked_site_reg_campaign_rewards + req = SaoGetMLinkedSiteRegCampaignRewardsRequest(header, request) + tbl = self.load_data_csv('LinkedSiteRegCampaignRewards') + resp = SaoGetMLinkedSiteRegCampaignRewardsResponse(tbl) + return resp.make() diff --git a/titles/sao/config.py b/titles/sao/config.py index dfae3c0..892fdca 100644 --- a/titles/sao/config.py +++ b/titles/sao/config.py @@ -30,9 +30,21 @@ class SaoServerConfig: ) @property - def use_https(self) -> bool: + def photon_app_id(self) -> str: return CoreConfig.get_config_field( - self.__config, "sao", "server", "use_https", default=False + self.__config, "sao", "server", "photon_app_id", default="7df3a2f6-d69d-4073-aafe-810ee61e1cea" + ) + + @property + def data_version(self) -> int: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "data_version", default=1 + ) + + @property + def game_version(self) -> int: + return CoreConfig.get_config_field( + self.__config, "sao", "server", "game_version", default=33 ) class SaoCryptConfig: @@ -50,12 +62,6 @@ class SaoCryptConfig: return CoreConfig.get_config_field( self.__config, "sao", "crypt", "key", default="" ) - - @property - def iv(self) -> str: - return CoreConfig.get_config_field( - self.__config, "sao", "crypt", "iv", default="" - ) class SaoHashConfig: def __init__(self, parent_config: "SaoConfig"): @@ -73,9 +79,31 @@ class SaoHashConfig: self.__config, "sao", "hash", "hash_base", default="" ) +class SaoCardConfig: + def __init__(self, parent_config: "SaoConfig"): + self.__config = parent_config + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "sao", "card", "enable", default=True + ) + + @property + def crypt_password(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "card", "crypt_password", default="" + ) + + @property + def crypt_salt(self) -> str: + return CoreConfig.get_config_field( + self.__config, "sao", "card", "crypt_salt", default="" + ) class SaoConfig(dict): def __init__(self) -> None: self.server = SaoServerConfig(self) self.crypt = SaoCryptConfig(self) self.hash = SaoHashConfig(self) + self.card = SaoCardConfig(self) diff --git a/titles/sao/const.py b/titles/sao/const.py index d181556..d687c64 100644 --- a/titles/sao/const.py +++ b/titles/sao/const.py @@ -1,5 +1,4 @@ -from enum import Enum - +from enum import IntEnum class SaoConstants: GAME_CODE = "SDEW" @@ -9,8 +8,8 @@ class SaoConstants: VER_SAO = 0 - VERSION_NAMES = ("Sword Art Online Arcade") - + VERSION_NAMES = ("Sword Art Online Arcade",) + SERIAL_IDENT_SATALITE = 4 SERIAL_IDENT_TERMINAL = 5 @@ -23,3 +22,653 @@ class SaoConstants: @classmethod def game_ver_to_string(cls, ver: int): return cls.VERSION_NAMES[ver] + +class RewardType(IntEnum): + None_ = 0 + HeroLog = 1 + Equipment = 2 + Item = 3 + Col = 4 + VP = 5 + YuiMadal = 6 + VPGashaTicket = 7 + SupportLog = 8 + EpisodeAppend = 9 + EventItem = 10 + Ticket = 11 + +class ItemType(IntEnum): + ADD_HERO_PROPERTY = 1 + ADD_WEAPON_PROPERTY = 2 + ADD_ARMOR_PROPERTY = 3 + UNLOCK_PROPERTY = 4 + REMOVE_PROPERTY = 5 + ADD_SKILL_SLOT = 6 + ADD_XP = 7 + REDEMPTION = 8 + HERO_LEVEL_LIMIT_BREAK = 9 + WEAPON_LEVEL_LIMIT_BREAK = 10 + ARMOR_LEVEL_LIMIT_BREAK = 11 + ADD_AWAKENING_XP = 12 + +class ExBonusCondition(IntEnum): + CLEAR_UNDER_X_SECS = 1 + DEFEAT_X_MONSTER_Y_TIMES = 2 + DEFEAT_X_MONSTERS = 3 + CLEAR_X_MISSIONS = 4 + CLEAR_MISSION_DIFFICULTY_X = 5 + COLLECT_X_LOGS = 6 + CLEAR_SKILL_LEVEL_X = 7 + NO_LOSSES = 8 + ACCEL_X_TIMES = 9 + MAX_COMBO_X = 10 + MULTIPLAYER_CLEAR_X = 11 + +class UnanalyzedLogGrade(IntEnum): + WHITE = 1 + COPPER = 2 + SILVER = 3 + GOLD = 4 + RAINBOW = 5 + +class QuestType(IntEnum): + EPISODE = 1 + TRIAL_TOWER = 2 + SIDE = 3 + VERSUS = 4 + EX_TOWER = 5 + EPISODE_CHAPTER = 6 # Unused + +class GameconnectCmd(IntEnum): + TICKET_REQUEST = 0xC000 + TICKET_RESPONSE = 0xC001 + GET_APP_VERSIONS_REQUEST = 0xC100 + GET_APP_VERSIONS_RESPONSE = 0xC101 + MASTER_DATA_VERSION_CHECK_REQUEST = 0xC102 + MASTER_DATA_VERSION_CHECK_RESPONSE = 0xC103 + LOGIN_REQUEST = 0xC104 + LOGIN_RESPONSE = 0xC105 + LOGOUT_REQUEST = 0xC106 + LOGOUT_RESPONSE = 0xC107 + LOGOUT_TICKET_UNPURCHASED_REQUEST = 0xC108 + LOGOUT_TICKET_UNPURCHASED_RESPONSE = 0xC109 + PAYING_PLAY_START_REQUEST = 0xC10A + PAYING_PLAY_START_RESPONSE = 0xC10B + PAYING_PLAY_END_REQUEST = 0xC10C + PAYING_PLAY_END_RESPONSE = 0xC10D + PURCHASE_TICKET_REQUEST = 0xC10E + PURCHASE_TICKET_RESPONSE = 0xC10F + CONSUME_TICKET_REQUEST = 0xC110 + CONSUME_TICKET_RESPONSE = 0xC111 + ADD_CREDIT_REQUEST = 0xC112 + ADD_CREDIT_RESPONSE = 0xC113 + CONSUME_CREDIT_REQUEST = 0xC114 + CONSUME_CREDIT_RESPONSE = 0xC115 + PURCHASE_TICKET_GUEST_REQUEST = 0xC116 + PURCHASE_TICKET_GUEST_RESPONSE = 0xC117 + CONSUME_TICKET_GUEST_REQUEST = 0xC118 + CONSUME_TICKET_GUEST_RESPONSE = 0xC119 + ADD_CREDIT_GUEST_REQUEST = 0xC11A + ADD_CREDIT_GUEST_RESPONSE = 0xC11B + CONSUME_CREDIT_GUEST_REQUEST = 0xC11C + CONSUME_CREDIT_GUEST_RESPONSE = 0xC11D + GET_AUTH_CARD_DATA_REQUEST = 0xC11E + GET_AUTH_CARD_DATA_RESPONSE = 0xC11F + GET_ACCESS_CODE_BY_KEITAI_REQUEST = 0xC120 + GET_ACCESS_CODE_BY_KEITAI_RESPONSE = 0xC121 + GET_MAINTENANCE_INFO_REQUEST = 0xC122 + GET_MAINTENANCE_INFO_RESPONSE = 0xC123 + GET_RESOURCE_PATH_INFO_REQUEST = 0xC124 + GET_RESOURCE_PATH_INFO_RESPONSE = 0xC125 + VALIDATION_ERROR_NOTIFICATION_REQUEST = 0xC126 + VALIDATION_ERROR_NOTIFICATION_RESPONSE = 0xC127 + POWER_CUTTING_RETURN_NOTIFICATION_REQUEST = 0xC128 + POWER_CUTTING_RETURN_NOTIFICATION_RESPONSE = 0xC129 + GIVE_FREE_TICKET_REQUEST = 0xC12A + GIVE_FREE_TICKET_RESPONSE = 0xC12B + MATCHING_ERROR_NOTIFICATION_REQUEST = 0xC12C + MATCHING_ERROR_NOTIFICATION_RESPONSE = 0xC12D + AC_CABINET_BOOT_NOTIFICATION_REQUEST = 0xC12E + AC_CABINET_BOOT_NOTIFICATION_RESPONSE = 0xC12F + FIRST_TUTORIAL_END_REQUEST = 0xC200 + FIRST_TUTORIAL_END_RESPONSE = 0xC201 + VARIOUS_TUTORIAL_END_REQUEST = 0xC202 + VARIOUS_TUTORIAL_END_RESPONSE = 0xC203 + GET_VARIOUS_TUTORIAL_DATA_LIST_REQUEST = 0xC204 + GET_VARIOUS_TUTORIAL_DATA_LIST_RESPONSE = 0xC205 + DISCHARGE_PROFILE_CARD_REQUEST = 0xC300 + DISCHARGE_PROFILE_CARD_RESPONSE = 0xC301 + DISCHARGE_RESOURCE_CARD_REQUEST = 0xC302 + DISCHARGE_RESOURCE_CARD_RESPONSE = 0xC303 + DISCHARGE_RESOURCE_CARD_COMPLETE_REQUEST = 0xC304 + DISCHARGE_RESOURCE_CARD_COMPLETE_RESPONSE = 0xC305 + SCAN_QR_QUEST_PROFILE_CARD_REQUEST = 0xC306 + SCAN_QR_QUEST_PROFILE_CARD_RESPONSE = 0xC307 + SCAN_QR_SHOP_RESOURCE_CARD_REQUEST = 0xC308 + SCAN_QR_SHOP_RESOURCE_CARD_RESPONSE = 0xC309 + SCAN_QR_QUEST_RESOURCE_CARD_REQUEST = 0xC30A + SCAN_QR_QUEST_RESOURCE_CARD_RESPONSE = 0xC30B + CHECK_YUI_MEDAL_GET_CONDITION_REQUEST = 0xC400 + CHECK_YUI_MEDAL_GET_CONDITION_RESPONSE = 0xC401 + GET_YUI_MEDAL_BONUS_USER_DATA_REQUEST = 0xC402 + GET_YUI_MEDAL_BONUS_USER_DATA_RESPONSE = 0xC403 + CHECK_COMEBACK_EVENT_REQUEST = 0xC404 + CHECK_COMEBACK_EVENT_RESPONSE = 0xC405 + CHANGE_MY_STORE_REQUEST = 0xC406 + CHANGE_MY_STORE_RESPONSE = 0xC407 + CHECK_TITLE_GET_DECISION_REQUEST = 0xC408 + CHECK_TITLE_GET_DECISION_RESPONSE = 0xC409 + CHECK_PROFILE_CARD_USED_REWARD_REQUEST = 0xC40A + CHECK_PROFILE_CARD_USED_REWARD_RESPONSE = 0xC40B + CHECK_AC_LOGIN_BONUS_REQUEST = 0xC40C + CHECK_AC_LOGIN_BONUS_RESPONSE = 0xC40D + GET_USER_BASIC_DATA_REQUEST = 0xC500 + GET_USER_BASIC_DATA_RESPONSE = 0xC501 + GET_VP_GASHA_TICKET_DATA_LIST_REQUEST = 0xC502 + GET_VP_GASHA_TICKET_DATA_LIST_RESPONSE = 0xC503 + GET_PRESENT_BOX_NUM_REQUEST = 0xC504 + GET_PRESENT_BOX_NUM_RESPONSE = 0xC505 + GET_HERO_LOG_USER_DATA_LIST_REQUEST = 0xC600 + GET_HERO_LOG_USER_DATA_LIST_RESPONSE = 0xC601 + GET_EQUIPMENT_USER_DATA_LIST_REQUEST = 0xC602 + GET_EQUIPMENT_USER_DATA_LIST_RESPONSE = 0xC603 + GET_ITEM_USER_DATA_LIST_REQUEST = 0xC604 + GET_ITEM_USER_DATA_LIST_RESPONSE = 0xC605 + GET_SUPPORT_LOG_USER_DATA_LIST_REQUEST = 0xC606 + GET_SUPPORT_LOG_USER_DATA_LIST_RESPONSE = 0xC607 + GET_EPISODE_APPEND_DATA_LIST_REQUEST = 0xC608 + GET_EPISODE_APPEND_DATA_LIST_RESPONSE = 0xC609 + GET_EVENT_ITEM_DATA_LIST_REQUEST = 0xC60A + GET_EVENT_ITEM_DATA_LIST_RESPONSE = 0xC60B + GET_GASHA_MEDAL_USER_DATA_LIST_REQUEST = 0xC60C + GET_GASHA_MEDAL_USER_DATA_LIST_RESPONSE = 0xC60D + GET_SHOP_RESOURCE_SALES_DATA_LIST_REQUEST = 0xC700 + GET_SHOP_RESOURCE_SALES_DATA_LIST_RESPONSE = 0xC701 + PURCHASE_SHOP_RESOURCE_REQUEST = 0xC702 + PURCHASE_SHOP_RESOURCE_RESPONSE = 0xC703 + DISCARD_SHOP_RESOURCE_REQUEST = 0xC704 + DISCARD_SHOP_RESOURCE_RESPONSE = 0xC705 + GET_TITLE_USER_DATA_LIST_REQUEST = 0xC800 + GET_TITLE_USER_DATA_LIST_RESPONSE = 0xC801 + CHANGE_TITLE_REQUEST = 0xC802 + CHANGE_TITLE_RESPONSE = 0xC803 + GET_PARTY_DATA_LIST_REQUEST = 0xC804 + GET_PARTY_DATA_LIST_RESPONSE = 0xC805 + CHANGE_PARTY_REQUEST = 0xC806 + CHANGE_PARTY_RESPONSE = 0xC807 + GET_SUPPORT_LOG_PARTY_DATA_LIST_REQUEST = 0xC808 + GET_SUPPORT_LOG_PARTY_DATA_LIST_RESPONSE = 0xC809 + CHANGE_SUPPORT_LOG_PARTY_REQUEST = 0xC80A + CHANGE_SUPPORT_LOG_PARTY_RESPONSE = 0xC80B + CHANGE_HERO_LOG_LAST_SETTING_SKILL_REQUEST = 0xC80C + CHANGE_HERO_LOG_LAST_SETTING_SKILL_RESPONSE = 0xC80D + LOCK_RESOURCE_REQUEST = 0xC80E + LOCK_RESOURCE_RESPONSE = 0xC80F + UNLOCK_RESOURCE_REQUEST = 0xC810 + UNLOCK_RESOURCE_RESPONSE = 0xC811 + DISPOSAL_RESOURCE_REQUEST = 0xC812 + DISPOSAL_RESOURCE_RESPONSE = 0xC813 + SYNTHESIZE_ENHANCEMENT_HERO_LOG_REQUEST = 0xC814 + SYNTHESIZE_ENHANCEMENT_HERO_LOG_RESPONSE = 0xC815 + SYNTHESIZE_ENHANCEMENT_EQUIPMENT_REQUEST = 0xC816 + SYNTHESIZE_ENHANCEMENT_EQUIPMENT_RESPONSE = 0xC817 + SYNTHESIZE_ENHANCEMENT_SUPPORT_LOG_REQUEST = 0xC818 + SYNTHESIZE_ENHANCEMENT_SUPPORT_LOG_RESPONSE = 0xC819 + SYNTHESIZE_ABILITY_HERO_LOG_REQUEST = 0xC81A + SYNTHESIZE_ABILITY_HERO_LOG_RESPONSE = 0xC81B + SYNTHESIZE_ABILITY_EQUIPMENT_REQUEST = 0xC81C + SYNTHESIZE_ABILITY_EQUIPMENT_RESPONSE = 0xC81D + SYNTHESIZE_ABILITY_SUPPORT_LOG_REQUEST = 0xC81E + SYNTHESIZE_ABILITY_SUPPORT_LOG_RESPONSE = 0xC820 + GET_QUEST_SCENE_USER_DATA_LIST_REQUEST = 0xC900 + GET_QUEST_SCENE_USER_DATA_LIST_RESPONSE = 0xC901 + GET_QUEST_SCENE_PREV_SCAN_PROFILE_CARD_REQUEST = 0xC902 + GET_QUEST_SCENE_PREV_SCAN_PROFILE_CARD_RESPONSE = 0xC903 + EPISODE_PLAY_START_REQUEST = 0xC904 + EPISODE_PLAY_START_RESPONSE = 0xC905 + EPISODE_PLAY_CONTINUE_REQUEST = 0xC906 + EPISODE_PLAY_CONTINUE_RESPONSE = 0xC907 + EPISODE_PLAY_END_REQUEST = 0xC908 + EPISODE_PLAY_END_RESPONSE = 0xC909 + EPISODE_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC90A + EPISODE_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC90B + SIDE_QUEST_PLAY_START_REQUEST = 0xC90C + SIDE_QUEST_PLAY_START_RESPONSE = 0xC90D + SIDE_QUEST_PLAY_CONTINUE_REQUEST = 0xC90E + SIDE_QUEST_PLAY_CONTINUE_RESPONSE = 0xC90F + SIDE_QUEST_PLAY_END_REQUEST = 0xC910 + SIDE_QUEST_PLAY_END_RESPONSE = 0xC911 + SIDE_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC912 + SIDE_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC913 + TRIAL_TOWER_PLAY_START_REQUEST = 0xC914 + TRIAL_TOWER_PLAY_START_RESPONSE = 0xC915 + TRIAL_TOWER_PLAY_CONTINUE_REQUEST = 0xC916 + TRIAL_TOWER_PLAY_CONTINUE_RESPONSE = 0xC917 + TRIAL_TOWER_PLAY_END_REQUEST = 0xC918 + TRIAL_TOWER_PLAY_END_RESPONSE = 0xC919 + TRIAL_TOWER_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC91A + TRIAL_TOWER_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC91B + DEFRAG_MATCH_QUEST_PLAY_START_REQUEST = 0xC91C + DEFRAG_MATCH_QUEST_PLAY_START_RESPONSE = 0xC91D + DEFRAG_MATCH_QUEST_PLAY_END_REQUEST = 0xC91E + DEFRAG_MATCH_QUEST_PLAY_END_RESPONSE = 0xC91F + DEFRAG_MATCH_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC920 + DEFRAG_MATCH_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC921 + EX_TOWER_QUEST_PLAY_START_REQUEST = 0xC922 + EX_TOWER_QUEST_PLAY_START_RESPONSE = 0xC923 + EX_TOWER_QUEST_PLAY_CONTINUE_REQUEST = 0xC924 + EX_TOWER_QUEST_PLAY_CONTINUE_RESPONSE = 0xC925 + EX_TOWER_QUEST_PLAY_END_REQUEST = 0xC926 + EX_TOWER_QUEST_PLAY_END_RESPONSE = 0xC927 + EX_TOWER_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC928 + EX_TOWER_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC929 + GET_EX_TOWER_USER_DATA_REQUEST = 0xC92A + GET_EX_TOWER_USER_DATA_RESPONSE = 0xC92B + REG_EX_TOWER_HALL_OF_FAME_CONFIRM_FLAG_REQUEST = 0xC92C + REG_EX_TOWER_HALL_OF_FAME_CONFIRM_FLAG_RESPONSE = 0xC92D + REG_EX_TOWER_CONFIRM_FLAG_REQUEST = 0xC92E + REG_EX_TOWER_CONFIRM_FLAG_RESPONSE = 0xC92F + GET_CHAT_SIDE_STORY_USER_DATA_LIST_REQUEST = 0xC930 + GET_CHAT_SIDE_STORY_USER_DATA_LIST_RESPONSE = 0xC931 + GET_USER_QUEST_EPISODE_STATUS_DATA_LIST_REQUEST = 0xC932 + GET_USER_QUEST_EPISODE_STATUS_DATA_LIST_RESPONSE = 0xC933 + APPLY_QUEST_DROP_BOOST_REQUEST = 0xC934 + APPLY_QUEST_DROP_BOOST_RESPONSE = 0xC935 + CREATE_QUEST_SCENE_MULTI_PLAY_ROOM_ID_REQUEST = 0xCA00 + CREATE_QUEST_SCENE_MULTI_PLAY_ROOM_ID_RESPONSE = 0xCA01 + GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_REQUEST = 0xCA02 + GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_RESPONSE = 0xCA03 + GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_BY_ROOM_REQUEST = 0xCA04 + GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_BY_ROOM_RESPONSE = 0xCA05 + GET_QUEST_BEST_SOCRE_RANKING_LIST_REQUEST = 0xCB00 + GET_QUEST_BEST_SOCRE_RANKING_LIST_RESPONSE = 0xCB01 + GET_QUEST_HIERARCHY_PROGRESS_DEGREES_RANKING_LIST_REQUEST = 0xCB02 + GET_QUEST_HIERARCHY_PROGRESS_DEGREES_RANKING_LIST_RESPONSE = 0xCB03 + GET_QUEST_POPULAR_HERO_LOG_RANKING_LIST_REQUEST = 0xCB04 + GET_QUEST_POPULAR_HERO_LOG_RANKING_LIST_RESPONSE = 0xCB05 + GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_STORE_RANKING_LIST_REQUEST = 0xCB06 + GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_STORE_RANKING_LIST_RESPONSE = 0xCB07 + GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_NATIONAL_RANKING_LIST_REQUEST = 0xCB08 + GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_NATIONAL_RANKING_LIST_RESPONSE = 0xCB09 + GET_TREASURE_HUNT_BASIC_DATA_REQUEST = 0xCC00 + GET_TREASURE_HUNT_BASIC_DATA_RESPONSE = 0xCC01 + GET_TREASURE_HUNT_WHOLE_TASK_DATA_LIST_REQUEST = 0xCC02 + GET_TREASURE_HUNT_WHOLE_TASK_DATA_LIST_RESPONSE = 0xCC03 + GET_TREASURE_HUNT_INDIVIDUAL_TASK_DATA_LIST_REQUEST = 0xCC04 + GET_TREASURE_HUNT_INDIVIDUAL_TASK_DATA_LIST_RESPONSE = 0xCC05 + TREASURE_HUNT_AD_CONFIRM_NOTIFICATION_REQUEST = 0xCC06 + TREASURE_HUNT_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xCC07 + GET_TREASURE_HUNT_EVENT_POINT_RANKING_USER_DATA_REQUEST = 0xCC08 + GET_TREASURE_HUNT_EVENT_POINT_RANKING_USER_DATA_RESPONSE = 0xCC09 + GET_TREASURE_HUNT_EVENT_POINT_STORE_RANKING_LIST_REQUEST = 0xCC0A + GET_TREASURE_HUNT_EVENT_POINT_STORE_RANKING_LIST_RESPONSE = 0xCC0B + GET_TREASURE_HUNT_EVENT_POINT_NATIONAL_RANKING_LIST_REQUEST = 0xCC0C + GET_TREASURE_HUNT_EVENT_POINT_NATIONAL_RANKING_LIST_RESPONSE = 0xCC0D + GET_DEFRAG_MATCH_BASIC_DATA_REQUEST = 0xCD00 + GET_DEFRAG_MATCH_BASIC_DATA_RESPONSE = 0xCD01 + GET_DEFRAG_MATCH_RANKING_USER_DATA_REQUEST = 0xCD02 + GET_DEFRAG_MATCH_RANKING_USER_DATA_RESPONSE = 0xCD03 + GET_DEFRAG_MATCH_LEAGUE_POINT_RANKING_LIST_REQUEST = 0xCD04 + GET_DEFRAG_MATCH_LEAGUE_POINT_RANKING_LIST_RESPONSE = 0xCD05 + GET_DEFRAG_MATCH_LEAGUE_SCORE_RANKING_LIST_REQUEST = 0xCD06 + GET_DEFRAG_MATCH_LEAGUE_SCORE_RANKING_LIST_RESPONSE = 0xCD07 + DEFRAG_MATCH_AD_CONFIRM_NOTIFICATION_REQUEST = 0xCD08 + DEFRAG_MATCH_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xCD09 + CHECK_GET_SEED_DEFRAG_MATCH_REQUEST = 0xCD0A + CHECK_GET_SEED_DEFRAG_MATCH_RESPONSE = 0xCD0B + REG_DEFRAG_MATCH_HALL_OF_FAME_CONFIRM_FLAG_REQUEST = 0xCD0C + REG_DEFRAG_MATCH_HALL_OF_FAME_CONFIRM_FLAG_RESPONSE = 0xCD0D + GET_EVENT_SCENE_USER_DATA_LIST_REQUEST = 0xCE00 + GET_EVENT_SCENE_USER_DATA_LIST_RESPONSE = 0xCE01 + REG_PLAYED_EVENT_SCENE_USER_DATA_LIST_REQUEST = 0xCE02 + REG_PLAYED_EVENT_SCENE_USER_DATA_LIST_RESPONSE = 0xCE03 + GET_GASHA_LIST_REQUEST = 0xCF00 + GET_GASHA_LIST_RESPONSE = 0xCF01 + EXEC_GASHA_REQUEST = 0xCF02 + EXEC_GASHA_RESPONSE = 0xCF03 + EXEC_CREDIT_GASHA_START_REQUEST = 0xCF04 + EXEC_CREDIT_GASHA_START_RESPONSE = 0xCF05 + EXEC_CREDIT_GASHA_EXTRA_PAYING_REQUEST = 0xCF06 + EXEC_CREDIT_GASHA_EXTRA_PAYING_RESPONSE = 0xCF07 + EXEC_CREDIT_GASHA_END_REQUEST = 0xCF08 + EXEC_CREDIT_GASHA_END_RESPONSE = 0xCF09 + EXEC_CREDIT_GASHA_CARD_DISCHARGE_STATE_MIGRATION_REQUEST = 0xCF0A + EXEC_CREDIT_GASHA_CARD_DISCHARGE_STATE_MIGRATION_RESPONSE = 0xCF0B + EXEC_CREDIT_GASHA_CARD_DISCHARGE_END_REQUEST = 0xCF0C + EXEC_CREDIT_GASHA_CARD_DISCHARGE_END_RESPONSE = 0xCF0D + GET_GASHA_MEDAL_SHOP_USER_DATA_LIST_REQUEST = 0xCF0E + GET_GASHA_MEDAL_SHOP_USER_DATA_LIST_RESPONSE = 0xCF0F + PURCHASE_GASHA_MEDAL_SHOP_ITEM_REQUEST = 0xCF10 + PURCHASE_GASHA_MEDAL_SHOP_ITEM_RESPONSE = 0xCF11 + GET_GASHA_FREE_CAMPAIGN_USER_DATA_REQUEST = 0xCF12 + GET_GASHA_FREE_CAMPAIGN_USER_DATA_RESPONSE = 0xCF13 + GET_ADVENTURE_EXEC_USER_DATA_REQUEST = 0xD000 + GET_ADVENTURE_EXEC_USER_DATA_RESPONSE = 0xD001 + GET_ADVENTURE_PARTY_DATA_LIST_REQUEST = 0xD002 + GET_ADVENTURE_PARTY_DATA_LIST_RESPONSE = 0xD003 + GET_YUI_MEDAL_SHOP_USER_DATA_LIST_REQUEST = 0xD100 + GET_YUI_MEDAL_SHOP_USER_DATA_LIST_RESPONSE = 0xD101 + PURCHASE_YUI_MEDAL_SHOP_ITEM_REQUEST = 0xD102 + PURCHASE_YUI_MEDAL_SHOP_ITEM_RESPONSE = 0xD103 + PURCHASE_EVENT_SHOP_ITEM_REQUEST = 0xD104 + PURCHASE_EVENT_SHOP_ITEM_RESPONSE = 0xD105 + GET_BEGINNER_MISSION_USER_DATA_REQUEST = 0xD200 + GET_BEGINNER_MISSION_USER_DATA_RESPONSE = 0xD201 + GET_BEGINNER_MISSION_PROGRESSES_USER_DATA_LIST_REQUEST = 0xD202 + GET_BEGINNER_MISSION_PROGRESSES_USER_DATA_LIST_RESPONSE = 0xD203 + GET_BEGINNER_MISSION_SEAT_PROGRESSES_USER_DATA_LIST_REQUEST = 0xD204 + GET_BEGINNER_MISSION_SEAT_PROGRESSES_USER_DATA_LIST_RESPONSE = 0xD205 + BEGINNER_MISSION_AD_CONFIRM_NOTIFICATION_REQUEST = 0xD206 + BEGINNER_MISSION_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xD207 + RECEIVE_BEGINNER_MISSION_REWARD_REQUEST = 0xD208 + RECEIVE_BEGINNER_MISSION_REWARD_RESPONSE = 0xD209 + GET_RES_EARN_CAMPAIGN_SHOP_USER_DATA_LIST_REQUEST = 0xD300 + GET_RES_EARN_CAMPAIGN_SHOP_USER_DATA_LIST_RESPONSE = 0xD301 + PURCHASE_RES_EARN_CAMPAIGN_SHOP_ITEM_REQUEST = 0xD302 + PURCHASE_RES_EARN_CAMPAIGN_SHOP_ITEM_RESPONSE = 0xD303 + PAYING_YUI_MEDAL_BONUS_GET_CHECK_REQUEST = 0xD304 + PAYING_YUI_MEDAL_BONUS_GET_CHECK_RESPONSE = 0xD305 + PAYING_YUI_MEDAL_BONUS_AD_CONFIRM_NOTIFICATION_REQUEST = 0xD306 + PAYING_YUI_MEDAL_BONUS_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xD307 + GET_PLAY_CAMPAIGN_USER_DATA_LIST_REQUEST = 0xD308 + GET_PLAY_CAMPAIGN_USER_DATA_LIST_RESPONSE = 0xD309 + GET_PLAY_CAMPAIGN_STORE_USER_DATA_LIST_REQUEST = 0xD30A + GET_PLAY_CAMPAIGN_STORE_USER_DATA_LIST_RESPONSE = 0xD30B + GET_PLAY_CAMPAIGN_REWARD_USER_DATA_LIST_REQUEST = 0xD30C + GET_PLAY_CAMPAIGN_REWARD_USER_DATA_LIST_RESPONSE = 0xD30D + APPLY_FIRST_TICKET_PURCHASE_CAMPAIGN_REQUEST = 0xD30E + APPLY_FIRST_TICKET_PURCHASE_CAMPAIGN_RESPONSE = 0xD30F + GET_FIRST_TICKET_PURCHASE_CAMPAIGN_USER_DATA_REQUEST = 0xD310 + GET_FIRST_TICKET_PURCHASE_CAMPAIGN_USER_DATA_RESPONSE = 0xD311 + GET_LINKED_SITE_REG_CAMPAIGN_USER_DATA_REQUEST = 0xD312 + GET_LINKED_SITE_REG_CAMPAIGN_USER_DATA_RESPONSE = 0xD313 + GET_HERO_LOG_UNIT_USER_DATA_LIST_REQUEST = 0xD400 + GET_HERO_LOG_UNIT_USER_DATA_LIST_RESPONSE = 0xD401 + GET_CHARA_UNIT_USER_DATA_LIST_REQUEST = 0xD402 + GET_CHARA_UNIT_USER_DATA_LIST_RESPONSE = 0xD403 + BNID_SERIAL_CODE_CHECK_REQUEST = 0xD404 + BNID_SERIAL_CODE_CHECK_RESPONSE = 0xD405 + BNID_SERIAL_CODE_ENTRY_BY_APPENDIX_CARD_REQUEST = 0xD406 + BNID_SERIAL_CODE_ENTRY_BY_APPENDIX_CARD_RESPONSE = 0xD407 + GET_M_PLAYER_RANKS_REQUEST = 0xD500 + GET_M_PLAYER_RANKS_RESPONSE = 0xD501 + GET_M_TITLES_REQUEST = 0xD502 + GET_M_TITLES_RESPONSE = 0xD503 + GET_M_FRAGMENTS_REQUEST = 0xD504 + GET_M_FRAGMENTS_RESPONSE = 0xD505 + GET_M_REWARD_TABLES_REQUEST = 0xD506 + GET_M_REWARD_TABLES_RESPONSE = 0xD507 + GET_M_REWARD_SETS_REQUEST = 0xD508 + GET_M_REWARD_SETS_RESPONSE = 0xD509 + GET_M_UNANALYZED_LOG_GRADES_REQUEST = 0xD50A + GET_M_UNANALYZED_LOG_GRADES_RESPONSE = 0xD50B + GET_M_APPOINT_LEADER_PARAMS_REQUEST = 0xD50C + GET_M_APPOINT_LEADER_PARAMS_RESPONSE = 0xD50D + GET_M_APPOINT_LEADER_EFFECTS_REQUEST = 0xD50E + GET_M_APPOINT_LEADER_EFFECTS_RESPONSE = 0xD50F + GET_M_APPOINT_LEADER_EFFECT_TYPES_REQUEST = 0xD510 + GET_M_APPOINT_LEADER_EFFECT_TYPES_RESPONSE = 0xD511 + GET_M_RARITIES_REQUEST = 0xD512 + GET_M_RARITIES_RESPONSE = 0xD513 + GET_M_COMPOSITION_EVENTS_REQUEST = 0xD514 + GET_M_COMPOSITION_EVENTS_RESPONSE = 0xD515 + GET_M_COMPOSITION_PARAMS_REQUEST = 0xD516 + GET_M_COMPOSITION_PARAMS_RESPONSE = 0xD517 + GET_M_GAME_PLAY_PRICES_REQUEST = 0xD518 + GET_M_GAME_PLAY_PRICES_RESPONSE = 0xD519 + GET_M_BUY_TICKETS_REQUEST = 0xD51A + GET_M_BUY_TICKETS_RESPONSE = 0xD51B + GET_M_TIPS_REQUEST = 0xD51C + GET_M_TIPS_RESPONSE = 0xD51D + GET_M_CAPS_REQUEST = 0xD51E + GET_M_CAPS_RESPONSE = 0xD51F + GET_M_HERO_LOG_REQUEST = 0xD520 + GET_M_HERO_LOG_RESPONSE = 0xD521 + GET_M_HERO_LOG_LEVELS_REQUEST = 0xD522 + GET_M_HERO_LOG_LEVELS_RESPONSE = 0xD523 + GET_M_HERO_LOG_ROLES_REQUEST = 0xD524 + GET_M_HERO_LOG_ROLES_RESPONSE = 0xD525 + GET_M_HERO_LOG_TRUST_RANKS_REQUEST = 0xD526 + GET_M_HERO_LOG_TRUST_RANKS_RESPONSE = 0xD527 + GET_M_CHARAS_REQUEST = 0xD528 + GET_M_CHARAS_RESPONSE = 0xD529 + GET_M_CHARA_FRIENDLY_RANKS_REQUEST = 0xD52A + GET_M_CHARA_FRIENDLY_RANKS_RESPONSE = 0xD52B + GET_M_EQUIPMENTS_REQUEST = 0xD52C + GET_M_EQUIPMENTS_RESPONSE = 0xD52D + GET_M_EQUIPMENT_LEVELS_REQUEST = 0xD52E + GET_M_EQUIPMENT_LEVELS_RESPONSE = 0xD52F + GET_M_WEAPON_TYPES_REQUEST = 0xD530 + GET_M_WEAPON_TYPES_RESPONSE = 0xD531 + GET_M_ITEMS_REQUEST = 0xD532 + GET_M_ITEMS_RESPONSE = 0xD533 + GET_M_ITEM_TYPES_REQUEST = 0xD534 + GET_M_ITEM_TYPES_RESPONSE = 0xD535 + GET_M_BUFF_ITEMS_REQUEST = 0xD536 + GET_M_BUFF_ITEMS_RESPONSE = 0xD537 + GET_M_ENEMIES_REQUEST = 0xD538 + GET_M_ENEMIES_RESPONSE = 0xD539 + GET_M_ENEMY_SETS_REQUEST = 0xD53A + GET_M_ENEMY_SETS_RESPONSE = 0xD53B + GET_M_ENEMY_KINDS_REQUEST = 0xD53C + GET_M_ENEMY_KINDS_RESPONSE = 0xD53D + GET_M_ENEMY_CATEGORIES_REQUEST = 0xD53E + GET_M_ENEMY_CATEGORIES_RESPONSE = 0xD53F + GET_M_UNITS_REQUEST = 0xD540 + GET_M_UNITS_RESPONSE = 0xD541 + GET_M_UNIT_GIMMICKS_REQUEST = 0xD542 + GET_M_UNIT_GIMMICKS_RESPONSE = 0xD543 + GET_M_UNIT_COLLISIONS_REQUEST = 0xD544 + GET_M_UNIT_COLLISIONS_RESPONSE = 0xD545 + GET_M_UNIT_POWERS_REQUEST = 0xD546 + GET_M_UNIT_POWERS_RESPONSE = 0xD547 + GET_M_GIMMICK_ATTACKS_REQUEST = 0xD548 + GET_M_GIMMICK_ATTACKS_RESPONSE = 0xD549 + GET_M_CHARA_ATTACKS_REQUEST = 0xD54A + GET_M_CHARA_ATTACKS_RESPONSE = 0xD54B + GET_M_BOSS_ATTACKS_REQUEST = 0xD54C + GET_M_BOSS_ATTACKS_RESPONSE = 0xD54D + GET_M_MONSTER_ATTACKS_REQUEST = 0xD54E + GET_M_MONSTER_ATTACKS_RESPONSE = 0xD54F + GET_M_MONSTER_ACTIONS_REQUEST = 0xD550 + GET_M_MONSTER_ACTIONS_RESPONSE = 0xD551 + GET_M_PROPERTIES_REQUEST = 0xD552 + GET_M_PROPERTIES_RESPONSE = 0xD553 + GET_M_PROPERTY_TABLES_REQUEST = 0xD554 + GET_M_PROPERTY_TABLES_RESPONSE = 0xD555 + GET_M_PROPERTY_TYPES_REQUEST = 0xD556 + GET_M_PROPERTY_TYPES_RESPONSE = 0xD557 + GET_M_SKILLS_REQUEST = 0xD558 + GET_M_SKILLS_RESPONSE = 0xD559 + GET_M_SKILL_TABLES_REQUEST = 0xD55A + GET_M_SKILL_TABLES_RESPONSE = 0xD55B + GET_M_SKILL_LEVELS_REQUEST = 0xD55C + GET_M_SKILL_LEVELS_RESPONSE = 0xD55D + GET_M_AWAKENINGS_REQUEST = 0xD55E + GET_M_AWAKENINGS_RESPONSE = 0xD55F + GET_M_SYNCHRO_SKILLS_REQUEST = 0xD560 + GET_M_SYNCHRO_SKILLS_RESPONSE = 0xD561 + GET_M_SOUND_SKILL_CUT_IN_VOICES_REQUEST = 0xD562 + GET_M_SOUND_SKILL_CUT_IN_VOICES_RESPONSE = 0xD563 + GET_M_QUEST_SCENES_REQUEST = 0xD564 + GET_M_QUEST_SCENES_RESPONSE = 0xD565 + GET_M_QUEST_EXIST_UNITS_REQUEST = 0xD566 + GET_M_QUEST_EXIST_UNITS_RESPONSE = 0xD567 + GET_M_QUEST_EPISODE_APPEND_REWARDS_REQUEST = 0xD568 + GET_M_QUEST_EPISODE_APPEND_REWARDS_RESPONSE = 0xD569 + GET_M_SIDE_QUESTS_REQUEST = 0xD56A + GET_M_SIDE_QUESTS_RESPONSE = 0xD56B + GET_M_EPISODES_REQUEST = 0xD56C + GET_M_EPISODES_RESPONSE = 0xD56D + GET_M_EPISODE_CHAPTERS_REQUEST = 0xD56E + GET_M_EPISODE_CHAPTERS_RESPONSE = 0xD56F + GET_M_EPISODE_PARTS_REQUEST = 0xD570 + GET_M_EPISODE_PARTS_RESPONSE = 0xD571 + GET_M_TRIAL_TOWERS_REQUEST = 0xD572 + GET_M_TRIAL_TOWERS_RESPONSE = 0xD573 + GET_M_EX_TOWERS_REQUEST = 0xD574 + GET_M_EX_TOWERS_RESPONSE = 0xD575 + GET_M_EX_TOWER_QUESTS_REQUEST = 0xD576 + GET_M_EX_TOWER_QUESTS_RESPONSE = 0xD577 + GET_M_MENU_DISPLAY_ENEMIES_REQUEST = 0xD578 + GET_M_MENU_DISPLAY_ENEMIES_RESPONSE = 0xD579 + GET_M_MISSIONS_REQUEST = 0xD57A + GET_M_MISSIONS_RESPONSE = 0xD57B + GET_M_MISSION_TABLES_REQUEST = 0xD57C + GET_M_MISSION_TABLES_RESPONSE = 0xD57D + GET_M_MISSION_DIFFICULTIES_REQUEST = 0xD57E + GET_M_MISSION_DIFFICULTIES_RESPONSE = 0xD57F + GET_M_BATTLE_CAMERAS_REQUEST = 0xD580 + GET_M_BATTLE_CAMERAS_RESPONSE = 0xD581 + GET_M_CHAT_MAIN_STORIES_REQUEST = 0xD582 + GET_M_CHAT_MAIN_STORIES_RESPONSE = 0xD583 + GET_M_CHAT_SIDE_STORIES_REQUEST = 0xD584 + GET_M_CHAT_SIDE_STORIES_RESPONSE = 0xD585 + GET_M_CHAT_EVENT_STORIES_REQUEST = 0xD586 + GET_M_CHAT_EVENT_STORIES_RESPONSE = 0xD587 + GET_M_NAVIGATOR_CHARAS_REQUEST = 0xD588 + GET_M_NAVIGATOR_CHARAS_RESPONSE = 0xD589 + GET_M_NAVIGATOR_COMMENTS_REQUEST = 0xD58A + GET_M_NAVIGATOR_COMMENTS_RESPONSE = 0xD58B + GET_M_EX_BONUS_TABLES_REQUEST = 0xD58C + GET_M_EX_BONUS_TABLES_RESPONSE = 0xD58D + GET_M_EX_BONUS_CONDITIONS_REQUEST = 0xD58E + GET_M_EX_BONUS_CONDITIONS_RESPONSE = 0xD58F + GET_M_QUEST_RARE_DROPS_REQUEST = 0xD590 + GET_M_QUEST_RARE_DROPS_RESPONSE = 0xD591 + GET_M_QUEST_SPECIAL_RARE_DROP_SETTINGS_REQUEST = 0xD592 + GET_M_QUEST_SPECIAL_RARE_DROP_SETTINGS_RESPONSE = 0xD593 + GET_M_QUEST_SPECIAL_RARE_DROPS_REQUEST = 0xD594 + GET_M_QUEST_SPECIAL_RARE_DROPS_RESPONSE = 0xD595 + GET_M_QUEST_TUTORIALS_REQUEST = 0xD596 + GET_M_QUEST_TUTORIALS_RESPONSE = 0xD597 + GET_M_QUEST_PLAYER_TRACE_TABLES_REQUEST = 0xD598 + GET_M_QUEST_PLAYER_TRACE_TABLES_RESPONSE = 0xD599 + GET_M_QUEST_STILLS_REQUEST = 0xD59A + GET_M_QUEST_STILLS_RESPONSE = 0xD59B + GET_M_GASHAS_REQUEST = 0xD59C + GET_M_GASHAS_RESPONSE = 0xD59D + GET_M_GASHA_HEADERS_REQUEST = 0xD59E + GET_M_GASHA_HEADERS_RESPONSE = 0xD59F + GET_M_GASHA_LOTTERY_RARITIES_REQUEST = 0xD5A0 + GET_M_GASHA_LOTTERY_RARITIES_RESPONSE = 0xD5A1 + GET_M_GASHA_PRIZES_REQUEST = 0xD5A2 + GET_M_GASHA_PRIZES_RESPONSE = 0xD5A3 + GET_M_COMEBACK_EVENTS_REQUEST = 0xD5A4 + GET_M_COMEBACK_EVENTS_RESPONSE = 0xD5A5 + GET_M_AD_BANNERS_REQUEST = 0xD5A6 + GET_M_AD_BANNERS_RESPONSE = 0xD5A7 + GET_M_EVENTS_REQUEST = 0xD5A8 + GET_M_EVENTS_RESPONSE = 0xD5A9 + GET_M_TREASURE_HUNTS_REQUEST = 0xD5AA + GET_M_TREASURE_HUNTS_RESPONSE = 0xD5AB + GET_M_TREASURE_HUNT_WHOLE_TASKS_REQUEST = 0xD5AC + GET_M_TREASURE_HUNT_WHOLE_TASKS_RESPONSE = 0xD5AD + GET_M_TREASURE_HUNT_INDIVIDUAL_TASKS_REQUEST = 0xD5AE + GET_M_TREASURE_HUNT_INDIVIDUAL_TASKS_RESPONSE = 0xD5AF + GET_M_TREASURE_HUNT_SPECIAL_EFFECTS_REQUEST = 0xD5B0 + GET_M_TREASURE_HUNT_SPECIAL_EFFECTS_RESPONSE = 0xD5B1 + GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_COMMON_REWARDS_REQUEST = 0xD5B2 + GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_COMMON_REWARDS_RESPONSE = 0xD5B3 + GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_TITLES_REQUEST = 0xD5B4 + GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_TITLES_RESPONSE = 0xD5B5 + GET_M_TREASURE_HUNT_TASK_TEXTS_REQUEST = 0xD5B6 + GET_M_TREASURE_HUNT_TASK_TEXTS_RESPONSE = 0xD5B7 + GET_M_BNID_SERIAL_CODES_REQUEST = 0xD5B8 + GET_M_BNID_SERIAL_CODES_RESPONSE = 0xD5B9 + GET_M_BNID_SERIAL_CODE_REWARDS_REQUEST = 0xD5BA + GET_M_BNID_SERIAL_CODE_REWARDS_RESPONSE = 0xD5BB + GET_M_SUPPORT_LOG_REQUEST = 0xD5BC + GET_M_SUPPORT_LOG_RESPONSE = 0xD5BD + GET_M_SUPPORT_LOG_TYPES_REQUEST = 0xD5BE + GET_M_SUPPORT_LOG_TYPES_RESPONSE = 0xD5BF + GET_M_EPISODE_APPENDS_REQUEST = 0xD5C0 + GET_M_EPISODE_APPENDS_RESPONSE = 0xD5C1 + GET_M_QUEST_DEFRAG_MATCH_QUESTS_REQUEST = 0xD5C2 + GET_M_QUEST_DEFRAG_MATCH_QUESTS_RESPONSE = 0xD5C3 + GET_M_QUEST_DEFRAG_MATCH_QUEST_BOSS_TABLES_REQUEST = 0xD5C4 + GET_M_QUEST_DEFRAG_MATCH_QUEST_BOSS_TABLES_RESPONSE = 0xD5C5 + GET_M_DEFRAG_MATCHES_REQUEST = 0xD5C6 + GET_M_DEFRAG_MATCHES_RESPONSE = 0xD5C7 + GET_M_DEFRAG_MATCH_SEED_REQUEST = 0xD5C8 + GET_M_DEFRAG_MATCH_SEED_RESPONSE = 0xD5C9 + GET_M_DEFRAG_MATCH_SPECIAL_EFFECTS_REQUEST = 0xD5CA + GET_M_DEFRAG_MATCH_SPECIAL_EFFECTS_RESPONSE = 0xD5CB + GET_M_DEFRAG_MATCH_GRADES_REQUEST = 0xD5CC + GET_M_DEFRAG_MATCH_GRADES_RESPONSE = 0xD5CD + GET_M_DEFRAG_MATCH_CPU_UNITS_REQUEST = 0xD5CE + GET_M_DEFRAG_MATCH_CPU_UNITS_RESPONSE = 0xD5CF + GET_M_DEFRAG_MATCH_CPU_SUPPORT_LOGS_REQUEST = 0xD5D0 + GET_M_DEFRAG_MATCH_CPU_SUPPORT_LOGS_RESPONSE = 0xD5D1 + GET_M_DEFRAG_MATCH_PERIOD_BONUSES_REQUEST = 0xD5D2 + GET_M_DEFRAG_MATCH_PERIOD_BONUSES_RESPONSE = 0xD5D3 + GET_M_DEFRAG_MATCH_RANDOM_BONUS_TABLES_REQUEST = 0xD5D4 + GET_M_DEFRAG_MATCH_RANDOM_BONUS_TABLES_RESPONSE = 0xD5D5 + GET_M_DEFRAG_MATCH_RANDOM_BONUS_CONDITIONS_REQUEST = 0xD5D6 + GET_M_DEFRAG_MATCH_RANDOM_BONUS_CONDITIONS_RESPONSE = 0xD5D7 + GET_M_DEFRAG_MATCH_RARE_DROPS_REQUEST = 0xD5D8 + GET_M_DEFRAG_MATCH_RARE_DROPS_RESPONSE = 0xD5D9 + GET_M_YUI_MEDAL_SHOPS_REQUEST = 0xD5DA + GET_M_YUI_MEDAL_SHOPS_RESPONSE = 0xD5DB + GET_M_YUI_MEDAL_SHOP_ITEMS_REQUEST = 0xD5DC + GET_M_YUI_MEDAL_SHOP_ITEMS_RESPONSE = 0xD5DD + GET_M_EVENT_SCENES_REQUEST = 0xD5DE + GET_M_EVENT_SCENES_RESPONSE = 0xD5DF + GET_M_GENERIC_CAMPAIGN_PERIODS_REQUEST = 0xD5E0 + GET_M_GENERIC_CAMPAIGN_PERIODS_RESPONSE = 0xD5E1 + GET_M_BEGINNER_MISSIONS_REQUEST = 0xD5E2 + GET_M_BEGINNER_MISSIONS_RESPONSE = 0xD5E3 + GET_M_BEGINNER_MISSION_CONDITIONS_REQUEST = 0xD5E4 + GET_M_BEGINNER_MISSION_CONDITIONS_RESPONSE = 0xD5E5 + GET_M_BEGINNER_MISSION_REWARDS_REQUEST = 0xD5E6 + GET_M_BEGINNER_MISSION_REWARDS_RESPONSE = 0xD5E7 + GET_M_BEGINNER_MISSION_SEAT_CONDITIONS_REQUEST = 0xD5E8 + GET_M_BEGINNER_MISSION_SEAT_CONDITIONS_RESPONSE = 0xD5E9 + GET_M_BEGINNER_MISSION_SEAT_REWARDS_REQUEST = 0xD5EA + GET_M_BEGINNER_MISSION_SEAT_REWARDS_RESPONSE = 0xD5EB + GET_M_EVENT_ITEMS_REQUEST = 0xD5EC + GET_M_EVENT_ITEMS_RESPONSE = 0xD5ED + GET_M_EVENT_MONSTERS_REQUEST = 0xD5EE + GET_M_EVENT_MONSTERS_RESPONSE = 0xD5EF + GET_M_YUI_MEDAL_BONUSES_REQUEST = 0xD5F0 + GET_M_YUI_MEDAL_BONUSES_RESPONSE = 0xD5F1 + GET_M_YUI_MEDAL_BONUS_CONDITIONS_REQUEST = 0xD5F2 + GET_M_YUI_MEDAL_BONUS_CONDITIONS_RESPONSE = 0xD5F3 + GET_M_GASHA_MEDALS_REQUEST = 0xD5F4 + GET_M_GASHA_MEDALS_RESPONSE = 0xD5F5 + GET_M_GASHA_MEDAL_TYPES_REQUEST = 0xD5F6 + GET_M_GASHA_MEDAL_TYPES_RESPONSE = 0xD5F7 + GET_M_GASHA_MEDAL_SETTINGS_REQUEST = 0xD5F8 + GET_M_GASHA_MEDAL_SETTINGS_RESPONSE = 0xD5F9 + GET_M_GASHA_MEDAL_BONUSES_REQUEST = 0xD5FA + GET_M_GASHA_MEDAL_BONUSES_RESPONSE = 0xD5FB + GET_M_GASHA_MEDAL_SHOPS_REQUEST = 0xD5FC + GET_M_GASHA_MEDAL_SHOPS_RESPONSE = 0xD5FD + GET_M_GASHA_MEDAL_SHOP_ITEMS_REQUEST = 0xD5FE + GET_M_GASHA_MEDAL_SHOP_ITEMS_RESPONSE = 0xD5FF + GET_M_RES_EARN_CAMPAIGN_APPLICATIONS_REQUEST = 0xD600 + GET_M_RES_EARN_CAMPAIGN_APPLICATIONS_RESPONSE = 0xD601 + GET_M_RES_EARN_CAMPAIGN_APPLICATION_PRODUCTS_REQUEST = 0xD602 + GET_M_RES_EARN_CAMPAIGN_APPLICATION_PRODUCTS_RESPONSE = 0xD603 + GET_M_RES_EARN_CAMPAIGN_SHOPS_REQUEST = 0xD604 + GET_M_RES_EARN_CAMPAIGN_SHOPS_RESPONSE = 0xD605 + GET_M_RES_EARN_CAMPAIGN_SHOP_ITEMS_REQUEST = 0xD606 + GET_M_RES_EARN_CAMPAIGN_SHOP_ITEMS_RESPONSE = 0xD607 + GET_M_PAYING_YUI_MEDAL_BONUSES_REQUEST = 0xD608 + GET_M_PAYING_YUI_MEDAL_BONUSES_RESPONSE = 0xD609 + GET_M_AC_LOGIN_BONUSES_REQUEST = 0xD60A + GET_M_AC_LOGIN_BONUSES_RESPONSE = 0xD60B + GET_M_PLAY_CAMPAIGNS_REQUEST = 0xD60C + GET_M_PLAY_CAMPAIGNS_RESPONSE = 0xD60D + GET_M_PLAY_CAMPAIGN_REWARDS_REQUEST = 0xD60E + GET_M_PLAY_CAMPAIGN_REWARDS_RESPONSE = 0xD60F + GET_M_GASHA_FREE_CAMPAIGNS_REQUEST = 0xD610 + GET_M_GASHA_FREE_CAMPAIGNS_RESPONSE = 0xD611 + GET_M_QUEST_DROP_BOOST_CAMPAIGNS_REQUEST = 0xD612 + GET_M_QUEST_DROP_BOOST_CAMPAIGNS_RESPONSE = 0xD613 + GET_M_FIRST_TICKET_PURCHASE_CAMPAIGNS_REQUEST = 0xD614 + GET_M_FIRST_TICKET_PURCHASE_CAMPAIGNS_RESPONSE = 0xD615 + GET_M_LINKED_SITE_REG_CAMPAIGNS_REQUEST = 0xD616 + GET_M_LINKED_SITE_REG_CAMPAIGNS_RESPONSE = 0xD617 + GET_M_LINKED_SITE_REG_CAMPAIGN_REWARDS_REQUEST = 0xD618 + GET_M_LINKED_SITE_REG_CAMPAIGN_REWARDS_RESPONSE = 0xD619 diff --git a/titles/sao/data/1/AcLoginBonuses.csv b/titles/sao/data/1/AcLoginBonuses.csv new file mode 100644 index 0000000..166c5b2 Binary files /dev/null and b/titles/sao/data/1/AcLoginBonuses.csv differ diff --git a/titles/sao/data/1/AdBanners.csv b/titles/sao/data/1/AdBanners.csv new file mode 100644 index 0000000..52400ff Binary files /dev/null and b/titles/sao/data/1/AdBanners.csv differ diff --git a/titles/sao/data/1/AppointLeaderEffect.csv b/titles/sao/data/1/AppointLeaderEffect.csv new file mode 100644 index 0000000..d7079fd Binary files /dev/null and b/titles/sao/data/1/AppointLeaderEffect.csv differ diff --git a/titles/sao/data/1/AppointLeaderEffectType.csv b/titles/sao/data/1/AppointLeaderEffectType.csv new file mode 100644 index 0000000..754a94e Binary files /dev/null and b/titles/sao/data/1/AppointLeaderEffectType.csv differ diff --git a/titles/sao/data/1/AppointLeaderParam.csv b/titles/sao/data/1/AppointLeaderParam.csv new file mode 100644 index 0000000..173f312 Binary files /dev/null and b/titles/sao/data/1/AppointLeaderParam.csv differ diff --git a/titles/sao/data/1/Awakening.csv b/titles/sao/data/1/Awakening.csv new file mode 100644 index 0000000..05d1bd0 Binary files /dev/null and b/titles/sao/data/1/Awakening.csv differ diff --git a/titles/sao/data/1/BattleCamera.csv b/titles/sao/data/1/BattleCamera.csv new file mode 100644 index 0000000..d4410da Binary files /dev/null and b/titles/sao/data/1/BattleCamera.csv differ diff --git a/titles/sao/data/1/BeginnerMissionConditions.csv b/titles/sao/data/1/BeginnerMissionConditions.csv new file mode 100644 index 0000000..7b7c9af Binary files /dev/null and b/titles/sao/data/1/BeginnerMissionConditions.csv differ diff --git a/titles/sao/data/1/BeginnerMissionRewards.csv b/titles/sao/data/1/BeginnerMissionRewards.csv new file mode 100644 index 0000000..8e24ce5 Binary files /dev/null and b/titles/sao/data/1/BeginnerMissionRewards.csv differ diff --git a/titles/sao/data/1/BeginnerMissionSeatConditions.csv b/titles/sao/data/1/BeginnerMissionSeatConditions.csv new file mode 100644 index 0000000..ad97ac7 Binary files /dev/null and b/titles/sao/data/1/BeginnerMissionSeatConditions.csv differ diff --git a/titles/sao/data/1/BeginnerMissionSeatRewards.csv b/titles/sao/data/1/BeginnerMissionSeatRewards.csv new file mode 100644 index 0000000..c306a39 Binary files /dev/null and b/titles/sao/data/1/BeginnerMissionSeatRewards.csv differ diff --git a/titles/sao/data/1/BeginnerMissions.csv b/titles/sao/data/1/BeginnerMissions.csv new file mode 100644 index 0000000..56cbf19 Binary files /dev/null and b/titles/sao/data/1/BeginnerMissions.csv differ diff --git a/titles/sao/data/1/BnidSerialCodeRewards.csv b/titles/sao/data/1/BnidSerialCodeRewards.csv new file mode 100644 index 0000000..556883d Binary files /dev/null and b/titles/sao/data/1/BnidSerialCodeRewards.csv differ diff --git a/titles/sao/data/1/BnidSerialCodes.csv b/titles/sao/data/1/BnidSerialCodes.csv new file mode 100644 index 0000000..5eff1e9 Binary files /dev/null and b/titles/sao/data/1/BnidSerialCodes.csv differ diff --git a/titles/sao/data/1/BossAttack.csv b/titles/sao/data/1/BossAttack.csv new file mode 100644 index 0000000..2ae2572 Binary files /dev/null and b/titles/sao/data/1/BossAttack.csv differ diff --git a/titles/sao/data/1/BuffItem.csv b/titles/sao/data/1/BuffItem.csv new file mode 100644 index 0000000..b4f3fcd Binary files /dev/null and b/titles/sao/data/1/BuffItem.csv differ diff --git a/titles/sao/data/1/BuyTicket.csv b/titles/sao/data/1/BuyTicket.csv new file mode 100644 index 0000000..58f0c48 Binary files /dev/null and b/titles/sao/data/1/BuyTicket.csv differ diff --git a/titles/sao/data/1/Cap.csv b/titles/sao/data/1/Cap.csv new file mode 100644 index 0000000..2d57742 Binary files /dev/null and b/titles/sao/data/1/Cap.csv differ diff --git a/titles/sao/data/1/Chara.csv b/titles/sao/data/1/Chara.csv new file mode 100644 index 0000000..00ae23e Binary files /dev/null and b/titles/sao/data/1/Chara.csv differ diff --git a/titles/sao/data/1/CharaAttack.csv b/titles/sao/data/1/CharaAttack.csv new file mode 100644 index 0000000..95d3fe4 Binary files /dev/null and b/titles/sao/data/1/CharaAttack.csv differ diff --git a/titles/sao/data/1/CharaComment.csv b/titles/sao/data/1/CharaComment.csv new file mode 100644 index 0000000..2836383 Binary files /dev/null and b/titles/sao/data/1/CharaComment.csv differ diff --git a/titles/sao/data/1/CharaFriendlyRank.csv b/titles/sao/data/1/CharaFriendlyRank.csv new file mode 100644 index 0000000..0e39ed1 Binary files /dev/null and b/titles/sao/data/1/CharaFriendlyRank.csv differ diff --git a/titles/sao/data/1/ChatEventStory.csv b/titles/sao/data/1/ChatEventStory.csv new file mode 100644 index 0000000..3616cc0 Binary files /dev/null and b/titles/sao/data/1/ChatEventStory.csv differ diff --git a/titles/sao/data/1/ChatMainStory.csv b/titles/sao/data/1/ChatMainStory.csv new file mode 100644 index 0000000..542d3b6 Binary files /dev/null and b/titles/sao/data/1/ChatMainStory.csv differ diff --git a/titles/sao/data/1/ChatSideStory.csv b/titles/sao/data/1/ChatSideStory.csv new file mode 100644 index 0000000..e6ad4f0 Binary files /dev/null and b/titles/sao/data/1/ChatSideStory.csv differ diff --git a/titles/sao/data/1/ComebackEvent.csv b/titles/sao/data/1/ComebackEvent.csv new file mode 100644 index 0000000..2c8e775 Binary files /dev/null and b/titles/sao/data/1/ComebackEvent.csv differ diff --git a/titles/sao/data/1/CompositionEvent.csv b/titles/sao/data/1/CompositionEvent.csv new file mode 100644 index 0000000..a221ad5 Binary files /dev/null and b/titles/sao/data/1/CompositionEvent.csv differ diff --git a/titles/sao/data/1/CompositionParam.csv b/titles/sao/data/1/CompositionParam.csv new file mode 100644 index 0000000..7314322 Binary files /dev/null and b/titles/sao/data/1/CompositionParam.csv differ diff --git a/titles/sao/data/1/CostumeType.csv b/titles/sao/data/1/CostumeType.csv new file mode 100644 index 0000000..bd1ce89 Binary files /dev/null and b/titles/sao/data/1/CostumeType.csv differ diff --git a/titles/sao/data/1/DebugValidator.csv b/titles/sao/data/1/DebugValidator.csv new file mode 100644 index 0000000..edaaf66 Binary files /dev/null and b/titles/sao/data/1/DebugValidator.csv differ diff --git a/titles/sao/data/1/DefragMatchAIPattern.csv b/titles/sao/data/1/DefragMatchAIPattern.csv new file mode 100644 index 0000000..862581a Binary files /dev/null and b/titles/sao/data/1/DefragMatchAIPattern.csv differ diff --git a/titles/sao/data/1/DefragMatchBossTable.csv b/titles/sao/data/1/DefragMatchBossTable.csv new file mode 100644 index 0000000..d5b19ee Binary files /dev/null and b/titles/sao/data/1/DefragMatchBossTable.csv differ diff --git a/titles/sao/data/1/DefragMatchChat.csv b/titles/sao/data/1/DefragMatchChat.csv new file mode 100644 index 0000000..6aa812e Binary files /dev/null and b/titles/sao/data/1/DefragMatchChat.csv differ diff --git a/titles/sao/data/1/DefragMatchCpuSupportLogs.csv b/titles/sao/data/1/DefragMatchCpuSupportLogs.csv new file mode 100644 index 0000000..62746e2 Binary files /dev/null and b/titles/sao/data/1/DefragMatchCpuSupportLogs.csv differ diff --git a/titles/sao/data/1/DefragMatchCpuUnits.csv b/titles/sao/data/1/DefragMatchCpuUnits.csv new file mode 100644 index 0000000..c035b67 Binary files /dev/null and b/titles/sao/data/1/DefragMatchCpuUnits.csv differ diff --git a/titles/sao/data/1/DefragMatchGrade.csv b/titles/sao/data/1/DefragMatchGrade.csv new file mode 100644 index 0000000..b81e7bf Binary files /dev/null and b/titles/sao/data/1/DefragMatchGrade.csv differ diff --git a/titles/sao/data/1/DefragMatchPeriodBonuses.csv b/titles/sao/data/1/DefragMatchPeriodBonuses.csv new file mode 100644 index 0000000..eb348f5 Binary files /dev/null and b/titles/sao/data/1/DefragMatchPeriodBonuses.csv differ diff --git a/titles/sao/data/1/DefragMatchQuest.csv b/titles/sao/data/1/DefragMatchQuest.csv new file mode 100644 index 0000000..89cabc0 Binary files /dev/null and b/titles/sao/data/1/DefragMatchQuest.csv differ diff --git a/titles/sao/data/1/DefragMatchRandomBonusConditions.csv b/titles/sao/data/1/DefragMatchRandomBonusConditions.csv new file mode 100644 index 0000000..47c2e01 Binary files /dev/null and b/titles/sao/data/1/DefragMatchRandomBonusConditions.csv differ diff --git a/titles/sao/data/1/DefragMatchRandomBonusTables.csv b/titles/sao/data/1/DefragMatchRandomBonusTables.csv new file mode 100644 index 0000000..6c8bf15 Binary files /dev/null and b/titles/sao/data/1/DefragMatchRandomBonusTables.csv differ diff --git a/titles/sao/data/1/DefragMatchRareDrops.csv b/titles/sao/data/1/DefragMatchRareDrops.csv new file mode 100644 index 0000000..50ec74d Binary files /dev/null and b/titles/sao/data/1/DefragMatchRareDrops.csv differ diff --git a/titles/sao/data/1/DefragMatchSeed.csv b/titles/sao/data/1/DefragMatchSeed.csv new file mode 100644 index 0000000..a5903cb Binary files /dev/null and b/titles/sao/data/1/DefragMatchSeed.csv differ diff --git a/titles/sao/data/1/DefragMatchSpecialEffects.csv b/titles/sao/data/1/DefragMatchSpecialEffects.csv new file mode 100644 index 0000000..48244f2 Binary files /dev/null and b/titles/sao/data/1/DefragMatchSpecialEffects.csv differ diff --git a/titles/sao/data/1/DefragMatchs.csv b/titles/sao/data/1/DefragMatchs.csv new file mode 100644 index 0000000..5115b35 Binary files /dev/null and b/titles/sao/data/1/DefragMatchs.csv differ diff --git a/titles/sao/data/1/Enemy.csv b/titles/sao/data/1/Enemy.csv new file mode 100644 index 0000000..8f5323f Binary files /dev/null and b/titles/sao/data/1/Enemy.csv differ diff --git a/titles/sao/data/1/EnemyKind.csv b/titles/sao/data/1/EnemyKind.csv new file mode 100644 index 0000000..e969dbf Binary files /dev/null and b/titles/sao/data/1/EnemyKind.csv differ diff --git a/titles/sao/data/1/EnemySet.csv b/titles/sao/data/1/EnemySet.csv new file mode 100644 index 0000000..771ed20 Binary files /dev/null and b/titles/sao/data/1/EnemySet.csv differ diff --git a/titles/sao/data/1/Episode.csv b/titles/sao/data/1/Episode.csv new file mode 100644 index 0000000..8cb8c6d Binary files /dev/null and b/titles/sao/data/1/Episode.csv differ diff --git a/titles/sao/data/1/EpisodeAppends.csv b/titles/sao/data/1/EpisodeAppends.csv new file mode 100644 index 0000000..665d16f Binary files /dev/null and b/titles/sao/data/1/EpisodeAppends.csv differ diff --git a/titles/sao/data/1/EpisodeChapter.csv b/titles/sao/data/1/EpisodeChapter.csv new file mode 100644 index 0000000..834419f Binary files /dev/null and b/titles/sao/data/1/EpisodeChapter.csv differ diff --git a/titles/sao/data/1/EpisodePart.csv b/titles/sao/data/1/EpisodePart.csv new file mode 100644 index 0000000..97134fa Binary files /dev/null and b/titles/sao/data/1/EpisodePart.csv differ diff --git a/titles/sao/data/Equipment.csv b/titles/sao/data/1/Equipment.csv similarity index 86% rename from titles/sao/data/Equipment.csv rename to titles/sao/data/1/Equipment.csv index a7fd263..96fb7cb 100644 Binary files a/titles/sao/data/Equipment.csv and b/titles/sao/data/1/Equipment.csv differ diff --git a/titles/sao/data/1/EquipmentLevel.csv b/titles/sao/data/1/EquipmentLevel.csv new file mode 100644 index 0000000..68f4e43 Binary files /dev/null and b/titles/sao/data/1/EquipmentLevel.csv differ diff --git a/titles/sao/data/1/Event.csv b/titles/sao/data/1/Event.csv new file mode 100644 index 0000000..18a37ed Binary files /dev/null and b/titles/sao/data/1/Event.csv differ diff --git a/titles/sao/data/1/EventItems.csv b/titles/sao/data/1/EventItems.csv new file mode 100644 index 0000000..3842ab5 Binary files /dev/null and b/titles/sao/data/1/EventItems.csv differ diff --git a/titles/sao/data/1/EventMonsters.csv b/titles/sao/data/1/EventMonsters.csv new file mode 100644 index 0000000..d756ff5 Binary files /dev/null and b/titles/sao/data/1/EventMonsters.csv differ diff --git a/titles/sao/data/1/EventScenes.csv b/titles/sao/data/1/EventScenes.csv new file mode 100644 index 0000000..d5cde61 Binary files /dev/null and b/titles/sao/data/1/EventScenes.csv differ diff --git a/titles/sao/data/1/ExBonusCondition.csv b/titles/sao/data/1/ExBonusCondition.csv new file mode 100644 index 0000000..d6f0f9a Binary files /dev/null and b/titles/sao/data/1/ExBonusCondition.csv differ diff --git a/titles/sao/data/1/ExBonusTable.csv b/titles/sao/data/1/ExBonusTable.csv new file mode 100644 index 0000000..d85ffac Binary files /dev/null and b/titles/sao/data/1/ExBonusTable.csv differ diff --git a/titles/sao/data/1/ExTowerQuests.csv b/titles/sao/data/1/ExTowerQuests.csv new file mode 100644 index 0000000..0b8a944 Binary files /dev/null and b/titles/sao/data/1/ExTowerQuests.csv differ diff --git a/titles/sao/data/1/ExTowers.csv b/titles/sao/data/1/ExTowers.csv new file mode 100644 index 0000000..7b371e9 Binary files /dev/null and b/titles/sao/data/1/ExTowers.csv differ diff --git a/titles/sao/data/1/FirstTicketPurchaseCampaigns.csv b/titles/sao/data/1/FirstTicketPurchaseCampaigns.csv new file mode 100644 index 0000000..21a19f6 Binary files /dev/null and b/titles/sao/data/1/FirstTicketPurchaseCampaigns.csv differ diff --git a/titles/sao/data/1/FixedFormChat.csv b/titles/sao/data/1/FixedFormChat.csv new file mode 100644 index 0000000..2433d02 Binary files /dev/null and b/titles/sao/data/1/FixedFormChat.csv differ diff --git a/titles/sao/data/1/FixedFormChatText.csv b/titles/sao/data/1/FixedFormChatText.csv new file mode 100644 index 0000000..72e3f1b Binary files /dev/null and b/titles/sao/data/1/FixedFormChatText.csv differ diff --git a/titles/sao/data/1/Fragment.csv b/titles/sao/data/1/Fragment.csv new file mode 100644 index 0000000..5273d23 Binary files /dev/null and b/titles/sao/data/1/Fragment.csv differ diff --git a/titles/sao/data/1/GamePlayPrice.csv b/titles/sao/data/1/GamePlayPrice.csv new file mode 100644 index 0000000..5104fe9 Binary files /dev/null and b/titles/sao/data/1/GamePlayPrice.csv differ diff --git a/titles/sao/data/1/GashaFreeCampaigns.csv b/titles/sao/data/1/GashaFreeCampaigns.csv new file mode 100644 index 0000000..145be89 Binary files /dev/null and b/titles/sao/data/1/GashaFreeCampaigns.csv differ diff --git a/titles/sao/data/1/GashaMedalBonuses.csv b/titles/sao/data/1/GashaMedalBonuses.csv new file mode 100644 index 0000000..3df4b2a Binary files /dev/null and b/titles/sao/data/1/GashaMedalBonuses.csv differ diff --git a/titles/sao/data/1/GashaMedalSettings.csv b/titles/sao/data/1/GashaMedalSettings.csv new file mode 100644 index 0000000..b4a7566 Binary files /dev/null and b/titles/sao/data/1/GashaMedalSettings.csv differ diff --git a/titles/sao/data/1/GashaMedalShopItems.csv b/titles/sao/data/1/GashaMedalShopItems.csv new file mode 100644 index 0000000..66a3bac Binary files /dev/null and b/titles/sao/data/1/GashaMedalShopItems.csv differ diff --git a/titles/sao/data/GashaMedalShops.csv b/titles/sao/data/1/GashaMedalShops.csv similarity index 65% rename from titles/sao/data/GashaMedalShops.csv rename to titles/sao/data/1/GashaMedalShops.csv index a4a78dc..b92e0b4 100644 Binary files a/titles/sao/data/GashaMedalShops.csv and b/titles/sao/data/1/GashaMedalShops.csv differ diff --git a/titles/sao/data/1/GashaMedalTypes.csv b/titles/sao/data/1/GashaMedalTypes.csv new file mode 100644 index 0000000..7e6d6fd Binary files /dev/null and b/titles/sao/data/1/GashaMedalTypes.csv differ diff --git a/titles/sao/data/1/GashaMedals.csv b/titles/sao/data/1/GashaMedals.csv new file mode 100644 index 0000000..d399fbc Binary files /dev/null and b/titles/sao/data/1/GashaMedals.csv differ diff --git a/titles/sao/data/1/GenericCampaignPeriods.csv b/titles/sao/data/1/GenericCampaignPeriods.csv new file mode 100644 index 0000000..839ceb2 Binary files /dev/null and b/titles/sao/data/1/GenericCampaignPeriods.csv differ diff --git a/titles/sao/data/1/GimmickAttack.csv b/titles/sao/data/1/GimmickAttack.csv new file mode 100644 index 0000000..e177454 Binary files /dev/null and b/titles/sao/data/1/GimmickAttack.csv differ diff --git a/titles/sao/data/HeroLog.csv b/titles/sao/data/1/HeroLog.csv similarity index 94% rename from titles/sao/data/HeroLog.csv rename to titles/sao/data/1/HeroLog.csv index 6fbacac..339cce3 100644 Binary files a/titles/sao/data/HeroLog.csv and b/titles/sao/data/1/HeroLog.csv differ diff --git a/titles/sao/data/1/HeroLogLevel.csv b/titles/sao/data/1/HeroLogLevel.csv new file mode 100644 index 0000000..4a9e8fd Binary files /dev/null and b/titles/sao/data/1/HeroLogLevel.csv differ diff --git a/titles/sao/data/1/HeroLogRole.csv b/titles/sao/data/1/HeroLogRole.csv new file mode 100644 index 0000000..4b75e29 Binary files /dev/null and b/titles/sao/data/1/HeroLogRole.csv differ diff --git a/titles/sao/data/1/HeroLogTrustRank.csv b/titles/sao/data/1/HeroLogTrustRank.csv new file mode 100644 index 0000000..cfc8e0d Binary files /dev/null and b/titles/sao/data/1/HeroLogTrustRank.csv differ diff --git a/titles/sao/data/Item.csv b/titles/sao/data/1/Item.csv similarity index 79% rename from titles/sao/data/Item.csv rename to titles/sao/data/1/Item.csv index fc780af..564bf8f 100644 Binary files a/titles/sao/data/Item.csv and b/titles/sao/data/1/Item.csv differ diff --git a/titles/sao/data/1/ItemType.csv b/titles/sao/data/1/ItemType.csv new file mode 100644 index 0000000..ada5092 Binary files /dev/null and b/titles/sao/data/1/ItemType.csv differ diff --git a/titles/sao/data/1/LinkedSiteRegCampaignRewards.csv b/titles/sao/data/1/LinkedSiteRegCampaignRewards.csv new file mode 100644 index 0000000..20b34b3 Binary files /dev/null and b/titles/sao/data/1/LinkedSiteRegCampaignRewards.csv differ diff --git a/titles/sao/data/1/LinkedSiteRegCampaigns.csv b/titles/sao/data/1/LinkedSiteRegCampaigns.csv new file mode 100644 index 0000000..745a86d Binary files /dev/null and b/titles/sao/data/1/LinkedSiteRegCampaigns.csv differ diff --git a/titles/sao/data/1/MenuDisplayEnemy.csv b/titles/sao/data/1/MenuDisplayEnemy.csv new file mode 100644 index 0000000..50db0f5 Binary files /dev/null and b/titles/sao/data/1/MenuDisplayEnemy.csv differ diff --git a/titles/sao/data/1/Mission.csv b/titles/sao/data/1/Mission.csv new file mode 100644 index 0000000..bacb114 Binary files /dev/null and b/titles/sao/data/1/Mission.csv differ diff --git a/titles/sao/data/1/MissionClearText.csv b/titles/sao/data/1/MissionClearText.csv new file mode 100644 index 0000000..316e1ee Binary files /dev/null and b/titles/sao/data/1/MissionClearText.csv differ diff --git a/titles/sao/data/1/MissionDifficulty.csv b/titles/sao/data/1/MissionDifficulty.csv new file mode 100644 index 0000000..9eebb74 Binary files /dev/null and b/titles/sao/data/1/MissionDifficulty.csv differ diff --git a/titles/sao/data/1/MissionTable.csv b/titles/sao/data/1/MissionTable.csv new file mode 100644 index 0000000..b399f22 Binary files /dev/null and b/titles/sao/data/1/MissionTable.csv differ diff --git a/titles/sao/data/1/MonsterAction.csv b/titles/sao/data/1/MonsterAction.csv new file mode 100644 index 0000000..052d917 Binary files /dev/null and b/titles/sao/data/1/MonsterAction.csv differ diff --git a/titles/sao/data/1/MonsterAttack.csv b/titles/sao/data/1/MonsterAttack.csv new file mode 100644 index 0000000..06f8838 Binary files /dev/null and b/titles/sao/data/1/MonsterAttack.csv differ diff --git a/titles/sao/data/1/NavigatorChara.csv b/titles/sao/data/1/NavigatorChara.csv new file mode 100644 index 0000000..e1a42ac Binary files /dev/null and b/titles/sao/data/1/NavigatorChara.csv differ diff --git a/titles/sao/data/1/NavigatorComment.csv b/titles/sao/data/1/NavigatorComment.csv new file mode 100644 index 0000000..5651a72 Binary files /dev/null and b/titles/sao/data/1/NavigatorComment.csv differ diff --git a/titles/sao/data/1/PayingYuiMedalBonuses.csv b/titles/sao/data/1/PayingYuiMedalBonuses.csv new file mode 100644 index 0000000..91a77c4 Binary files /dev/null and b/titles/sao/data/1/PayingYuiMedalBonuses.csv differ diff --git a/titles/sao/data/1/PlayCampaignRewards.csv b/titles/sao/data/1/PlayCampaignRewards.csv new file mode 100644 index 0000000..627fbee Binary files /dev/null and b/titles/sao/data/1/PlayCampaignRewards.csv differ diff --git a/titles/sao/data/1/PlayCampaigns.csv b/titles/sao/data/1/PlayCampaigns.csv new file mode 100644 index 0000000..96ec39c Binary files /dev/null and b/titles/sao/data/1/PlayCampaigns.csv differ diff --git a/titles/sao/data/1/PlayerRank.csv b/titles/sao/data/1/PlayerRank.csv new file mode 100644 index 0000000..fe95c74 Binary files /dev/null and b/titles/sao/data/1/PlayerRank.csv differ diff --git a/titles/sao/data/1/PlayerTraceTable.csv b/titles/sao/data/1/PlayerTraceTable.csv new file mode 100644 index 0000000..1d36d9a Binary files /dev/null and b/titles/sao/data/1/PlayerTraceTable.csv differ diff --git a/titles/sao/data/1/Property.csv b/titles/sao/data/1/Property.csv new file mode 100644 index 0000000..d7d8c30 Binary files /dev/null and b/titles/sao/data/1/Property.csv differ diff --git a/titles/sao/data/1/PropertyTable.csv b/titles/sao/data/1/PropertyTable.csv new file mode 100644 index 0000000..17a29bc Binary files /dev/null and b/titles/sao/data/1/PropertyTable.csv differ diff --git a/titles/sao/data/1/PropertyType.csv b/titles/sao/data/1/PropertyType.csv new file mode 100644 index 0000000..d028b0e Binary files /dev/null and b/titles/sao/data/1/PropertyType.csv differ diff --git a/titles/sao/data/1/QuestDropBoostCampaigns.csv b/titles/sao/data/1/QuestDropBoostCampaigns.csv new file mode 100644 index 0000000..347b692 Binary files /dev/null and b/titles/sao/data/1/QuestDropBoostCampaigns.csv differ diff --git a/titles/sao/data/1/QuestEpisodeAppendRewards.csv b/titles/sao/data/1/QuestEpisodeAppendRewards.csv new file mode 100644 index 0000000..26efbb1 Binary files /dev/null and b/titles/sao/data/1/QuestEpisodeAppendRewards.csv differ diff --git a/titles/sao/data/1/QuestExistUnit.csv b/titles/sao/data/1/QuestExistUnit.csv new file mode 100644 index 0000000..355b290 Binary files /dev/null and b/titles/sao/data/1/QuestExistUnit.csv differ diff --git a/titles/sao/data/1/QuestParam.csv b/titles/sao/data/1/QuestParam.csv new file mode 100644 index 0000000..705a3a3 Binary files /dev/null and b/titles/sao/data/1/QuestParam.csv differ diff --git a/titles/sao/data/1/QuestRareDrop.csv b/titles/sao/data/1/QuestRareDrop.csv new file mode 100644 index 0000000..0d9c8ce Binary files /dev/null and b/titles/sao/data/1/QuestRareDrop.csv differ diff --git a/titles/sao/data/QuestScene.csv b/titles/sao/data/1/QuestScene.csv similarity index 89% rename from titles/sao/data/QuestScene.csv rename to titles/sao/data/1/QuestScene.csv index 3e6b402..3307404 100644 Binary files a/titles/sao/data/QuestScene.csv and b/titles/sao/data/1/QuestScene.csv differ diff --git a/titles/sao/data/1/QuestSpecialRareDropSettings.csv b/titles/sao/data/1/QuestSpecialRareDropSettings.csv new file mode 100644 index 0000000..ed7ced5 Binary files /dev/null and b/titles/sao/data/1/QuestSpecialRareDropSettings.csv differ diff --git a/titles/sao/data/1/QuestSpecialRareDrops.csv b/titles/sao/data/1/QuestSpecialRareDrops.csv new file mode 100644 index 0000000..623a264 Binary files /dev/null and b/titles/sao/data/1/QuestSpecialRareDrops.csv differ diff --git a/titles/sao/data/1/QuestTutorial.csv b/titles/sao/data/1/QuestTutorial.csv new file mode 100644 index 0000000..ea45ad9 Binary files /dev/null and b/titles/sao/data/1/QuestTutorial.csv differ diff --git a/titles/sao/data/1/Rarity.csv b/titles/sao/data/1/Rarity.csv new file mode 100644 index 0000000..40f6f41 Binary files /dev/null and b/titles/sao/data/1/Rarity.csv differ diff --git a/titles/sao/data/1/ResEarnCampaignShopItems.csv b/titles/sao/data/1/ResEarnCampaignShopItems.csv new file mode 100644 index 0000000..e2bc6fd Binary files /dev/null and b/titles/sao/data/1/ResEarnCampaignShopItems.csv differ diff --git a/titles/sao/data/1/ResEarnCampaignShops.csv b/titles/sao/data/1/ResEarnCampaignShops.csv new file mode 100644 index 0000000..9413883 Binary files /dev/null and b/titles/sao/data/1/ResEarnCampaignShops.csv differ diff --git a/titles/sao/data/1/RewardSet.csv b/titles/sao/data/1/RewardSet.csv new file mode 100644 index 0000000..7858328 Binary files /dev/null and b/titles/sao/data/1/RewardSet.csv differ diff --git a/titles/sao/data/1/RewardTable.csv b/titles/sao/data/1/RewardTable.csv new file mode 100644 index 0000000..50ea22b Binary files /dev/null and b/titles/sao/data/1/RewardTable.csv differ diff --git a/titles/sao/data/1/SideQuest.csv b/titles/sao/data/1/SideQuest.csv new file mode 100644 index 0000000..970ce37 Binary files /dev/null and b/titles/sao/data/1/SideQuest.csv differ diff --git a/titles/sao/data/1/Skill.csv b/titles/sao/data/1/Skill.csv new file mode 100644 index 0000000..6fe8939 Binary files /dev/null and b/titles/sao/data/1/Skill.csv differ diff --git a/titles/sao/data/1/SkillCutInTextIndex.csv b/titles/sao/data/1/SkillCutInTextIndex.csv new file mode 100644 index 0000000..982ea23 Binary files /dev/null and b/titles/sao/data/1/SkillCutInTextIndex.csv differ diff --git a/titles/sao/data/1/SkillCutin.csv b/titles/sao/data/1/SkillCutin.csv new file mode 100644 index 0000000..ccae1b0 Binary files /dev/null and b/titles/sao/data/1/SkillCutin.csv differ diff --git a/titles/sao/data/1/SkillLevel.csv b/titles/sao/data/1/SkillLevel.csv new file mode 100644 index 0000000..2ed21d7 Binary files /dev/null and b/titles/sao/data/1/SkillLevel.csv differ diff --git a/titles/sao/data/1/SkillTable.csv b/titles/sao/data/1/SkillTable.csv new file mode 100644 index 0000000..4273c80 Binary files /dev/null and b/titles/sao/data/1/SkillTable.csv differ diff --git a/titles/sao/data/1/SoundBgm.csv b/titles/sao/data/1/SoundBgm.csv new file mode 100644 index 0000000..f43eedc Binary files /dev/null and b/titles/sao/data/1/SoundBgm.csv differ diff --git a/titles/sao/data/1/SoundSe.csv b/titles/sao/data/1/SoundSe.csv new file mode 100644 index 0000000..30a1699 Binary files /dev/null and b/titles/sao/data/1/SoundSe.csv differ diff --git a/titles/sao/data/1/SoundUi.csv b/titles/sao/data/1/SoundUi.csv new file mode 100644 index 0000000..0b2aa14 Binary files /dev/null and b/titles/sao/data/1/SoundUi.csv differ diff --git a/titles/sao/data/1/SoundVoice.csv b/titles/sao/data/1/SoundVoice.csv new file mode 100644 index 0000000..a4900b9 Binary files /dev/null and b/titles/sao/data/1/SoundVoice.csv differ diff --git a/titles/sao/data/1/Sound_SkillCutinVoice.csv b/titles/sao/data/1/Sound_SkillCutinVoice.csv new file mode 100644 index 0000000..a169923 Binary files /dev/null and b/titles/sao/data/1/Sound_SkillCutinVoice.csv differ diff --git a/titles/sao/data/1/SubStateMonster.csv b/titles/sao/data/1/SubStateMonster.csv new file mode 100644 index 0000000..b800149 Binary files /dev/null and b/titles/sao/data/1/SubStateMonster.csv differ diff --git a/titles/sao/data/SupportLog.csv b/titles/sao/data/1/SupportLog.csv similarity index 95% rename from titles/sao/data/SupportLog.csv rename to titles/sao/data/1/SupportLog.csv index 92295b1..1c64be5 100644 Binary files a/titles/sao/data/SupportLog.csv and b/titles/sao/data/1/SupportLog.csv differ diff --git a/titles/sao/data/1/SupportLogType.csv b/titles/sao/data/1/SupportLogType.csv new file mode 100644 index 0000000..fda48a1 Binary files /dev/null and b/titles/sao/data/1/SupportLogType.csv differ diff --git a/titles/sao/data/1/SynchroSkill.csv b/titles/sao/data/1/SynchroSkill.csv new file mode 100644 index 0000000..311b6dd Binary files /dev/null and b/titles/sao/data/1/SynchroSkill.csv differ diff --git a/titles/sao/data/1/Tips.csv b/titles/sao/data/1/Tips.csv new file mode 100644 index 0000000..8cf361c Binary files /dev/null and b/titles/sao/data/1/Tips.csv differ diff --git a/titles/sao/data/1/Title.csv b/titles/sao/data/1/Title.csv new file mode 100644 index 0000000..1218ddf Binary files /dev/null and b/titles/sao/data/1/Title.csv differ diff --git a/titles/sao/data/1/TreasureHuntTaskText.csv b/titles/sao/data/1/TreasureHuntTaskText.csv new file mode 100644 index 0000000..b737d5d Binary files /dev/null and b/titles/sao/data/1/TreasureHuntTaskText.csv differ diff --git a/titles/sao/data/1/TrialTower.csv b/titles/sao/data/1/TrialTower.csv new file mode 100644 index 0000000..ff51b87 Binary files /dev/null and b/titles/sao/data/1/TrialTower.csv differ diff --git a/titles/sao/data/1/UnanalyzedLogGrade.csv b/titles/sao/data/1/UnanalyzedLogGrade.csv new file mode 100644 index 0000000..b0bf28a Binary files /dev/null and b/titles/sao/data/1/UnanalyzedLogGrade.csv differ diff --git a/titles/sao/data/1/Unit.csv b/titles/sao/data/1/Unit.csv new file mode 100644 index 0000000..70fbca1 Binary files /dev/null and b/titles/sao/data/1/Unit.csv differ diff --git a/titles/sao/data/1/UnitCollision.csv b/titles/sao/data/1/UnitCollision.csv new file mode 100644 index 0000000..98eb0f9 Binary files /dev/null and b/titles/sao/data/1/UnitCollision.csv differ diff --git a/titles/sao/data/1/UnitGimmick.csv b/titles/sao/data/1/UnitGimmick.csv new file mode 100644 index 0000000..0c10a6c Binary files /dev/null and b/titles/sao/data/1/UnitGimmick.csv differ diff --git a/titles/sao/data/1/UnitPower.csv b/titles/sao/data/1/UnitPower.csv new file mode 100644 index 0000000..e9cf9b4 Binary files /dev/null and b/titles/sao/data/1/UnitPower.csv differ diff --git a/titles/sao/data/1/WeaponType.csv b/titles/sao/data/1/WeaponType.csv new file mode 100644 index 0000000..a6174b4 Binary files /dev/null and b/titles/sao/data/1/WeaponType.csv differ diff --git a/titles/sao/data/1/YuiMedalBonus.csv b/titles/sao/data/1/YuiMedalBonus.csv new file mode 100644 index 0000000..66bdb18 Binary files /dev/null and b/titles/sao/data/1/YuiMedalBonus.csv differ diff --git a/titles/sao/data/1/YuiMedalBonusCondition.csv b/titles/sao/data/1/YuiMedalBonusCondition.csv new file mode 100644 index 0000000..5f33487 Binary files /dev/null and b/titles/sao/data/1/YuiMedalBonusCondition.csv differ diff --git a/titles/sao/data/1/YuiMedalGetCondition.csv b/titles/sao/data/1/YuiMedalGetCondition.csv new file mode 100644 index 0000000..606ec4c Binary files /dev/null and b/titles/sao/data/1/YuiMedalGetCondition.csv differ diff --git a/titles/sao/data/1/YuiMedalShopItems.csv b/titles/sao/data/1/YuiMedalShopItems.csv new file mode 100644 index 0000000..6a12e42 Binary files /dev/null and b/titles/sao/data/1/YuiMedalShopItems.csv differ diff --git a/titles/sao/data/YuiMedalShops.csv b/titles/sao/data/1/YuiMedalShops.csv similarity index 60% rename from titles/sao/data/YuiMedalShops.csv rename to titles/sao/data/1/YuiMedalShops.csv index 5bac235..831d4f1 100644 Binary files a/titles/sao/data/YuiMedalShops.csv and b/titles/sao/data/1/YuiMedalShops.csv differ diff --git a/titles/sao/data/EquipmentLevel.csv b/titles/sao/data/EquipmentLevel.csv deleted file mode 100644 index 803bf7e..0000000 Binary files a/titles/sao/data/EquipmentLevel.csv and /dev/null differ diff --git a/titles/sao/data/GashaMedalShopItems.csv b/titles/sao/data/GashaMedalShopItems.csv deleted file mode 100644 index c5a46ae..0000000 Binary files a/titles/sao/data/GashaMedalShopItems.csv and /dev/null differ diff --git a/titles/sao/data/HeroLogLevel.csv b/titles/sao/data/HeroLogLevel.csv deleted file mode 100644 index 36a53b9..0000000 Binary files a/titles/sao/data/HeroLogLevel.csv and /dev/null differ diff --git a/titles/sao/data/PlayerRank.csv b/titles/sao/data/PlayerRank.csv deleted file mode 100644 index 9031661..0000000 Binary files a/titles/sao/data/PlayerRank.csv and /dev/null differ diff --git a/titles/sao/data/RareDropTable.csv b/titles/sao/data/RareDropTable.csv deleted file mode 100644 index cb674f6..0000000 Binary files a/titles/sao/data/RareDropTable.csv and /dev/null differ diff --git a/titles/sao/data/ResEarnCampaignShopItems.csv b/titles/sao/data/ResEarnCampaignShopItems.csv deleted file mode 100644 index 6d5dd17..0000000 Binary files a/titles/sao/data/ResEarnCampaignShopItems.csv and /dev/null differ diff --git a/titles/sao/data/ResEarnCampaignShops.csv b/titles/sao/data/ResEarnCampaignShops.csv deleted file mode 100644 index 2c201c9..0000000 Binary files a/titles/sao/data/ResEarnCampaignShops.csv and /dev/null differ diff --git a/titles/sao/data/RewardTable.csv b/titles/sao/data/RewardTable.csv deleted file mode 100644 index 929ff82..0000000 Binary files a/titles/sao/data/RewardTable.csv and /dev/null differ diff --git a/titles/sao/data/Title.csv b/titles/sao/data/Title.csv deleted file mode 100644 index 73b4dfd..0000000 Binary files a/titles/sao/data/Title.csv and /dev/null differ diff --git a/titles/sao/data/YuiMedalShopItems.csv b/titles/sao/data/YuiMedalShopItems.csv deleted file mode 100644 index f5d91fe..0000000 Binary files a/titles/sao/data/YuiMedalShopItems.csv and /dev/null differ diff --git a/titles/sao/frontend.py b/titles/sao/frontend.py new file mode 100644 index 0000000..f9264fc --- /dev/null +++ b/titles/sao/frontend.py @@ -0,0 +1,262 @@ +import yaml +import jinja2 +from typing import List +from starlette.requests import Request +from starlette.responses import Response, RedirectResponse, JSONResponse, PlainTextResponse +from starlette.routing import Route +from os import path +import random +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Hash import SHA1 +from Crypto.Cipher import AES, _mode_cbc + +from core.frontend import FE_Base, UserSession +from core.config import CoreConfig +from .database import SaoData +from .config import SaoConfig +from .const import SaoConstants + + +class SaoFrontend(FE_Base): + SN_PREFIX = SaoConstants.SERIAL_IDENT + NETID_PREFIX = SaoConstants.NETID_PREFIX + ALL_HEROS = [] + def __init__( + self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str + ) -> None: + super().__init__(cfg, environment) + self.data = SaoData(cfg) + self.game_cfg = SaoConfig() + if path.exists(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}"): + self.game_cfg.update( + yaml.safe_load(open(f"{cfg_dir}/{SaoConstants.CONFIG_NAME}")) + ) + self.nav_name = "SAO" + self.card_key= None + self.card_iv = None + + if self.game_cfg.card.enable and self.game_cfg.card.crypt_password and self.game_cfg.card.crypt_salt: + hash = PBKDF2( + self.game_cfg.card.crypt_password, + bytes.fromhex(self.game_cfg.card.crypt_salt), + 48, + count=1000, + hmac_hash_module=SHA1, + ) + + self.card_key = hash[:32] + self.card_iv = hash[32:48] + + def get_routes(self) -> List[Route]: + return [ + Route("/", self.render_GET, methods=['GET']), + Route("/update.name", self.change_name, methods=['POST']), + Route("/matching.auth", self.matching_auth, methods=['POST']), + Route("/matching.auth/", self.matching_auth, methods=['POST']), + Route("/qr.read", self.read_qr, methods=['POST']), + Route("/qr.register", self.reg_qr, methods=['POST']), + Route("/profile.register", self.reg_profile, methods=['POST']) + ] + + async def render_GET(self, request: Request) -> Response: + template = self.environment.get_template( + "titles/sao/templates/sao_index.jinja" + ) + pf = None + + usr_sesh = self.validate_session(request) + if not usr_sesh: + usr_sesh = UserSession() + + else: + profile = await self.data.profile.get_profile(usr_sesh.user_id) + if profile is not None: + pf = profile._asdict() + if not self.ALL_HEROS: + self.ALL_HEROS = await self.data.static.get_heros() + + err = 0 + suc = 0 + if "e" in request.query_params: + err = request.query_params.get("e", 0) + if "s" in request.query_params: + suc = request.query_params.get("s", 0) + + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + profile=pf, + success=int(suc), + error=int(err), + all_heros=self.ALL_HEROS if self.ALL_HEROS else [] + ), media_type="text/html; charset=utf-8") + + async def change_name(self, request: Request) -> RedirectResponse: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/game/sao/", 303) + + frm = await request.form() + new_name = frm.get("new_name") + + if len(new_name) > 16: + return RedirectResponse("/game/sao/?e=8", 303) + + await self.data.profile.set_profile_name(usr_sesh.user_id, new_name) + self.logger.info(f"User {usr_sesh.user_id} changed name to {new_name}") + + return RedirectResponse("/game/sao/?s=1", 303) + + async def matching_auth(self, request: Request) -> JSONResponse: + self.logger.debug(f"Mathing auth params: {request.query_params}") + self.logger.debug(f"Mathing auth headers: {request.headers}") + uid = request.query_params.get('userId', '') + + if not uid: + uid = f'Guest{str(random.randint(1,9999)).zfill(4)}' + self.logger.info(f"Matching auth request with no userId, using {uid}") + + else: + self.logger.info(f"Matching auth request for userId {uid}") + + return JSONResponse({ "ResultCode": 1, "UserId": uid }) # Just auth everything for now + + async def read_qr(self, request: Request) -> PlainTextResponse: + if not self.card_key or not self.card_iv: + return PlainTextResponse("e13-1", 400) + + usr_sesh = self.validate_session(request) + if not usr_sesh: + return PlainTextResponse("e9", 403) + + frm = await request.form() + qr_data = frm.get("qr_data", "") + if not qr_data.isalnum() or not len(qr_data) == 0x40: + return PlainTextResponse("e14-1", 400) + + try: + cipher: _mode_cbc.CbcMode = AES.new(self.card_key, AES.MODE_CBC, iv=self.card_iv) + except Exception as e: + self.logger.error(f"Error creating card cipher - {e}") + return PlainTextResponse("e13-2", 500) + sn = b"" + try: + sn = cipher.decrypt(bytes.fromhex(qr_data))[:19] + sn = sn.decode() + except Exception as e: + self.logger.error(f"Error decrypting card data {qr_data} ({sn}) - {e}") + return PlainTextResponse("e14-2", 400) + + if not sn.isnumeric(): + self.logger.error(f"Card serial {sn} decrypted incorrectly") + return PlainTextResponse("e13-3", 400) + + return PlainTextResponse(sn) + + async def reg_qr(self, request: Request) -> RedirectResponse: + if not self.card_key or not self.card_iv: + return RedirectResponse("/game/sao/?e=14", 303) + + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/game/sao/?e=9", 303) + + frm = await request.form() + serial = frm.get("qr_register_serial") + hero = frm.get("qr_register_hero") + is_holo = bool(frm.get("qr_register_holo", False)) + + user_hero = await self.data.item.get_hero_log(usr_sesh.user_id, hero) + if not user_hero: + hero_statc = await self.data.static.get_hero_by_id(hero) + if not hero_statc: + self.logger.error(f"Failed to find hero log {hero}! Please run the reader") + return RedirectResponse(" /game/sao/?e=13", 303) + + skills = await self.data.static.get_skill_table_by_subid(hero_statc['SkillTableSubId']) + if not skills: + self.logger.error(f"Failed to find skill table {hero_statc['SkillTableSubId']}! Please run the reader") + return RedirectResponse("/game/sao/?e=13", 303) + + default_skills = [] + now_have_skills = [None, None, None, None, None] + x = 0 + for skill in skills: + if skill['LevelObtained'] == 1 and skill['AwakeningId'] == 0: + default_skills[x] = skill['SkillId'] + x += 1 + if x >= 5: + break + + for skill in default_skills: + skill_info = await self.data.static.get_skill_by_id(skill) + skill_slot = skill_info['Level'] - 1 + if now_have_skills[skill_slot] is not None: + now_have_skills[skill] + + + user_hero_id = await self.data.item.put_hero_log( + usr_sesh.user_id, + hero, + 1, + 0, + hero_statc['DefaultEquipmentId1'], + hero_statc['DefaultEquipmentId2'], + now_have_skills[0], + now_have_skills[1], + now_have_skills[2], + now_have_skills[3], + now_have_skills[4] + ) + if not user_hero_id: + self.logger.error(f"Failed to give user {usr_sesh.user_id} hero {hero}!") + return RedirectResponse("/game/sao/?e=99", 303) + else: + user_hero_id = user_hero['id'] + + card_id = await self.data.profile.put_hero_card(usr_sesh.user_id, serial, user_hero_id, is_holo) + if not card_id: + self.logger.error(f"Failed to give user {usr_sesh.user_id} hero card {hero}!") + return RedirectResponse("/game/sao/?e=99", 303) + + self.logger.info(f"User {usr_sesh.user_id} added hero {hero} as card with id {card_id}") + + return RedirectResponse("/game/sao/?s=1", 303) + + async def reg_profile(self, request: Request) -> RedirectResponse: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/game/sao/?e=9", 303) + + frm = await request.form() + name = frm.get("sao_register_username") + + if len(name) > 16: + return RedirectResponse("/game/sao/?e=8", 303) + + profile_id = await self.data.profile.create_profile(usr_sesh.user_id) + if not profile_id: + self.logger.error(f"Failed to web register User {usr_sesh.user_id} with name {name}") + return RedirectResponse("/game/sao/?e=99", 303) + + await self.data.profile.set_profile_name(usr_sesh.user_id, name) + + equip1 = await self.data.item.put_equipment(usr_sesh.user_id, 101000000) + equip2 = await self.data.item.put_equipment(usr_sesh.user_id, 102000000) + equip3 = await self.data.item.put_equipment(usr_sesh.user_id, 109000000) + if not equip1 or not equip2 or not equip3: + self.logger.error(f"Failed to create profile for user {usr_sesh.user_id} from (could not add equipment)") + return RedirectResponse("/game/sao/?e=98", 303) + + hero1 = await self.data.item.put_hero_log(usr_sesh.user_id, 101000010, 1, 0, equip1, None, 1002, 1003, 1014, None, None) + hero2 = await self.data.item.put_hero_log(usr_sesh.user_id, 102000010, 1, 0, equip2, None, 3001, 3002, 3004, None, None) + hero3 = await self.data.item.put_hero_log(usr_sesh.user_id, 105000010, 1, 0, equip3, None, 10005, 10002, 10004, None, None) + if not hero1 or not hero2 or not hero3: + self.logger.error(f"Failed to create profile for user {usr_sesh.user_id} (could not add heros)") + return RedirectResponse("/game/sao/?e=97", 303) + + await self.data.item.put_hero_party(usr_sesh.user_id, 0, hero1, hero2, hero3) + self.logger.info(f"Web registered User {usr_sesh.user_id} profile {profile_id} with name {name}") + + return RedirectResponse("/game/sao/?s=1", 303) \ No newline at end of file diff --git a/titles/sao/handlers/base.py b/titles/sao/handlers/base.py index 2f2f13e..e2cb9d1 100644 --- a/titles/sao/handlers/base.py +++ b/titles/sao/handlers/base.py @@ -1,10 +1,9 @@ import struct from datetime import datetime -from typing import List -from construct import * +from typing import List, Union from .helpers import * -import csv -from csv import * + +from ..const import GameconnectCmd class SaoRequestHeader: def __init__(self, data: bytes) -> None: @@ -21,10 +20,9 @@ class SaoRequestHeader: class SaoBaseRequest: def __init__(self, header: SaoRequestHeader, data: bytes) -> None: self.header = header - # TODO: Length check class SaoResponseHeader: - def __init__(self, cmd_id: int) -> None: + def __init__(self, cmd_id: GameconnectCmd) -> None: self.cmd = cmd_id self.err_status = 0 self.error_type = 0 @@ -37,12 +35,40 @@ class SaoResponseHeader: return struct.pack("!HHIIIII", self.cmd, self.err_status, self.error_type, self.vendor_id, self.game_id, self.version_id, self.length) class SaoBaseResponse: - def __init__(self, cmd_id: int) -> None: - self.header = SaoResponseHeader(cmd_id) + def __init__(self, cmd_id: Union[GameconnectCmd, int]) -> None: + if type(cmd_id) == int: + cmd = GameconnectCmd(cmd_id) + else: + cmd = cmd_id + self.header = SaoResponseHeader(cmd) def make(self) -> bytes: return self.header.make() +class SaoGenericUserTicketRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + ticket_id = decode_str(data, off) + self.ticket_id = ticket_id[0] + off += ticket_id[1] + + user_id = decode_str(data, off) + self.user_id = user_id[0] + off += user_id[1] + +class SaoGenericUserRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + +class SaoNoopRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + self.dummy = decode_byte(data, 0) + class SaoNoopResponse(SaoBaseResponse): def __init__(self, cmd: int) -> None: super().__init__(cmd) @@ -51,154 +77,164 @@ class SaoNoopResponse(SaoBaseResponse): def make(self) -> bytes: return super().make() + struct.pack("!bI", self.result, 0) - -class SaoGetMaintRequest(SaoBaseRequest): + +class SaoTicketRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) - # TODO: The rest of the mait info request + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off -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" +class SaoTicketResponse(SaoBaseResponse): + def __init__(self, ticket_id: str = "9") -> None: + super().__init__(GameconnectCmd.TICKET_RESPONSE) + self.result = "1" + self.ticket_id = ticket_id 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 + return super().make() \ + + encode_str(self.result) \ + + encode_str(self.ticket_id) - maint_begin_bytes = b"" - maint_end_bytes = b"" - - for x in maint_begin_list: - maint_begin_bytes += struct.pack(" None: + super().__init__(GameconnectCmd.GET_MAINTENANCE_INFO_RESPONSE) + self.result = 1 + self.maint_begin = maint_start + self.maint_end = maint_end + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_date_str(self.maint_begin) \ + + encode_date_str(self.maint_end) class SaoCommonAcCabinetBootNotificationRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF -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 - ) + self.net_id, new_off = decode_str(data, off) + off += new_off - resp_data = resp_struct.build(dict( - result=self.result, - )) + self.place_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.current_version_app_id = decode_int(data, off) + off += INT_OFF + + self.current_master_data_version = decode_int(data, off) + off += INT_OFF class SaoMasterDataVersionCheckRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + self.current_data_version = decode_int(data, 0) class SaoMasterDataVersionCheckResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self, current_ver: int, game_ver: int) -> None: + super().__init__(GameconnectCmd.MASTER_DATA_VERSION_CHECK_RESPONSE) self.result = 1 - self.update_flag = 0 - self.data_version = 100 + self.update_flag = 1 if game_ver != current_ver else 0 + self.data_version = current_ver 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) + resp_data = encode_byte(self.result) + resp_data += encode_byte(self.update_flag) + resp_data += encode_int(self.data_version) return super().make() + resp_data -class SaoCommonGetAppVersionsRequest(SaoBaseRequest): +class SaoGetAppVersionsResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_APP_VERSIONS_RESPONSE) + self.result = 1 + self.data_list: List[AppVersionData] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_cls(self.data_list) + return super().make() + ret + +class SaoPayingPlayStartRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.paying_user_id, new_off = decode_str(data, off) + off += new_off -class SaoCommonGetAppVersionsRequest(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + self.game_sub_id, new_off = decode_str(data, off) + off += new_off + + self.net_id, new_off = decode_str(data, off) + off += new_off + + self.place_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + +class SaoPayingPlayStartResponse(SaoBaseResponse): + def __init__(self, session_id: str = "1") -> None: + super().__init__(GameconnectCmd.PAYING_PLAY_START_RESPONSE) self.result = 1 - self.data_list_size = 1 # Number of arrays - - self.version_app_id = 1 - self.applying_start_date = "20230520193000" + self.paying_session_id = session_id def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "data_list_size" / Int32ub, + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.paying_session_id) - "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): +class SaoPayingPlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.paying_session_id, new_off = decode_str(data, off) + off += new_off -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)], - ) + self.paying_user_id, new_off = decode_str(data, off) + off += new_off - 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.net_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.place_id, new_off = decode_str(data, off) + off += new_off + + self.played_date, new_off = decode_str(data, off) + off += new_off + + self.played_type = decode_short(data, off) + off += SHORT_OFF + + self.played_amount = decode_short(data, off) + off += SHORT_OFF + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off class SaoGetAuthCardDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: @@ -207,7 +243,7 @@ class SaoGetAuthCardDataRequest(SaoBaseRequest): self.cabinet_type = decode_byte(data, off) off += BYTE_OFF - self.auth_type = decode_byte(data, off) + self.auth_type = AuthType(decode_byte(data, off)) # 0 is unknown, 1 is card (banapass, aime, AICC), 2 is moble off += BYTE_OFF store_id = decode_str(data, off) @@ -226,147 +262,90 @@ class SaoGetAuthCardDataRequest(SaoBaseRequest): self.chip_id = chip_id[0] off += chip_id[1] -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) - +class SaoGetAuthCardDataResponse(SaoBaseResponse): + def __init__(self, nicknname: str, user_id: int) -> None: + super().__init__(GameconnectCmd.GET_AUTH_CARD_DATA_RESPONSE) 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']) + self.nick_name = nicknname + self.personal_id = str(user_id) 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)] - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.unused_card_flag) \ + + encode_byte(self.first_play_flag) \ + + encode_byte(self.tutorial_complete_flag) \ + + encode_str(self.nick_name) \ + + encode_str(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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, 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): +class SaoGetAccessCodeByKeitaiRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 self.cabinet_type = decode_byte(data, off) off += BYTE_OFF - self.auth_type = decode_byte(data, off) + self.auth_type = AuthType(decode_byte(data, off)) # 0 is unknown, 1 is card (banapass, aime, AICC), 2 is moble + off += BYTE_OFF + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.chip_id, new_off = decode_str(data, off) + off += new_off + +class SaoGetAccessCodeByKeitaiResponse(SaoBaseResponse): + def __init__(self, access_code: str) -> None: + super().__init__(GameconnectCmd.GET_ACCESS_CODE_BY_KEITAI_RESPONSE) + self.result = 1 + self.access_code = access_code + self.ba_id_flag = 1 + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.access_code) \ + + encode_byte(self.ba_id_flag) + +class SaoCheckAcLoginBonusResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.CHECK_AC_LOGIN_BONUS_RESPONSE) + self.result = 1 + self.reward_get_flag = 1 + + self.get_ac_login_bonus_id_list: List[int] = [] # "2020年7月9日~(アニメ&リコリス記念)" + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_byte(self.reward_get_flag) \ + + encode_arr_num(self.get_ac_login_bonus_id_list, INT_OFF) \ + +class SaoGetQuestSceneMultiPlayPhotonServerResponse(SaoBaseResponse): + def __init__(self, app_id: str = "7df3a2f6-d69d-4073-aafe-810ee61e1cea") -> None: + super().__init__(GameconnectCmd.GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_RESPONSE) + self.result = 1 + self.application_id = app_id + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.application_id) + +class SaoLoginRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.auth_type = AuthType(decode_byte(data, off)) # 0 is unknown, 1 is card (banapass, aime, AICC), 2 is moble off += BYTE_OFF store_id = decode_str(data, off) @@ -392,1107 +371,555 @@ class SaoCommonLoginRequest(SaoBaseRequest): self.free_ticket_distribution_target_flag = decode_byte(data, off) off += BYTE_OFF -class SaoCommonLoginResponse(SaoBaseResponse): - def __init__(self, cmd, profile_data) -> None: - super().__init__(cmd) +class SaoLoginResponse(SaoBaseResponse): + def __init__(self, user_id: int, first_play: bool = False, logged_in_today: bool = True) -> None: + super().__init__(GameconnectCmd.LOGIN_RESPONSE) 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 + self.user_id = str(user_id) + self.first_play_flag = first_play + self.grantable_free_ticket_flag = not logged_in_today + self.login_reward_vp = 0 if logged_in_today else 100 + self.today_paying_flag = logged_in_today 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 - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.user_id) \ + + encode_byte(self.first_play_flag) \ + + encode_byte(self.grantable_free_ticket_flag) \ + + encode_short(self.login_reward_vp) \ + + encode_byte(self.today_paying_flag) - 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): +class SaoLogoutRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off -class SaoCheckComebackEventRequest(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) # 0 satalite, 1 terminal + off += BYTE_OFF + + self.remaining_ticket_num = decode_short(data, off) + off += SHORT_OFF + +class SaoPurchaseTicketRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.discount_type = decode_byte(data, off) + off += BYTE_OFF + self.purchase_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoConsumeTicketRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.discount_type = decode_byte(data, off) + off += BYTE_OFF + self.act_type = ActTypeConsumeTicket(decode_byte(data, off)) + off += BYTE_OFF + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoAddCreditRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.add_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoConsumeCreditRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.act_type = ActTypeConsumeCredit(decode_byte(data, off)) + off += BYTE_OFF + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoPurchaseTicketGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.discount_type = decode_byte(data, off) + off += BYTE_OFF + self.purchase_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoConsumeTicketGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.discount_type = decode_byte(data, off) + off += BYTE_OFF + self.act_type = ActTypeConsumeTicket(decode_byte(data, off)) + off += BYTE_OFF + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoAddCreditGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.add_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoConsumeCreditGuestRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + self.act_type = ActTypeConsumeCredit(decode_byte(data, off)) + off += BYTE_OFF + self.consume_num = decode_byte(data, off) + off += BYTE_OFF + +class SaoCheckComebackEventResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.CHECK_COMEBACK_EVENT_RESPONSE) self.result = 1 - self.get_flag_ = 1 - self.get_comeback_event_id_list = "" # Array of events apparently + self.get_flag = 1 + self.get_comeback_event_id_list: List[int] = [] # 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)], - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_byte(self.get_flag) \ + + encode_arr_num(self.get_comeback_event_id_list, INT_OFF) - 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], - )) +class SaoChangeMyStoreRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + +class SaoCheckTitleGetDecisionRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_offset = decode_str(data, off) + off += new_offset + + self.user_id, new_offset = decode_str(data, off) + off += new_offset + +class SaoCheckTitleGetDecisionResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.CHECK_TITLE_GET_DECISION_RESPONSE) + self.result = 1 + self.get_title_id_list: List[int] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_num(self.get_title_id_list, INT_OFF) class SaoGetUserBasicDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off class SaoGetUserBasicDataResponse(SaoBaseResponse): - def __init__(self, cmd, profile_data) -> None: - super().__init__(cmd) + def __init__(self, profile_data) -> None: + super().__init__(GameconnectCmd.GET_USER_BASIC_DATA_RESPONSE) 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" - + self.user_basic_data: List[UserBasicData] = [UserBasicData.from_args(profile_data)] + 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, + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.user_basic_data) - "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): +class SaoGetVpGashaTicketDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + +class SaoGetVpGashaTicketDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_VP_GASHA_TICKET_DATA_LIST_RESPONSE) + self.result = 1 + self.vp_gasha_ticket_data_list: List[VpGashaTicketData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.vp_gasha_ticket_data_list) + +class SaoChangeTitleRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.user_title_id, new_off = decode_str(data, off) + off += new_off + +class SaoGetPresentBoxNumResponse(SaoBaseResponse): + def __init__(self, num_box: int = 0, max_num: int = 0) -> None: + super().__init__(GameconnectCmd.GET_PRESENT_BOX_NUM_RESPONSE) + self.result = 1 + self.num = num_box + self.max_num = max_num + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_short(self.num) \ + + encode_short(self.max_num) class SaoGetHeroLogUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, hero_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_HERO_LOG_USER_DATA_LIST_RESPONSE) 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) - - resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) - - # 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.hero_log_user_data_list) class SaoGetEquipmentUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, equipment_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_EQUIPMENT_USER_DATA_LIST_RESPONSE) self.result = 1 - - self.user_equipment_id = [] - self.enhancement_value = [] - self.max_enhancement_value_extended_num = [] - self.enhancement_exp = [] - self.awakening_stage = [] - self.awakening_exp = [] - self.possible_awakening_flag = [] - equipment_level = 0 - - for i in range(len(equipment_data)): - - # Calculate level based off experience and the CSV list - with open(r'titles/sao/data/EquipmentLevel.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 = equipment_data[i][4] - - for e in range(0,len(data)): - if exp>=int(data[e][1]) and exp 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[i], - max_enhancement_value_extended_num=self.max_enhancement_value_extended_num[i], - enhancement_exp=self.enhancement_exp[i], - possible_awakening_flag=self.possible_awakening_flag[i], - awakening_stage=self.awakening_stage[i], - awakening_exp=self.awakening_exp[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.equipment_user_data_list.append(equipment_data) - - resp_data["equipment_user_data_list_size"] = len(resp_data.equipment_user_data_list) - - # 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.equipment_user_data_list) class SaoGetItemUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, item_data) -> None: - super().__init__(cmd) + def __init__(self, item_data: List[Dict] = []) -> None: + super().__init__(GameconnectCmd.GET_ITEM_USER_DATA_LIST_RESPONSE) self.result = 1 + self.item_user_data_list: List[ItemUserData] = [] - self.user_item_id = [] - - for i in range(len(item_data)): - self.user_item_id.append(item_data[i][2]) - - # item_user_data_list - self.user_item_id = list(map(str,self.user_item_id)) #str - self.item_id = list(map(int,self.user_item_id)) #int - self.protect_flag = 0 #byte - self.get_date = "20230101120000" #str + if item_data: + for item in item_data: + self.item_user_data_list.append(ItemUserData.from_args(item)) 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) - - resp_data["item_user_data_list_size"] = len(resp_data.item_user_data_list) - - # 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.item_user_data_list) class SaoGetSupportLogUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, supportIdsData) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_SUPPORT_LOG_USER_DATA_LIST_RESPONSE) self.result = 1 + self.support_log_user_data_list: List[SupportLogUserData] = [] - # 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["support_log_user_data_list_size"] = len(resp_data.support_log_user_data_list) - - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data - -class SaoGetTitleUserDataListRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.support_log_user_data_list) class SaoGetTitleUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, titleIdsData) -> None: - super().__init__(cmd) + def __init__(self, user_id: str, titles: List[int]) -> None: + super().__init__(GameconnectCmd.GET_TITLE_USER_DATA_LIST_RESPONSE) self.result = 1 + self.title_user_data_list: List[TitleUserData] = [] + + if titles: + for x in titles: + self.title_user_data_list.append(TitleUserData.from_args(user_id + str(x), x)) - # 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) - - resp_data["title_user_data_list_size"] = len(resp_data.title_user_data_list) - - # 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.title_user_data_list) class SaoGetEpisodeAppendDataListResponse(SaoBaseResponse): - def __init__(self, cmd, profile_data) -> None: - super().__init__(cmd) - self.length = None + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_EPISODE_APPEND_DATA_LIST_RESPONSE) 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] + self.episode_append_data_list: List[EpisodeAppendUserData] = [] 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, - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.episode_append_data_list) - # 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], - )) - - resp_data["episode_append_data_list_size"] = len(resp_data.episode_append_data_list) - - # 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] - -class SaoGetPartyDataListResponse(SaoBaseResponse): # Default party - def __init__(self, cmd, hero1_data, hero2_data, hero3_data) -> None: - super().__init__(cmd) - +class SaoGetEventItemDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_EVENT_ITEM_DATA_LIST_RESPONSE) 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] + self.event_item_data_list: List[EventItemUserData] = [] 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 + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.event_item_data_list) - "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 SaoGetGashaMedalUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_GASHA_MEDAL_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalUserData] = [] -class SaoGetQuestScenePrevScanProfileCardRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetPartyDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_PARTY_DATA_LIST_RESPONSE) + self.result = 1 + self.party_data_list: List[PartyData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.party_data_list) + +class SaoGetSupportLogPartyDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_SUPPORT_LOG_PARTY_DATA_LIST_RESPONSE) + self.result = 1 + self.support_log_party_data_list: List[SupportLogPartyData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.support_log_party_data_list) class SaoGetQuestScenePrevScanProfileCardResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_QUEST_SCENE_PREV_SCAN_PROFILE_CARD_RESPONSE) self.result = 1 - self.profile_card_data = 1 # number of arrays - - self.profile_card_code = "" - self.nick_name = "" - + self.profile_card_data: List[ReadProfileCardData] = [] + 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, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.profile_card_data) class SaoGetResourcePathInfoResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self, base_url: str) -> None: + super().__init__(GameconnectCmd.GET_RESOURCE_PATH_INFO_RESPONSE) 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" + self.resource_base_url = base_url + self.gasha_base_dir = "gasha" + self.ad_base_dir = "ad" + self.event_base_dir = "event" 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)], - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.resource_base_url) \ + + encode_str(self.gasha_base_dir) \ + + encode_str(self.ad_base_dir) \ + + encode_str(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], - )) +class SaoValidationErrorNotificationRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.net_id, new_off = decode_str(data, off) + off += new_off + + self.place_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.send_protocol_name, new_off = decode_str(data, off) + off += new_off + + self.send_data_to_fraud_value, new_off = decode_str(data, off) + off += new_off + + self.send_data_to_modification_value, new_off = decode_str(data, off) + off += new_off + +class SaoGetBeginnerMissionUserDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + +class SaoGetBeginnerMissionUserDataResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_BEGINNER_MISSION_USER_DATA_RESPONSE) + self.result = 1 + self.data: List[BeginnerMissionUserData] = [BeginnerMissionUserData.from_args(datetime.now())] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data) + +class SaoMatchingErrorNotificationRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.net_id, new_off = decode_str(data, off) + off += new_off + + self.place_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.matching_error_data_list: List[MatchingErrorData] + self.matching_error_data_list, new_off = decode_arr_cls(data, off, MatchingErrorData) + +class SaoPowerCuttingReturnNotification(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.cabinet_type = decode_byte(data, off) + off += BYTE_OFF + + self.net_id, new_off = decode_str(data, off) + off += new_off + + self.place_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.serial_no, new_off = decode_str(data, off) + off += new_off + + self.last_act_type = decode_byte(data, off) + off += BYTE_OFF + + self.remaining_ticket_num = decode_short(data, off) + off += SHORT_OFF + + self.remaining_credit_num = decode_short(data, off) + off += SHORT_OFF class SaoEpisodePlayStartRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, off) - self.ticket_id = ticket_id[0] - off += ticket_id[1] + self.ticket_id, new_off = decode_str(data, off) + off += new_off - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] + self.user_id, new_off = decode_str(data, off) + off += new_off self.episode_id = decode_int(data, off) off += INT_OFF @@ -1500,183 +927,60 @@ class SaoEpisodePlayStartRequest(SaoBaseRequest): self.play_mode = decode_byte(data, off) off += BYTE_OFF - self.play_start_request_data_count = decode_int(data, off) - off += INT_OFF + self.play_start_request_data: List[QuestScenePlayStartRequestData] = [] + self.play_start_request_data, new_off = decode_arr_cls(data, off, QuestScenePlayStartRequestData) + off += new_off - self.play_start_request_data: List[PlayStartRequestData] = [] - for _ in range(self.play_start_request_data_count): - tmp = PlayStartRequestData(data, off) - self.play_start_request_data.append(tmp) - off += tmp.get_size() - - self.multi_play_start_request_data_count = decode_int(data, off) - off += INT_OFF - - self.multi_play_start_request_data: List[MultiPlayStartRequestData] = [] - for _ in range(self.multi_play_start_request_data_count): - tmp = MultiPlayStartRequestData(data, off) - off += tmp.get_size() - self.multi_play_start_request_data.append(tmp) + self.multi_play_start_request_data: List[QuestSceneMultiPlayStartRequestData] = [] + self.multi_play_start_request_data, new_off = decode_arr_cls(data, off, QuestSceneMultiPlayStartRequestData) + off += new_off class SaoEpisodePlayStartResponse(SaoBaseResponse): - def __init__(self, cmd, profile_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.EPISODE_PLAY_START_RESPONSE) 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.play_start_response_data: List[QuestScenePlayStartResponseData] = [] + self.multi_play_start_response_data: List[QuestSceneMultiPlayStartResponseData] = [] - 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 + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_start_response_data) \ + + encode_arr_cls(self.multi_play_start_response_data) class SaoEpisodePlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, off) - self.ticket_id = ticket_id[0] - off += ticket_id[1] + self.ticket_id, new_off = decode_str(data, off) + off += new_off - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] + self.user_id, new_off = decode_str(data, off) + off += new_off self.episode_id = decode_int(data, off) off += INT_OFF - self.play_end_request_data_count = decode_int(data, off) - off += INT_OFF + self.play_end_request_data: List[QuestScenePlayEndRequestData] = [] + self.play_end_request_data, new_off = decode_arr_cls(data, off, QuestScenePlayEndRequestData) + off += new_off - self.play_end_request_data_list: List[PlayEndRequestData] = [] - for _ in range(self.play_end_request_data_count): - tmp = PlayEndRequestData(data, off) - off += tmp.get_size() - self.play_end_request_data_list.append(tmp) - - self.multi_play_end_request_data_count = decode_int(data, off) - off += INT_OFF - - self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] - for _ in range(self.multi_play_end_request_data_count): - tmp = MultiPlayEndRequestData(data, off) - off += tmp.get_size() - self.multi_play_end_request_data_list.append(tmp) + self.multi_play_end_request_data: List[QuestSceneMultiPlayEndRequestData] = [] + self.multi_play_end_request_data, new_off = decode_arr_cls(data, off, QuestSceneMultiPlayEndRequestData) + off += new_off class SaoEpisodePlayEndResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.EPISODE_PLAY_END_RESPONSE) 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.play_end_response_data: List[QuestScenePlayEndResponseData] = [QuestScenePlayEndResponseData.from_args()] + self.multi_play_end_response_data: List[QuestSceneMultiPlayEndResponseData] = [QuestSceneMultiPlayEndResponseData.from_args()] - 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 + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_end_response_data) \ + + encode_arr_cls(self.multi_play_end_response_data) class SaoTrialTowerPlayStartRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: @@ -1714,160 +1018,56 @@ class SaoTrialTowerPlayStartRequest(SaoBaseRequest): off += tmp.get_size() self.multi_play_start_request_data.append(tmp) +class SaoTrialTowerPlayStartResponse(SaoBaseResponse): + def __init__(self, sesh_id: int, nickname: str) -> None: + super().__init__(GameconnectCmd.TRIAL_TOWER_PLAY_START_RESPONSE) + self.result = 1 + self.play_start_response_data: List[QuestScenePlayStartResponseData] = [QuestScenePlayStartResponseData.from_args(sesh_id, nickname)] + self.multi_play_start_response_data: List[QuestSceneMultiPlayStartResponseData] = [QuestSceneMultiPlayStartResponseData.from_args()] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_start_response_data) \ + + encode_arr_cls(self.multi_play_start_response_data) + class SaoTrialTowerPlayEndRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, off) - self.ticket_id = ticket_id[0] - off += ticket_id[1] + self.ticket_id, new_off = decode_str(data, off) + off += new_off - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] + self.user_id, new_off = decode_str(data, off) + off += new_off self.trial_tower_id = decode_int(data, off) off += INT_OFF - self.play_end_request_data_count = decode_int(data, off) - off += INT_OFF + self.play_end_request_data: List[QuestScenePlayEndRequestData] = [] + self.play_end_request_data, new_off = decode_arr_cls(data, off, QuestScenePlayEndRequestData) + off += new_off - self.play_end_request_data_list: List[PlayEndRequestData] = [] - for _ in range(self.play_end_request_data_count): - tmp = PlayEndRequestData(data, off) - off += tmp.get_size() - self.play_end_request_data_list.append(tmp) - - self.multi_play_end_request_data_count = decode_int(data, off) - off += INT_OFF - - self.multi_play_end_request_data_list: List[MultiPlayEndRequestData] = [] - for _ in range(self.multi_play_end_request_data_count): - tmp = MultiPlayEndRequestData(data, off) - off += tmp.get_size() - self.multi_play_end_request_data_list.append(tmp) + self.multi_play_end_request_data: List[QuestSceneMultiPlayEndRequestData] + self.multi_play_end_request_data, new_off = decode_arr_cls(data, off, QuestSceneMultiPlayEndRequestData) + off += new_off class SaoTrialTowerPlayEndResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.TRIAL_TOWER_PLAY_END_RESPONSE) self.result = 1 - self.play_end_response_data_size = 1 # Number of arrays - self.multi_play_end_response_data_size = 1 # Unused on solo play - self.trial_tower_play_end_updated_notification_data_size = 1 # Number of arrays - self.treasure_hunt_play_end_response_data_size = 1 # Number of arrays + self.play_end_response_data: List[QuestScenePlayEndResponseData] = [QuestScenePlayEndResponseData.from_args()] + self.multi_play_end_response_data: List[QuestSceneMultiPlayEndResponseData] = [QuestSceneMultiPlayEndResponseData.from_args()] + self.trial_tower_play_end_updated_notification_data: List[QuestTrialTowerPlayEndUpdatedNotificationData] = [QuestTrialTowerPlayEndUpdatedNotificationData.from_args()] + self.treasure_hunt_play_end_response_data: List[QuestTreasureHuntPlayEndResponseData] = [QuestTreasureHuntPlayEndResponseData.from_args()] - self.dummy_1 = 0 - self.dummy_2 = 0 - self.dummy_3 = 0 - - self.rarity_up_occurrence_flag = 0 - self.adventure_ex_area_occurrences_flag = 0 - self.ex_bonus_data_list_size = 1 # Number of arrays - self.play_end_player_trace_reward_data_list_size = 0 # Number of arrays - - self.ex_bonus_table_id = 0 # ExBonusTable.csv values, dont care for now - self.achievement_status = 1 - - self.common_reward_data_size = 1 # Number of arrays - - self.common_reward_type = 0 # dummy values from 2,101000000,1 from RewardTable.csv - self.common_reward_id = 0 - self.common_reward_num = 0 - - self.store_best_score_clear_time_flag = 0 - self.store_best_score_combo_num_flag = 0 - self.store_best_score_total_damage_flag = 0 - self.store_best_score_concurrent_destroying_num_flag = 0 - self.store_reaching_trial_tower_rank = 0 - - self.get_event_point = 0 - self.total_event_point = 0 - def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "play_end_response_data_size" / Int32ub, # big endian - - "rarity_up_occurrence_flag" / Int8ul, # result is either 0 or 1 - "adventure_ex_area_occurrences_flag" / Int8ul, # result is either 0 or 1 - "ex_bonus_data_list_size" / Int32ub, # big endian - "play_end_player_trace_reward_data_list_size" / Int32ub, # big endian - - # ex_bonus_data_list - "ex_bonus_table_id" / Int32ub, - "achievement_status" / Int8ul, # result is either 0 or 1 - - # play_end_player_trace_reward_data_list - "common_reward_data_size" / Int32ub, - - # common_reward_data - "common_reward_type" / Int16ub, # short - "common_reward_id" / Int32ub, - "common_reward_num" / Int32ub, - - "multi_play_end_response_data_size" / Int32ub, # big endian - - # multi_play_end_response_data - "dummy_1" / Int8ul, # result is either 0 or 1 - "dummy_2" / Int8ul, # result is either 0 or 1 - "dummy_3" / Int8ul, # result is either 0 or 1 - - "trial_tower_play_end_updated_notification_data_size" / Int32ub, # big endian - - #trial_tower_play_end_updated_notification_data - "store_best_score_clear_time_flag" / Int8ul, # result is either 0 or 1 - "store_best_score_combo_num_flag" / Int8ul, # result is either 0 or 1 - "store_best_score_total_damage_flag" / Int8ul, # result is either 0 or 1 - "store_best_score_concurrent_destroying_num_flag" / Int8ul, # result is either 0 or 1 - "store_reaching_trial_tower_rank" / Int32ub, - - "treasure_hunt_play_end_response_data_size" / Int32ub, # big endian - - #treasure_hunt_play_end_response_data - "get_event_point" / Int32ub, - "total_event_point" / Int32ub, - ) - - resp_data = resp_struct.build(dict( - result=self.result, - play_end_response_data_size=self.play_end_response_data_size, - - rarity_up_occurrence_flag=self.rarity_up_occurrence_flag, - adventure_ex_area_occurrences_flag=self.adventure_ex_area_occurrences_flag, - ex_bonus_data_list_size=self.ex_bonus_data_list_size, - play_end_player_trace_reward_data_list_size=self.play_end_player_trace_reward_data_list_size, - - ex_bonus_table_id=self.ex_bonus_table_id, - achievement_status=self.achievement_status, - - common_reward_data_size=self.common_reward_data_size, - - common_reward_type=self.common_reward_type, - common_reward_id=self.common_reward_id, - common_reward_num=self.common_reward_num, - - multi_play_end_response_data_size=self.multi_play_end_response_data_size, - - dummy_1=self.dummy_1, - dummy_2=self.dummy_2, - dummy_3=self.dummy_3, - - trial_tower_play_end_updated_notification_data_size=self.trial_tower_play_end_updated_notification_data_size, - store_best_score_clear_time_flag=self.store_best_score_clear_time_flag, - store_best_score_combo_num_flag=self.store_best_score_combo_num_flag, - store_best_score_total_damage_flag=self.store_best_score_total_damage_flag, - store_best_score_concurrent_destroying_num_flag=self.store_best_score_concurrent_destroying_num_flag, - store_reaching_trial_tower_rank=self.store_reaching_trial_tower_rank, - - treasure_hunt_play_end_response_data_size=self.treasure_hunt_play_end_response_data_size, - - get_event_point=self.get_event_point, - total_event_point=self.total_event_point, - )) - - self.length = len(resp_data) - return super().make() + resp_data + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_end_response_data) \ + + encode_arr_cls(self.multi_play_end_response_data) \ + + encode_arr_cls(self.trial_tower_play_end_updated_notification_data) \ + + encode_arr_cls(self.treasure_hunt_play_end_response_data) class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: @@ -1888,1150 +1088,597 @@ class SaoEpisodePlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): off += BYTE_OFF class SaoEpisodePlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): - def __init__(self, cmd, end_session_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.EPISODE_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE) self.result = 1 + self.play_end_unanalyzed_log_reward_data_list: List[QuestScenePlayEndUnanalyzedLogRewardData] = [] - self.unanalyzed_log_grade_id = [] - - self.common_reward_type = [] - self.common_reward_id = [] - self.common_reward_num = 1 - - for x in range(len(end_session_data)): - self.common_reward_id.append(end_session_data[x]) - - with open('titles/sao/data/RewardTable.csv', 'r') as f: - keys_unanalyzed = next(f).strip().split(',') - data_unanalyzed = list(DictReader(f, fieldnames=keys_unanalyzed)) - - for i in range(len(data_unanalyzed)): - if int(data_unanalyzed[i]["CommonRewardId"]) == int(end_session_data[x]): - self.unanalyzed_log_grade_id.append(int(data_unanalyzed[i]["UnanalyzedLogGradeId"])) - self.common_reward_type.append(int(data_unanalyzed[i]["CommonRewardType"])) - break - - self.unanalyzed_log_grade_id = list(map(int,self.unanalyzed_log_grade_id)) #int - self.common_reward_type = list(map(int,self.common_reward_type)) #int - self.common_reward_id = list(map(int,self.common_reward_id)) #int - def make(self) -> bytes: - #new stuff - common_reward_data_struct = Struct( - "common_reward_type" / Int16ub, - "common_reward_id" / Int32ub, - "common_reward_num" / Int32ub, - ) - - play_end_unanalyzed_log_reward_data_list_struct = Struct( - "unanalyzed_log_grade_id" / Int32ub, - "common_reward_data_size" / Rebuild(Int32ub, len_(this.common_reward_data)), # big endian - "common_reward_data" / Array(this.common_reward_data_size, common_reward_data_struct), - ) - - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "play_end_unanalyzed_log_reward_data_list_size" / Rebuild(Int32ub, len_(this.play_end_unanalyzed_log_reward_data_list)), # big endian - "play_end_unanalyzed_log_reward_data_list" / Array(this.play_end_unanalyzed_log_reward_data_list_size, play_end_unanalyzed_log_reward_data_list_struct), - ) - - resp_data = resp_struct.parse(resp_struct.build(dict( - result=self.result, - play_end_unanalyzed_log_reward_data_list_size=0, - play_end_unanalyzed_log_reward_data_list=[], - ))) - - for i in range(len(self.common_reward_id)): - reward_resp_data = dict( - unanalyzed_log_grade_id=self.unanalyzed_log_grade_id[i], - common_reward_data_size=0, - common_reward_data=[], - ) - - reward_resp_data["common_reward_data"].append(dict( - common_reward_type=self.common_reward_type[i], - common_reward_id=self.common_reward_id[i], - common_reward_num=self.common_reward_num, - )) - - resp_data.play_end_unanalyzed_log_reward_data_list.append(reward_resp_data) - - resp_data["play_end_unanalyzed_log_reward_data_list_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list) - for i in range(len(resp_data.play_end_unanalyzed_log_reward_data_list)): - resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data_size"] = len(resp_data.play_end_unanalyzed_log_reward_data_list[i]["common_reward_data"]) - - # finally, rebuild the resp_data - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data - -class SaoGetQuestSceneUserDataListRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.user_id = decode_str(data, 0)[0] + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_end_unanalyzed_log_reward_data_list) class SaoGetQuestSceneUserDataListResponse(SaoBaseResponse): - def __init__(self, cmd, quest_data) -> None: - super().__init__(cmd) - self.length = None + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_QUEST_SCENE_USER_DATA_LIST_RESPONSE) self.result = 1 + self.quest_scene_user_data_list: List[QuestSceneUserData] = [] - # quest_scene_user_data_list_size - self.quest_type = [] - self.quest_scene_id = [] - self.clear_flag = [] - - # quest_scene_best_score_user_data - self.clear_time = [] - self.combo_num = [] - self.total_damage = [] #string - self.concurrent_destroying_num = [] - - for i in range(len(quest_data)): - self.quest_type.append(1) - self.quest_scene_id.append(quest_data[i][2]) - self.clear_flag.append(int(quest_data[i][3])) - - self.clear_time.append(quest_data[i][4]) - self.combo_num.append(quest_data[i][5]) - self.total_damage.append(0) #totally absurd but Int16ul[1] is a big problem due to different lenghts... - self.concurrent_destroying_num.append(quest_data[i][7]) - - # quest_scene_ex_bonus_user_data_list - self.achievement_flag = [1,1,1] - self.ex_bonus_table_id = [1,2,3] - - - self.quest_type = list(map(int,self.quest_type)) #int - self.quest_scene_id = list(map(int,self.quest_scene_id)) #int - self.clear_flag = list(map(int,self.clear_flag)) #int - self.clear_time = list(map(int,self.clear_time)) #int - self.combo_num = list(map(int,self.combo_num)) #int - self.total_damage = list(map(str,self.total_damage)) #string - self.concurrent_destroying_num = list(map(int,self.combo_num)) #int - def make(self) -> bytes: - #new stuff - quest_scene_ex_bonus_user_data_list_struct = Struct( - "ex_bonus_table_id" / Int32ub, # big endian - "achievement_flag" / Int8ul, # result is either 0 or 1 - ) - - 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[1], - "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_resp_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_resp_data["quest_scene_best_score_user_data"].append(dict( - clear_time=self.clear_time[i], - combo_num=self.combo_num[i], - total_damage_size=len(self.total_damage[i]) * 2, - total_damage=[ord(x) for x in self.total_damage[i]], - concurrent_destroying_num=self.concurrent_destroying_num[i], - )) - - resp_data.quest_scene_user_data_list.append(quest_resp_data) - - resp_data["quest_scene_user_data_list_size"] = len(resp_data.quest_scene_user_data_list) - for i in range(len(resp_data.quest_scene_user_data_list)): - resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data_size"] = len(resp_data.quest_scene_user_data_list[i]["quest_scene_best_score_user_data"]) - - # finally, rebuild the resp_data - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.quest_scene_user_data_list) class SaoCheckYuiMedalGetConditionRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + self.user_id, new_off = decode_str(data, off) + off += new_off class SaoCheckYuiMedalGetConditionResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self, elapsed_days: int = 0, get_num: int = 0) -> None: + super().__init__(GameconnectCmd.CHECK_YUI_MEDAL_GET_CONDITION_RESPONSE) self.result = 1 - self.get_flag = 1 - self.elapsed_days = 0 - self.get_yui_medal_num = 0 + self.get_flag = int(get_num > 0) + self.elapsed_days = elapsed_days + self.get_yui_medal_num = get_num 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 + return super().make() \ + + encode_byte(self.result) \ + + encode_byte(self.get_flag) \ + + encode_short(self.elapsed_days) \ + + encode_short(self.get_yui_medal_num) \ class SaoGetYuiMedalBonusUserDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.yui_medal_bonus_id = decode_int(data, off) + off += INT_OFF class SaoGetYuiMedalBonusUserDataResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self, elapsed_days: int = 0, loop_num: int = 0) -> None: + super().__init__(GameconnectCmd.GET_YUI_MEDAL_BONUS_USER_DATA_RESPONSE) 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" + self.data: List[YuiMedalBonusUserData] = [YuiMedalBonusUserData.from_args(elapsed_days, loop_num)] 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 + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data) class SaoCheckProfileCardUsedRewardRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + self.user_id, new_off = decode_str(data, off) + off += new_off class SaoCheckProfileCardUsedRewardResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.CHECK_PROFILE_CARD_USED_REWARD_RESPONSE) self.result = 1 - self.get_flag = 1 + self.get_flag = 0 self.used_num = 0 - self.get_vp = 1 + self.get_vp = 0 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 - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_byte(self.get_flag) \ + + encode_int(self.used_num) \ + + encode_int(self.get_vp) - resp_data = resp_struct.build(dict( - result=self.result, - get_flag=self.get_flag, - used_num=self.used_num, - get_vp=self.get_vp, - - )) +class SaoDisposalResourceRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.disposal_common_reward_user_data_list: List[CommonRewardUserData] = [] + self.disposal_common_reward_user_data_list, new_off = decode_arr_cls(data, off, CommonRewardUserData) + off += new_off + +class SaoDisposalResourceResponse(SaoBaseResponse): + def __init__(self, get_col: int = 0) -> None: + super().__init__(GameconnectCmd.DISPOSAL_RESOURCE_RESPONSE) + self.result = 1 + self.get_col = get_col + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_int(self.get_col) class SaoSynthesizeEnhancementHeroLogRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, off) - self.ticket_id = ticket_id[0] - off += ticket_id[1] + self.ticket_id, new_off = decode_str(data, off) + off += new_off - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] + self.user_id, new_off = decode_str(data, off) + off += new_off - origin_user_hero_log_id = decode_str(data, off) - self.origin_user_hero_log_id = origin_user_hero_log_id[0] - off += origin_user_hero_log_id[1] + self.origin_user_hero_log_id, new_off = decode_str(data, off) + off += new_off - self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] - - self.material_common_reward_user_data_count = decode_int(data, off) - off += INT_OFF - - for _ in range(self.material_common_reward_user_data_count): - mat = MaterialCommonRewardUserData(data, off) - off += mat.get_size() - self.material_common_reward_user_data_list.append(mat) + self.material_common_reward_user_data_list: List[CommonRewardUserData] = [] + self.material_common_reward_user_data_list, new_off = decode_arr_cls(data, off, CommonRewardUserData) + off += new_off class SaoSynthesizeEnhancementHeroLogResponse(SaoBaseResponse): - def __init__(self, cmd, hero_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.SYNTHESIZE_ENHANCEMENT_HERO_LOG_RESPONSE) self.result = 1 + self.after_hero_log_user_data: List[HeroLogUserData] = [] - # Calculate level based off experience and the CSV list - with open(r'titles/sao/data/HeroLogLevel.csv') as csv_file: - csv_reader = csv.reader(csv_file, delimiter=',') - line_count = 0 - data = [] - rowf = False - for row in csv_reader: - if rowf==False: - rowf=True - else: - data.append(row) - - exp = hero_data[4] - - for e in range(0,len(data)): - if exp>=int(data[e][1]) and exp bytes: - #new stuff - - hero_log_user_data_list_struct = Struct( - "user_hero_log_id_size" / Int32ub, # big endian - "user_hero_log_id" / Int16ul[9], #string - "hero_log_id" / Int32ub, #int - "log_level" / Int16ub, #short - "max_log_level_extended_num" / Int16ub, #short - "log_exp" / Int32ub, #int - "possible_awakening_flag" / Int8ul, # result is either 0 or 1 - "awakening_stage" / Int16ub, #short - "awakening_exp" / Int32ub, #int - "skill_slot_correction_value" / Int8ul, # result is either 0 or 1 - "last_set_skill_slot1_skill_id" / Int16ub, #short - "last_set_skill_slot2_skill_id" / Int16ub, #short - "last_set_skill_slot3_skill_id" / Int16ub, #short - "last_set_skill_slot4_skill_id" / Int16ub, #short - "last_set_skill_slot5_skill_id" / Int16ub, #short - "property1_property_id" / Int32ub, - "property1_value1" / Int32ub, - "property1_value2" / Int32ub, - "property2_property_id" / Int32ub, - "property2_value1" / Int32ub, - "property2_value2" / Int32ub, - "property3_property_id" / Int32ub, - "property3_value1" / Int32ub, - "property3_value2" / Int32ub, - "property4_property_id" / Int32ub, - "property4_value1" / Int32ub, - "property4_value2" / Int32ub, - "converted_card_num" / Int16ub, - "shop_purchase_flag" / Int8ul, # result is either 0 or 1 - "protect_flag" / Int8ul, # result is either 0 or 1 - "get_date_size" / Int32ub, # big endian - "get_date" / Int16ul[len(self.get_date)], - ) - - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "hero_log_user_data_list_size" / Rebuild(Int32ub, len_(this.hero_log_user_data_list)), # big endian - "hero_log_user_data_list" / Array(this.hero_log_user_data_list_size, hero_log_user_data_list_struct), - ) - - resp_data = resp_struct.parse(resp_struct.build(dict( - result=self.result, - hero_log_user_data_list_size=0, - hero_log_user_data_list=[], - ))) - - hero_data = dict( - user_hero_log_id_size=len(self.user_hero_log_id) * 2, - user_hero_log_id=[ord(x) for x in self.user_hero_log_id], - hero_log_id=self.hero_log_id, - log_level=self.log_level, - max_log_level_extended_num=self.max_log_level_extended_num, - log_exp=self.log_exp, - possible_awakening_flag=self.possible_awakening_flag, - awakening_stage=self.awakening_stage, - awakening_exp=self.awakening_exp, - skill_slot_correction_value=self.skill_slot_correction_value, - last_set_skill_slot1_skill_id=self.last_set_skill_slot1_skill_id, - last_set_skill_slot2_skill_id=self.last_set_skill_slot2_skill_id, - last_set_skill_slot3_skill_id=self.last_set_skill_slot3_skill_id, - last_set_skill_slot4_skill_id=self.last_set_skill_slot4_skill_id, - last_set_skill_slot5_skill_id=self.last_set_skill_slot5_skill_id, - property1_property_id=self.property1_property_id, - property1_value1=self.property1_value1, - property1_value2=self.property1_value2, - property2_property_id=self.property2_property_id, - property2_value1=self.property2_value1, - property2_value2=self.property2_value2, - property3_property_id=self.property3_property_id, - property3_value1=self.property3_value1, - property3_value2=self.property3_value2, - property4_property_id=self.property4_property_id, - property4_value1=self.property4_value1, - property4_value2=self.property4_value2, - converted_card_num=self.converted_card_num, - shop_purchase_flag=self.shop_purchase_flag, - protect_flag=self.protect_flag, - get_date_size=len(self.get_date) * 2, - get_date=[ord(x) for x in self.get_date], - - ) - - resp_data.hero_log_user_data_list.append(hero_data) - - resp_data["hero_log_user_data_list_size"] = len(resp_data.hero_log_user_data_list) - - # finally, rebuild the resp_data - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.after_hero_log_user_data) class SaoSynthesizeEnhancementEquipmentRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 - ticket_id = decode_str(data, off) - self.ticket_id = ticket_id[0] - off += ticket_id[1] + self.ticket_id, new_off = decode_str(data, off) + off += new_off - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] - - origin_user_equipment_id = decode_str(data, off) - self.origin_user_equipment_id = origin_user_equipment_id[0] - off += origin_user_equipment_id[1] - - self.material_common_reward_user_data_list: List[MaterialCommonRewardUserData] = [] - - self.material_common_reward_user_data_count = decode_int(data, off) - off += INT_OFF + self.user_id, new_off = decode_str(data, off) + off += new_off - for _ in range(self.material_common_reward_user_data_count): - mat = MaterialCommonRewardUserData(data, off) - off += mat.get_size() - self.material_common_reward_user_data_list.append(mat) + self.origin_user_equipment_id, new_off = decode_str(data, off) + off += new_off + + self.material_common_reward_user_data_list: List[CommonRewardUserData] = [] + self.material_common_reward_user_data_list, new_off = decode_arr_cls(data, off, CommonRewardUserData) + off += new_off class SaoSynthesizeEnhancementEquipmentResponse(SaoBaseResponse): - def __init__(self, cmd, synthesize_equipment_data) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.SYNTHESIZE_ENHANCEMENT_EQUIPMENT_RESPONSE) self.result = 1 - equipment_level = 0 + self.after_equipment_user_data: List[EquipmentUserData] = [] - # Calculate level based off experience and the CSV list - with open(r'titles/sao/data/EquipmentLevel.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 = synthesize_equipment_data[4] - - for e in range(0,len(data)): - if exp>=int(data[e][1]) and exp bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.after_equipment_user_data) - after_equipment_user_data_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)], - ) +class SaoGetAdventureExecUserDataResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_ADVENTURE_EXEC_USER_DATA_RESPONSE) + self.result = 1 + self.adventure_exec_user_data: List[AdventureExecUserData] = [] - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "after_equipment_user_data_size" / Rebuild(Int32ub, len_(this.after_equipment_user_data)), # big endian - "after_equipment_user_data" / Array(this.after_equipment_user_data_size, after_equipment_user_data_struct), - ) + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.adventure_exec_user_data) - resp_data = resp_struct.parse(resp_struct.build(dict( - result=self.result, - after_equipment_user_data_size=0, - after_equipment_user_data=[], - ))) +class SaoGetChatSideStoryUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_CHAT_SIDE_STORY_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.chat_side_story_user_data_list: List[ChatSideStoryUserData] = [] - synthesize_equipment_data = dict( - user_equipment_id_size=len(self.user_equipment_id) * 2, - user_equipment_id=[ord(x) for x in self.user_equipment_id], - equipment_id=self.equipment_id, - 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], + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.chat_side_story_user_data_list) - ) - - resp_data.after_equipment_user_data.append(synthesize_equipment_data) +class SaoBeginnerMissionAdConfirmNotificationRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off - resp_data["after_equipment_user_data_size"] = len(resp_data.after_equipment_user_data) + self.user_id, new_off = decode_str(data, off) + off += new_off - # finally, rebuild the resp_data - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF class SaoGetDefragMatchBasicDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF class SaoGetDefragMatchBasicDataResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_DEFRAG_MATCH_BASIC_DATA_RESPONSE) self.result = 1 - self.defrag_match_basic_user_data_size = 1 # number of arrays - - self.seed_flag = 1 - self.ad_confirm_flag = 1 - self.total_league_point = 0 - self.have_league_score = 0 - self.class_num = 1 # 1 to 6 - self.hall_of_fame_confirm_flag = 0 + self.defrag_match_basic_user_data: List[DefragMatchBasicUserData] = [] def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "defrag_match_basic_user_data_size" / Int32ub, # big endian - - "seed_flag" / Int16ub, #short - "ad_confirm_flag" / Int8ul, # result is either 0 or 1 - "total_league_point" / Int32ub, #int - "have_league_score" / Int16ub, #short - "class_num" / Int16ub, #short - "hall_of_fame_confirm_flag" / Int8ul, # result is either 0 or 1 - - ) - - resp_data = resp_struct.build(dict( - result=self.result, - defrag_match_basic_user_data_size=self.defrag_match_basic_user_data_size, - - seed_flag=self.seed_flag, - ad_confirm_flag=self.ad_confirm_flag, - total_league_point=self.total_league_point, - have_league_score=self.have_league_score, - class_num=self.class_num, - hall_of_fame_confirm_flag=self.hall_of_fame_confirm_flag, - )) - - self.length = len(resp_data) - return super().make() + resp_data + ret = encode_byte(self.result) + ret += encode_arr_cls(self.defrag_match_basic_user_data) + return super().make() + ret class SaoGetDefragMatchRankingUserDataRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF class SaoGetDefragMatchRankingUserDataResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self, profile: Dict) -> None: + super().__init__(GameconnectCmd.GET_DEFRAG_MATCH_RANKING_USER_DATA_RESPONSE) self.result = 1 - self.ranking_user_data_size = 1 # number of arrays + self.ranking_user_data: List[DefragMatchRankingUserData] = [DefragMatchRankingUserData.from_args(profile)] - self.league_point_rank = 1 - self.league_score_rank = 1 - self.nick_name = "PLAYER" - self.setting_title_id = 20005 # Default saved during profile creation, no changing for those atm - self.favorite_hero_log_id = 101000010 # Default saved during profile creation - self.favorite_hero_log_awakening_stage = 0 - self.favorite_support_log_id = 0 - self.favorite_support_log_awakening_stage = 0 - self.total_league_point = 1 - self.have_league_score = 1 - def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "ranking_user_data_size" / Int32ub, # big endian - - "league_point_rank" / Int32ub, #int - "league_score_rank" / Int32ub, #int - "nick_name_size" / Int32ub, # big endian - "nick_name" / Int16ul[len(self.nick_name)], - "setting_title_id" / Int32ub, #int - "favorite_hero_log_id" / Int32ub, #int - "favorite_hero_log_awakening_stage" / Int16ub, #short - "favorite_support_log_id" / Int32ub, #int - "favorite_support_log_awakening_stage" / Int16ub, #short - "total_league_point" / Int32ub, #int - "have_league_score" / Int16ub, #short - ) - - resp_data = resp_struct.build(dict( - result=self.result, - ranking_user_data_size=self.ranking_user_data_size, - - league_point_rank=self.league_point_rank, - league_score_rank=self.league_score_rank, - nick_name_size=len(self.nick_name) * 2, - nick_name=[ord(x) for x in self.nick_name], - setting_title_id=self.setting_title_id, - favorite_hero_log_id=self.favorite_hero_log_id, - favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, - favorite_support_log_id=self.favorite_support_log_id, - favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, - total_league_point=self.total_league_point, - have_league_score=self.have_league_score, - )) - - self.length = len(resp_data) - return super().make() + resp_data + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.ranking_user_data) class SaoGetDefragMatchLeaguePointRankingListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.get_rank_start_num = decode_short(data, off) + off += SHORT_OFF + + self.get_rank_end_num = decode_short(data, off) + off += SHORT_OFF class SaoGetDefragMatchLeaguePointRankingListResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_DEFRAG_MATCH_LEAGUE_POINT_RANKING_LIST_RESPONSE) self.result = 1 - self.ranking_user_data_size = 1 # number of arrays + self.ranking_data_list: List[DefragMatchLeaguePointRankingData] = [] - self.rank = 1 - self.user_id = "1" - self.store_id = "123" - self.store_name = "ARTEMiS" - self.nick_name = "PLAYER" - self.setting_title_id = 20005 - self.favorite_hero_log_id = 101000010 - self.favorite_hero_log_awakening_stage = 0 - self.favorite_support_log_id = 0 - self.favorite_support_log_awakening_stage = 0 - self.class_num = 1 - self.total_league_point = 1 - def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "ranking_user_data_size" / Int32ub, # big endian - - "rank" / Int32ub, #int - "user_id_size" / Int32ub, # big endian - "user_id" / Int16ul[len(self.user_id)], - "store_id_size" / Int32ub, # big endian - "store_id" / Int16ul[len(self.store_id)], - "store_name_size" / Int32ub, # big endian - "store_name" / Int16ul[len(self.store_name)], - "nick_name_size" / Int32ub, # big endian - "nick_name" / Int16ul[len(self.nick_name)], - "setting_title_id" / Int32ub, #int - "favorite_hero_log_id" / Int32ub, #int - "favorite_hero_log_awakening_stage" / Int16ub, #short - "favorite_support_log_id" / Int32ub, #int - "favorite_support_log_awakening_stage" / Int16ub, #short - "class_num" / Int16ub, #short - "total_league_point" / Int32ub, #int - ) - - resp_data = resp_struct.build(dict( - result=self.result, - ranking_user_data_size=self.ranking_user_data_size, - - rank=self.rank, - user_id_size=len(self.user_id) * 2, - user_id=[ord(x) for x in self.user_id], - store_id_size=len(self.store_id) * 2, - store_id=[ord(x) for x in self.store_id], - store_name_size=len(self.store_name) * 2, - store_name=[ord(x) for x in self.store_name], - nick_name_size=len(self.nick_name) * 2, - nick_name=[ord(x) for x in self.nick_name], - setting_title_id=self.setting_title_id, - favorite_hero_log_id=self.favorite_hero_log_id, - favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, - favorite_support_log_id=self.favorite_support_log_id, - favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, - class_num=self.class_num, - total_league_point=self.total_league_point, - )) - - self.length = len(resp_data) - return super().make() + resp_data + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.ranking_data_list) class SaoGetDefragMatchLeagueScoreRankingListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.get_rank_start_num = decode_short(data, off) + off += SHORT_OFF + + self.get_rank_end_num = decode_short(data, off) + off += SHORT_OFF class SaoGetDefragMatchLeagueScoreRankingListResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_DEFRAG_MATCH_LEAGUE_SCORE_RANKING_LIST_RESPONSE) self.result = 1 - self.ranking_user_data_size = 1 # number of arrays - - self.rank = 1 - self.user_id = "1" - self.store_id = "123" - self.store_name = "ARTEMiS" - self.nick_name = "PLAYER" - self.setting_title_id = 20005 - self.favorite_hero_log_id = 101000010 - self.favorite_hero_log_awakening_stage = 0 - self.favorite_support_log_id = 0 - self.favorite_support_log_awakening_stage = 0 - self.class_num = 1 - self.have_league_score = 1 + self.ranking_user_data: List[DefragMatchLeagueScoreRankingList] = [] def make(self) -> bytes: # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "ranking_user_data_size" / Int32ub, # big endian - - "rank" / Int32ub, #int - "user_id_size" / Int32ub, # big endian - "user_id" / Int16ul[len(self.user_id)], - "store_id_size" / Int32ub, # big endian - "store_id" / Int16ul[len(self.store_id)], - "store_name_size" / Int32ub, # big endian - "store_name" / Int16ul[len(self.store_name)], - "nick_name_size" / Int32ub, # big endian - "nick_name" / Int16ul[len(self.nick_name)], - "setting_title_id" / Int32ub, #int - "favorite_hero_log_id" / Int32ub, #int - "favorite_hero_log_awakening_stage" / Int16ub, #short - "favorite_support_log_id" / Int32ub, #int - "favorite_support_log_awakening_stage" / Int16ub, #short - "class_num" / Int16ub, #short - "have_league_score" / Int16ub, #short - ) - - resp_data = resp_struct.build(dict( - result=self.result, - ranking_user_data_size=self.ranking_user_data_size, - - rank=self.rank, - user_id_size=len(self.user_id) * 2, - user_id=[ord(x) for x in self.user_id], - store_id_size=len(self.store_id) * 2, - store_id=[ord(x) for x in self.store_id], - store_name_size=len(self.store_name) * 2, - store_name=[ord(x) for x in self.store_name], - nick_name_size=len(self.nick_name) * 2, - nick_name=[ord(x) for x in self.nick_name], - setting_title_id=self.setting_title_id, - favorite_hero_log_id=self.favorite_hero_log_id, - favorite_hero_log_awakening_stage=self.favorite_hero_log_awakening_stage, - favorite_support_log_id=self.favorite_support_log_id, - favorite_support_log_awakening_stage=self.favorite_support_log_awakening_stage, - class_num=self.class_num, - have_league_score=self.have_league_score, - )) - - self.length = len(resp_data) + resp_data = encode_byte(self.result) + resp_data += encode_arr_cls(self.ranking_user_data) return super().make() + resp_data +class SaoGetBeginnerMissionProgressesUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + +class SaoGetBeginnerMissionProgressesUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_BEGINNER_MISSION_PROGRESSES_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionProgressesUserData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetBeginnerMissionSeatProgressesUserDataListRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + +class SaoGetBeginnerMissionSeatProgressesUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_BEGINNER_MISSION_SEAT_PROGRESSES_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionSeatProgressesUserData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetLinkedSiteRegCampaignUserDataRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.linked_site_reg_campaign_id = decode_int(data, off) + off += INT_OFF + +class SaoGetLinkedSiteRegCampaignUserDataResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_LINKED_SITE_REG_CAMPAIGN_USER_DATA_RESPONSE) + self.result = 1 + self.data: List[LinkedSiteRegCampaignUserData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data) + +class SaoGetHeroLogUnitUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_HERO_LOG_UNIT_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.hero_log_unit_user_data_list: List[HeroLogUnitUserData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.hero_log_unit_user_data_list) + +class SaoGetCharaUnitUserDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_CHARA_UNIT_USER_DATA_LIST_RESPONSE) + self.result = 1 + self.chara_unit_user_data_list: List[CharaUnitUserData] = [] + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.chara_unit_user_data_list) + +class SaoBeginnerMissionAdConfirmNotificationRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + class SaoBnidSerialCodeCheckRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.bnid_serial_code, new_off = decode_str(data, off) + off += new_off class SaoBnidSerialCodeCheckResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) - + def __init__(self) -> None: + super().__init__(GameconnectCmd.BNID_SERIAL_CODE_CHECK_RESPONSE) self.result = 1 - self.bnid_item_id = "130050" + self.bnid_item_id = "" self.use_status = 0 - + def make(self) -> bytes: - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "bnid_item_id_size" / Int32ub, # big endian - "bnid_item_id" / Int16ul[len(self.bnid_item_id)], - "use_status" / Int8ul, # result is either 0 or 1 - ) + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.bnid_item_id) \ + + encode_byte(self.use_status) - resp_data = resp_struct.build(dict( - result=self.result, - bnid_item_id_size=len(self.bnid_item_id) * 2, - bnid_item_id=[ord(x) for x in self.bnid_item_id], - use_status=self.use_status, - )) +class SaoBnidSerialCodeEntryByAppendixCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off - self.length = len(resp_data) - return super().make() + resp_data + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.bnid_serial_code, new_off = decode_str(data, off) + off += new_off + +class SaoBnidSerialCodeEntryByAppendixCardResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.BNID_SERIAL_CODE_ENTRY_BY_APPENDIX_CARD_RESPONSE) + self.result = 1 + self.get_bnid_item_id = "" + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.get_bnid_item_id) + +class SaoDischargeProfileCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.hero_log_user_hero_log_id, new_off = decode_str(data, off) + off += new_off + + self.main_weapon_user_equipment_id, new_off = decode_str(data, off) + off += new_off + + self.sub_equipment_user_equipment_id, new_off = decode_str(data, off) + off += new_off + + self.skill_id = decode_int(data, off) + off += INT_OFF + self.text_chara_message_id = decode_int(data, off) + off += INT_OFF + + self.holographic_flag = decode_byte(data, off) + off += BYTE_OFF + self.execute_print_type = PrintType(decode_byte(data, off)) + off += BYTE_OFF + +class SaoDischargeProfileCardResponse(SaoBaseResponse): + def __init__(self, serial: str) -> None: + super().__init__(GameconnectCmd.DISCHARGE_PROFILE_CARD_RESPONSE) + self.result = 1 + self.profile_card_code = serial + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.profile_card_code) + +class SaoDischargeResourceCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.holographic_flag = decode_byte(data, off) + off += BYTE_OFF + + self.execute_print_type = PrintType(decode_byte(data, off)) + off += BYTE_OFF + + self.common_reward_user_data: List[MaterialCommonRewardUserData] = [] # typing lol + self.common_reward_user_data, new_off = decode_arr_cls(data, off, MaterialCommonRewardUserData) + off += new_off + +class SaoDischargeResourceCardResponse(SaoBaseResponse): + def __init__(self, serial: str) -> None: + super().__init__(GameconnectCmd.DISCHARGE_RESOURCE_CARD_RESPONSE) + self.result = 1 + self.profile_card_code = serial + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_str(self.profile_card_code) + +class SaoDischargeResourceCardCompleteRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.resource_card_code, new_off = decode_str(data, off) + off += new_off class SaoScanQrQuestProfileCardRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.profile_card_code, new_off = decode_str(data, off) + off += new_off class SaoScanQrQuestProfileCardResponse(SaoBaseResponse): - def __init__(self, cmd) -> None: - super().__init__(cmd) + def __init__(self) -> None: + super().__init__(GameconnectCmd.SCAN_QR_QUEST_PROFILE_CARD_RESPONSE) self.result = 1 - - # read_profile_card_data - self.profile_card_code = "1234123412341234123" # ID of the QR code - self.nick_name = "PLAYER" - self.rank_num = 1 #short - self.setting_title_id = 20005 #int - self.skill_id = 0 #short - self.hero_log_hero_log_id = 118000230 #int - self.hero_log_log_level = 1 #short - self.hero_log_awakening_stage = 1 #short - - self.hero_log_property1_property_id = 0 #int - self.hero_log_property1_value1 = 0 #int - self.hero_log_property1_value2 = 0 #int - self.hero_log_property2_property_id = 0 #int - self.hero_log_property2_value1 = 0 #int - self.hero_log_property2_value2 = 0 #int - self.hero_log_property3_property_id = 0 #int - self.hero_log_property3_value1 = 0 #int - self.hero_log_property3_value2 = 0 #int - self.hero_log_property4_property_id = 0 #int - self.hero_log_property4_value1 = 0 #int - self.hero_log_property4_value2 = 0 #int - - self.main_weapon_equipment_id = 0 #int - self.main_weapon_enhancement_value = 0 #short - self.main_weapon_awakening_stage = 0 #short - - self.main_weapon_property1_property_id = 0 #int - self.main_weapon_property1_value1 = 0 #int - self.main_weapon_property1_value2 = 0 #int - self.main_weapon_property2_property_id = 0 #int - self.main_weapon_property2_value1 = 0 #int - self.main_weapon_property2_value2 = 0 #int - self.main_weapon_property3_property_id = 0 #int - self.main_weapon_property3_value1 = 0 #int - self.main_weapon_property3_value2 = 0 #int - self.main_weapon_property4_property_id = 0 #int - self.main_weapon_property4_value1 = 0 #int - self.main_weapon_property4_value2 = 0 #int - - self.sub_equipment_equipment_id = 0 #int - self.sub_equipment_enhancement_value = 0 #short - self.sub_equipment_awakening_stage = 0 #short - - self.sub_equipment_property1_property_id = 0 #int - self.sub_equipment_property1_value1 = 0 #int - self.sub_equipment_property1_value2 = 0 #int - self.sub_equipment_property2_property_id = 0 #int - self.sub_equipment_property2_value1 = 0 #int - self.sub_equipment_property2_value2 = 0 #int - self.sub_equipment_property3_property_id = 0 #int - self.sub_equipment_property3_value1 = 0 #int - self.sub_equipment_property3_value2 = 0 #int - self.sub_equipment_property4_property_id = 0 #int - self.sub_equipment_property4_value1 = 0 #int - self.sub_equipment_property4_value2 = 0 #int - - self.holographic_flag = 1 #byte + self.profile_card_data: List[ReadProfileCard] = [] def make(self) -> bytes: - #new stuff + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.profile_card_data) - read_profile_card_data_struct = Struct( - "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 - - ) - - # create a resp struct - resp_struct = Struct( - "result" / Int8ul, # result is either 0 or 1 - "read_profile_card_data_size" / Rebuild(Int32ub, len_(this.read_profile_card_data)), # big endian - "read_profile_card_data" / Array(this.read_profile_card_data_size, read_profile_card_data_struct), - ) - - resp_data = resp_struct.parse(resp_struct.build(dict( - result=self.result, - read_profile_card_data_size=0, - read_profile_card_data=[], - ))) - - hero_data = dict( - 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=self.rank_num, - setting_title_id=self.setting_title_id, - skill_id=self.skill_id, - hero_log_hero_log_id=self.hero_log_hero_log_id, - hero_log_log_level=self.hero_log_log_level, - hero_log_awakening_stage=self.hero_log_awakening_stage, - - hero_log_property1_property_id=self.hero_log_property1_property_id, - hero_log_property1_value1=self.hero_log_property1_value1, - hero_log_property1_value2=self.hero_log_property1_value2, - hero_log_property2_property_id=self.hero_log_property2_property_id, - hero_log_property2_value1=self.hero_log_property2_value1, - hero_log_property2_value2=self.hero_log_property2_value2, - hero_log_property3_property_id=self.hero_log_property3_property_id, - hero_log_property3_value1=self.hero_log_property3_value1, - hero_log_property3_value2=self.hero_log_property3_value2, - hero_log_property4_property_id=self.hero_log_property4_property_id, - hero_log_property4_value1=self.hero_log_property4_value1, - hero_log_property4_value2=self.hero_log_property4_value2, - - main_weapon_equipment_id=self.main_weapon_equipment_id, - main_weapon_enhancement_value=self.main_weapon_enhancement_value, - main_weapon_awakening_stage=self.main_weapon_awakening_stage, - - main_weapon_property1_property_id=self.main_weapon_property1_property_id, - main_weapon_property1_value1=self.main_weapon_property1_value1, - main_weapon_property1_value2=self.main_weapon_property1_value2, - main_weapon_property2_property_id=self.main_weapon_property2_property_id, - main_weapon_property2_value1=self.main_weapon_property2_value1, - main_weapon_property2_value2=self.main_weapon_property2_value2, - main_weapon_property3_property_id=self.main_weapon_property3_property_id, - main_weapon_property3_value1=self.main_weapon_property3_value1, - main_weapon_property3_value2=self.main_weapon_property3_value2, - main_weapon_property4_property_id=self.main_weapon_property4_property_id, - main_weapon_property4_value1=self.main_weapon_property4_value1, - main_weapon_property4_value2=self.main_weapon_property4_value2, - - sub_equipment_equipment_id=self.sub_equipment_equipment_id, - sub_equipment_enhancement_value=self.sub_equipment_enhancement_value, - sub_equipment_awakening_stage=self.sub_equipment_awakening_stage, - - sub_equipment_property1_property_id=self.sub_equipment_property1_property_id, - sub_equipment_property1_value1=self.sub_equipment_property1_value1, - sub_equipment_property1_value2=self.sub_equipment_property1_value2, - sub_equipment_property2_property_id=self.sub_equipment_property2_property_id, - sub_equipment_property2_value1=self.sub_equipment_property2_value1, - sub_equipment_property2_value2=self.sub_equipment_property2_value2, - sub_equipment_property3_property_id=self.sub_equipment_property3_property_id, - sub_equipment_property3_value1=self.sub_equipment_property3_value1, - sub_equipment_property3_value2=self.sub_equipment_property3_value2, - sub_equipment_property4_property_id=self.sub_equipment_property4_property_id, - sub_equipment_property4_value1=self.sub_equipment_property4_value1, - sub_equipment_property4_value2=self.sub_equipment_property4_value2, - - holographic_flag=self.holographic_flag, - ) +class SaoScanQrShopResourceCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off - resp_data.read_profile_card_data.append(hero_data) + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.resource_card_code, new_off = decode_str(data, off) + off += new_off - resp_data["read_profile_card_data_size"] = len(resp_data.read_profile_card_data) +class SaoScanQrQuestResourceCardRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.resource_card_code, new_off = decode_str(data, off) + off += new_off - # finally, rebuild the resp_data - resp_data = resp_struct.build(resp_data) - - self.length = len(resp_data) - return super().make() + resp_data +class SaoScanQrQuestResourceCardResponse(SaoBaseResponse): + def __init__(self, reward_type: int = 0, reward_id: int = 0, is_holo: bool = False) -> None: + super().__init__(GameconnectCmd.SCAN_QR_QUEST_RESOURCE_CARD_RESPONSE) + self.result = 1 + self.common_reward_type = reward_type + self.common_reward_id = reward_id + self.holographic_flag = is_holo + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_short(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_bool(self.holographic_flag) + class SaoConsumeCreditGuestRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3047,7 +1694,7 @@ class SaoConsumeCreditGuestRequest(SaoBaseRequest): self.cab_type = decode_byte(data, off) off += BYTE_OFF - self.act_type = decode_byte(data, off) + self.act_type = ActTypeConsumeCredit(decode_byte(data, off)) off += BYTE_OFF self.consume_num = decode_byte(data, off) @@ -3065,20 +1712,14 @@ class SaoChangePartyRequest(SaoBaseRequest): self.user_id = user_id[0] off += user_id[1] - self.act_type = decode_byte(data, off) + self.act_type = ActTypeChangeParty(decode_byte(data, off)) off += BYTE_OFF - self.party_data_count = decode_int(data, off) - off += INT_OFF - self.party_data_list: List[PartyData] = [] + self.party_data_list, new_off = decode_arr_cls(data, off, PartyData) + off += new_off - for _ in range(self.party_data_count): - tmp = PartyData(data, off) - self.party_data_list.append(tmp) - off += tmp.get_size() - -class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): +class SaoTrialTowerPlayEndUnanalyzedLogFixedRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) off = 0 @@ -3096,17 +1737,20 @@ class TrialTowerPlayEndUnanalyzedLogFixed(SaoBaseRequest): self.rarity_up_exec_flag = decode_byte(data, off) off += BYTE_OFF -class GetShopResourceSalesDataListRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - off = 0 - user_id = decode_str(data, off) - self.user_id = user_id[0] - off += user_id[1] +class SaoTrialTowerPlayEndUnanalyzedLogFixedResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.TRIAL_TOWER_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE) + self.result = 1 + self.play_end_unanalyzed_log_reward_data_list: List[QuestScenePlayEndUnanalyzedLogRewardData] = [] -class GetShopResourceSalesDataListResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.play_end_unanalyzed_log_reward_data_list) + +class SaoGetShopResourceSalesDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_SHOP_RESOURCE_SALES_DATA_LIST_RESPONSE) self.result = 1 # byte self.shop_resource_sales_data: List[ShopResourceSalesData] = [] @@ -3117,6 +1761,19 @@ class GetShopResourceSalesDataListResponse(SaoBaseResponse): self.header.length = len(ret) return super().make() + ret +class SaoPurchaseShopResourceRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.user_shop_resource_id, new_off = decode_str(data, off) + off += new_off + class GetYuiMedalShopUserDataListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3159,78 +1816,6 @@ class GetGashaMedalShopUserDataListResponse(SaoBaseResponse): self.header.length = len(ret) return super().make() + ret -class GetMYuiMedalShopDataRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.dummy = decode_byte(data, 0) - -class GetMYuiMedalShopDataResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.data_list: List[YuiMedalShopData] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.data_list) - - self.header.length = len(ret) - return super().make() + ret - -class GetMYuiMedalShopItemsRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.dummy = decode_byte(data, 0) - -class GetMYuiMedalShopItemsResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.data_list: List[YuiMedalShopItemData] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.data_list) - - self.header.length = len(ret) - return super().make() + ret - -class GetMGashaMedalShopsRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.dummy = decode_byte(data, 0) - -class GetMGashaMedalShopsResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.data_list: List[GashaMedalShop] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.data_list) - - self.header.length = len(ret) - return super().make() + ret - -class GetMResEarnCampaignShopsRequest(SaoBaseRequest): - def __init__(self, header: SaoRequestHeader, data: bytes) -> None: - super().__init__(header, data) - self.dummy = decode_byte(data, 0) - -class GetMResEarnCampaignShopsResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - self.data_list: List[ResEarnCampaignShop] = [] - - def make(self) -> bytes: - ret = encode_byte(self.result) - ret += encode_arr_cls(self.data_list) - - self.header.length = len(ret) - return super().make() + ret - class SaoGiveFreeTicketRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3244,15 +1829,6 @@ class SaoGiveFreeTicketRequest(SaoBaseRequest): self.give_num = decode_byte(data, off) off += BYTE_OFF -class SaoGiveFreeTicketResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - - def make(self) -> bytes: - ret = encode_byte(self.result) - return super().make() + ret - class SaoLogoutTicketUnpurchasedRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3266,15 +1842,6 @@ class SaoLogoutTicketUnpurchasedRequest(SaoBaseRequest): self.cabinet_type = decode_byte(data, off) off += BYTE_OFF -class SaoLogoutTicketUnpurchasedResponse(SaoBaseResponse): - def __init__(self, cmd_id: int) -> None: - super().__init__(cmd_id) - self.result = 1 # byte - - def make(self) -> bytes: - ret = encode_byte(self.result) - return super().make() + ret - class SaoGetQuestHierarchyProgressDegreesRankingListRequest(SaoBaseRequest): def __init__(self, header: SaoRequestHeader, data: bytes) -> None: super().__init__(header, data) @@ -3322,3 +1889,2337 @@ class SaoGetQuestPopularHeroLogRankingListResponse(SaoBaseResponse): ret = encode_byte(self.result) ret += encode_arr_cls(self.quest_popular_hero_log_ranking_data_list) return super().make() + ret + +class SaoGetVariousTutorialDataListResponse(SaoBaseResponse): + def __init__(self) -> None: + super().__init__(GameconnectCmd.GET_VARIOUS_TUTORIAL_DATA_LIST_RESPONSE) + self.result = 1 + self.end_tutorial_type_list: List[int] = [] + + def make(self) -> bytes: + ret = encode_byte(self.result) + ret += encode_arr_num(self.end_tutorial_type_list, BYTE_OFF) + return super().make() + ret + +class SaoVariousTutorialEndRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ticket_id, new_off = decode_str(data, off) + off += new_off + self.user_id, new_off = decode_str(data, off) + off += new_off + self.tutorial_type = decode_byte(data, off) + off += BYTE_OFF + +class SaoGetMExTowersRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ex_tower_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMExTowerQuestsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.ex_tower_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMChatEventStoriesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.event_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntWholeTasksRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntIndividualTasksRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntSpecialEffectsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntEventPointRewardCommonRewardsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMTreasureHuntEventPointRewardTitlesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.treasure_hunt_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchSeedRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchSpecialEffectsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchGradesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchPeriodBonusesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchRandomBonusTablesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMDefragMatchRareDropsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.defrag_match_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMEventScenesRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.event_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMBeginnerMissionConditionsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.beginner_mission_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMBeginnerMissionRewardsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.beginner_mission_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMBeginnerMissionSeatConditionsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.beginner_mission_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMBeginnerMissionSeatRewardsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.beginner_mission_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMEventMonstersRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.event_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMGashaMedalSettingsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.gasha_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMGashaMedalShopItemsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.gasha_medal_shop_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMResEarnCampaignApplicationProductsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.res_earn_campaign_application_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMResEarnCampaignShopItemsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.res_earn_campaign_shop_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMPlayCampaignRewardsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.play_campaign_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMLinkedSiteRegCampaignRewardsRequest(SaoBaseRequest): + def __init__(self, header: SaoRequestHeader, data: bytes) -> None: + super().__init__(header, data) + off = 0 + self.linked_site_reg_campaign_id_list, new_off = decode_arr_num(data, off, INT_OFF) + off += new_off + +class SaoGetMPlayerRanksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PLAYER_RANKS_RESPONSE) + self.result = 1 + self.data_list: List[PlayerRankData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PlayerRankData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTitlesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TITLES_RESPONSE) + self.result = 1 + self.data_list: List[TitleData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TitleData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMFragmentsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_FRAGMENTS_RESPONSE) + self.result = 1 + self.data_list: List[FragmentData] = [] + + if data_list: + for item in data_list: + self.data_list.append(FragmentData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMRewardTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_REWARD_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[RewardTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(RewardTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMRewardSetsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_REWARD_SETS_RESPONSE) + self.result = 1 + self.data_list: List[RewardSetData] = [] + + if data_list: + for item in data_list: + self.data_list.append(RewardSetData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMUnanalyzedLogGradesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_UNANALYZED_LOG_GRADES_RESPONSE) + self.result = 1 + self.data_list: List[UnanalyzedLogGradeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(UnanalyzedLogGradeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAppointLeaderParamsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_APPOINT_LEADER_PARAMS_RESPONSE) + self.result = 1 + self.data_list: List[AppointLeaderParamData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AppointLeaderParamData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAppointLeaderEffectsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_APPOINT_LEADER_EFFECTS_RESPONSE) + self.result = 1 + self.data_list: List[AppointLeaderEffectData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AppointLeaderEffectData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAppointLeaderEffectTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_APPOINT_LEADER_EFFECT_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[AppointLeaderEffectTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AppointLeaderEffectTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMRaritiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_RARITIES_RESPONSE) + self.result = 1 + self.data_list: List[RarityData] = [] + + if data_list: + for item in data_list: + self.data_list.append(RarityData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCompositionEventsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_COMPOSITION_EVENTS_RESPONSE) + self.result = 1 + self.data_list: List[CompositionEventData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CompositionEventData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCompositionParamsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_COMPOSITION_PARAMS_RESPONSE) + self.result = 1 + self.data_list: List[CompositionParamData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CompositionParamData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGamePlayPricesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GAME_PLAY_PRICES_RESPONSE) + self.result = 1 + self.data_list: List[GamePlayPriceData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GamePlayPriceData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBuyTicketsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BUY_TICKETS_RESPONSE) + self.result = 1 + self.data_list: List[BuyTicketData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BuyTicketData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTipsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TIPS_RESPONSE) + self.result = 1 + self.data_list: List[TipsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TipsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCapsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CAPS_RESPONSE) + self.result = 1 + self.data_list: List[CapData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CapData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMHeroLogResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_HERO_LOG_RESPONSE) + self.result = 1 + self.data_list: List[HeroLogData] = [] + + if data_list: + for item in data_list: + self.data_list.append(HeroLogData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMHeroLogLevelsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_HERO_LOG_LEVELS_RESPONSE) + self.result = 1 + self.data_list: List[HeroLogLevelData] = [] + + if data_list: + for item in data_list: + self.data_list.append(HeroLogLevelData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMHeroLogRolesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_HERO_LOG_ROLES_RESPONSE) + self.result = 1 + self.data_list: List[HeroLogRoleData] = [] + + if data_list: + for item in data_list: + self.data_list.append(HeroLogRoleData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMHeroLogTrustRanksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_HERO_LOG_TRUST_RANKS_RESPONSE) + self.result = 1 + self.data_list: List[HeroLogTrustRankData] = [] + + if data_list: + for item in data_list: + self.data_list.append(HeroLogTrustRankData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCharasResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHARAS_RESPONSE) + self.result = 1 + self.data_list: List[CharaData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CharaData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCharaFriendlyRanksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHARA_FRIENDLY_RANKS_RESPONSE) + self.result = 1 + self.data_list: List[CharaFriendlyRankData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CharaFriendlyRankData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEquipmentsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EQUIPMENTS_RESPONSE) + self.result = 1 + self.data_list: List[EquipmentData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EquipmentData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEquipmentLevelsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EQUIPMENT_LEVELS_RESPONSE) + self.result = 1 + self.data_list: List[EquipmentLevelData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EquipmentLevelData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMWeaponTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_WEAPON_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[WeaponTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(WeaponTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[ItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMItemTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ITEM_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[ItemTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ItemTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBuffItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BUFF_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[BuffItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BuffItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEnemiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ENEMIES_RESPONSE) + self.result = 1 + self.data_list: List[EnemyData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EnemyData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEnemySetsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ENEMY_SETS_RESPONSE) + self.result = 1 + self.data_list: List[EnemySetData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EnemySetData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEnemyKindsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ENEMY_KINDS_RESPONSE) + self.result = 1 + self.data_list: List[EnemyKindData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EnemyKindData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEnemyCategoriesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_ENEMY_CATEGORIES_RESPONSE) + self.result = 1 + self.data_list: List[EnemyCategoryData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EnemyCategoryData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMUnitsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_UNITS_RESPONSE) + self.result = 1 + self.data_list: List[UnitData] = [] + + if data_list: + for item in data_list: + self.data_list.append(UnitData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMUnitGimmicksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_UNIT_GIMMICKS_RESPONSE) + self.result = 1 + self.data_list: List[UnitGimmickData] = [] + + if data_list: + for item in data_list: + self.data_list.append(UnitGimmickData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMUnitCollisionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_UNIT_COLLISIONS_RESPONSE) + self.result = 1 + self.data_list: List[UnitCollisionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(UnitCollisionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMUnitPowersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_UNIT_POWERS_RESPONSE) + self.result = 1 + self.data_list: List[UnitPowerData] = [] + + if data_list: + for item in data_list: + self.data_list.append(UnitPowerData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGimmickAttacksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GIMMICK_ATTACKS_RESPONSE) + self.result = 1 + self.data_list: List[GimmickAttackData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GimmickAttackData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMCharaAttacksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHARA_ATTACKS_RESPONSE) + self.result = 1 + self.data_list: List[CharaAttackData] = [] + + if data_list: + for item in data_list: + self.data_list.append(CharaAttackData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBossAttacksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BOSS_ATTACKS_RESPONSE) + self.result = 1 + self.data_list: List[BossAttackData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BossAttackData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMonsterAttacksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MONSTER_ATTACKS_RESPONSE) + self.result = 1 + self.data_list: List[MonsterAttackData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MonsterAttackData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMonsterActionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MONSTER_ACTIONS_RESPONSE) + self.result = 1 + self.data_list: List[MonsterActionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MonsterActionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPropertiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PROPERTIES_RESPONSE) + self.result = 1 + self.data_list: List[PropertyData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PropertyData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPropertyTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PROPERTY_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[PropertyTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PropertyTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPropertyTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PROPERTY_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[PropertyTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PropertyTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSkillsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SKILLS_RESPONSE) + self.result = 1 + self.data_list: List[SkillData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SkillData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSkillTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SKILL_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[SkillTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SkillTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSkillLevelsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SKILL_LEVELS_RESPONSE) + self.result = 1 + self.data_list: List[SkillLevelData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SkillLevelData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAwakeningsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_AWAKENINGS_RESPONSE) + self.result = 1 + self.data_list: List[AwakeningData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AwakeningData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSynchroSkillsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SYNCHRO_SKILLS_RESPONSE) + self.result = 1 + self.data_list: List[SynchroSkillData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SynchroSkillData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSoundSkillCutInVoicesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SOUND_SKILL_CUT_IN_VOICES_RESPONSE) + self.result = 1 + self.data_list: List[SoundSkillCutInVoiceData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SoundSkillCutInVoiceData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestScenesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_SCENES_RESPONSE) + self.result = 1 + self.data_list: List[QuestSceneData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestSceneData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestExistUnitsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_EXIST_UNITS_RESPONSE) + self.result = 1 + self.data_list: List[QuestExistUnitData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestExistUnitData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestEpisodeAppendRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_EPISODE_APPEND_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[QuestEpisodeAppendRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestEpisodeAppendRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSideQuestsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SIDE_QUESTS_RESPONSE) + self.result = 1 + self.data_list: List[SideQuestData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SideQuestData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEpisodesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EPISODES_RESPONSE) + self.result = 1 + self.data_list: List[EpisodeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EpisodeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEpisodeChaptersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EPISODE_CHAPTERS_RESPONSE) + self.result = 1 + self.data_list: List[EpisodeChapterData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EpisodeChapterData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEpisodePartsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EPISODE_PARTS_RESPONSE) + self.result = 1 + self.data_list: List[EpisodePartData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EpisodePartData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTrialTowersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TRIAL_TOWERS_RESPONSE) + self.result = 1 + self.data_list: List[TrialTowerData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TrialTowerData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMExTowersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EX_TOWERS_RESPONSE) + self.result = 1 + self.data_list: List[ExTowerData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ExTowerData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMExTowerQuestsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EX_TOWER_QUESTS_RESPONSE) + self.result = 1 + self.data_list: List[ExTowerQuestData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ExTowerQuestData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMenuDisplayEnemiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MENU_DISPLAY_ENEMIES_RESPONSE) + self.result = 1 + self.data_list: List[MenuDisplayEnemyData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MenuDisplayEnemyData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMissionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MISSIONS_RESPONSE) + self.result = 1 + self.data_list: List[MissionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MissionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMissionTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MISSION_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[MissionTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MissionTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMMissionDifficultiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_MISSION_DIFFICULTIES_RESPONSE) + self.result = 1 + self.data_list: List[MissionDifficultyData] = [] + + if data_list: + for item in data_list: + self.data_list.append(MissionDifficultyData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBattleCamerasResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BATTLE_CAMERAS_RESPONSE) + self.result = 1 + self.data_list: List[BattleCameraData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BattleCameraData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMChatMainStoriesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHAT_MAIN_STORIES_RESPONSE) + self.result = 1 + self.data_list: List[ChatMainStoryData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ChatMainStoryData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMChatSideStoriesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHAT_SIDE_STORIES_RESPONSE) + self.result = 1 + self.data_list: List[ChatSideStoryData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ChatSideStoryData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMChatEventStoriesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_CHAT_EVENT_STORIES_RESPONSE) + self.result = 1 + self.data_list: List[ChatEventStoryData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ChatEventStoryData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMNavigatorCharasResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_NAVIGATOR_CHARAS_RESPONSE) + self.result = 1 + self.data_list: List[NavigatorCharaData] = [] + + if data_list: + for item in data_list: + self.data_list.append(NavigatorCharaData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMNavigatorCommentsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_NAVIGATOR_COMMENTS_RESPONSE) + self.result = 1 + self.data_list: List[NavigatorCommentData] = [] + + if data_list: + for item in data_list: + self.data_list.append(NavigatorCommentData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMExBonusTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EX_BONUS_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[ExBonusTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ExBonusTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMExBonusConditionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EX_BONUS_CONDITIONS_RESPONSE) + self.result = 1 + self.data_list: List[ExBonusConditionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ExBonusConditionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestRareDropsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_RARE_DROPS_RESPONSE) + self.result = 1 + self.data_list: List[QuestRareDropData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestRareDropData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestSpecialRareDropSettingsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_SPECIAL_RARE_DROP_SETTINGS_RESPONSE) + self.result = 1 + self.data_list: List[QuestSpecialRareDropSettingData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestSpecialRareDropSettingData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestSpecialRareDropsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_SPECIAL_RARE_DROPS_RESPONSE) + self.result = 1 + self.data_list: List[QuestSpecialRareDropData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestSpecialRareDropData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestTutorialsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_TUTORIALS_RESPONSE) + self.result = 1 + self.data_list: List[QuestTutorialData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestTutorialData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestPlayerTraceTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_PLAYER_TRACE_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[QuestPlayerTraceTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestPlayerTraceTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestStillsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_STILLS_RESPONSE) + self.result = 1 + self.data_list: List[QuestStillData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestStillData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashasResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHAS_RESPONSE) + self.result = 1 + self.data_list: List[GashaData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaHeadersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_HEADERS_RESPONSE) + self.result = 1 + self.data_list: List[GashaHeaderData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaHeaderData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaLotteryRaritiesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_LOTTERY_RARITIES_RESPONSE) + self.result = 1 + self.data_list: List[GashaLotteryRarityData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaLotteryRarityData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaPrizesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_PRIZES_RESPONSE) + self.result = 1 + self.data_list: List[GashaPrizeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaPrizeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMComebackEventsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_COMEBACK_EVENTS_RESPONSE) + self.result = 1 + self.data_list: List[ComebackEventData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ComebackEventData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAdBannersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_AD_BANNERS_RESPONSE) + self.result = 1 + self.data_list: List[AdBannerData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AdBannerData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEventsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EVENTS_RESPONSE) + self.result = 1 + self.data_list: List[EventsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EventsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNTS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntWholeTasksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_WHOLE_TASKS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntWholeTasksData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntWholeTasksData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntIndividualTasksResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_INDIVIDUAL_TASKS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntIndividualTasksData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntIndividualTasksData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntSpecialEffectsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_SPECIAL_EFFECTS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntSpecialEffectsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntSpecialEffectsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntEventPointRewardCommonRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_COMMON_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntEventPointRewardCommonRewardsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntEventPointRewardCommonRewardsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntEventPointRewardTitlesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_TITLES_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntEventPointRewardTitlesData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntEventPointRewardTitlesData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMTreasureHuntTaskTextsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_TREASURE_HUNT_TASK_TEXTS_RESPONSE) + self.result = 1 + self.data_list: List[TreasureHuntTaskTextData] = [] + + if data_list: + for item in data_list: + self.data_list.append(TreasureHuntTaskTextData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBnidSerialCodesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BNID_SERIAL_CODES_RESPONSE) + self.result = 1 + self.data_list: List[BnidSerialCodeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BnidSerialCodeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBnidSerialCodeRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BNID_SERIAL_CODE_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[BnidSerialCodeRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BnidSerialCodeRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSupportLogResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SUPPORT_LOG_RESPONSE) + self.result = 1 + self.data_list: List[SupportLogData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SupportLogData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMSupportLogTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_SUPPORT_LOG_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[SupportLogTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(SupportLogTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEpisodeAppendsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EPISODE_APPENDS_RESPONSE) + self.result = 1 + self.data_list: List[EpisodeAppendData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EpisodeAppendData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestDefragMatchQuestsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_DEFRAG_MATCH_QUESTS_RESPONSE) + self.result = 1 + self.data_list: List[QuestDefragMatchQuestData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestDefragMatchQuestData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestDefragMatchQuestBossTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_DEFRAG_MATCH_QUEST_BOSS_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[QuestDefragMatchQuestBossTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestDefragMatchQuestBossTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCHES_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchSeedResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_SEED_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchSeedData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchSeedData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchSpecialEffectsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_SPECIAL_EFFECTS_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchSpecialEffectData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchSpecialEffectData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchGradesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_GRADES_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchGradeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchGradeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchCpuUnitsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_CPU_UNITS_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchCpuUnitData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchCpuUnitData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchCpuSupportLogsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_CPU_SUPPORT_LOGS_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchCpuSupportLogData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchCpuSupportLogData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchPeriodBonusesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_PERIOD_BONUSES_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchPeriodBonusData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchPeriodBonusData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchRandomBonusTablesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_RANDOM_BONUS_TABLES_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchRandomBonusTableData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchRandomBonusTableData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchRandomBonusConditionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_RANDOM_BONUS_CONDITIONS_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchRandomBonusConditionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchRandomBonusConditionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMDefragMatchRareDropsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_DEFRAG_MATCH_RARE_DROPS_RESPONSE) + self.result = 1 + self.data_list: List[DefragMatchRareDropData] = [] + + if data_list: + for item in data_list: + self.data_list.append(DefragMatchRareDropData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMYuiMedalShopsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_YUI_MEDAL_SHOPS_RESPONSE) + self.result = 1 + self.data_list: List[YuiMedalShopData] = [] + + if data_list: + for item in data_list: + self.data_list.append(YuiMedalShopData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMYuiMedalShopItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_YUI_MEDAL_SHOP_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[YuiMedalShopItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(YuiMedalShopItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEventScenesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EVENT_SCENES_RESPONSE) + self.result = 1 + self.data_list: List[EventSceneData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EventSceneData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGenericCampaignPeriodsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GENERIC_CAMPAIGN_PERIODS_RESPONSE) + self.result = 1 + self.data_list: List[GenericCampaignPeriodData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GenericCampaignPeriodData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBeginnerMissionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BEGINNER_MISSIONS_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BeginnerMissionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBeginnerMissionConditionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BEGINNER_MISSION_CONDITIONS_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionConditionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BeginnerMissionConditionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBeginnerMissionRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BEGINNER_MISSION_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BeginnerMissionRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBeginnerMissionSeatConditionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BEGINNER_MISSION_SEAT_CONDITIONS_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionSeatConditionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BeginnerMissionSeatConditionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMBeginnerMissionSeatRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_BEGINNER_MISSION_SEAT_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[BeginnerMissionSeatRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(BeginnerMissionSeatRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEventItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EVENT_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[EventItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EventItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMEventMonstersResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_EVENT_MONSTERS_RESPONSE) + self.result = 1 + self.data_list: List[EventMonsterData] = [] + + if data_list: + for item in data_list: + self.data_list.append(EventMonsterData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMYuiMedalBonusesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_YUI_MEDAL_BONUSES_RESPONSE) + self.result = 1 + self.data_list: List[YuiMedalBonusData] = [] + + if data_list: + for item in data_list: + self.data_list.append(YuiMedalBonusData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMYuiMedalBonusConditionsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_YUI_MEDAL_BONUS_CONDITIONS_RESPONSE) + self.result = 1 + self.data_list: List[YuiMedalBonusConditionData] = [] + + if data_list: + for item in data_list: + self.data_list.append(YuiMedalBonusConditionData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDALS_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalTypesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDAL_TYPES_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalTypeData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalTypeData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalSettingsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDAL_SETTINGS_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalSettingData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalSettingData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalBonusesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDAL_BONUSES_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalBonusData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalBonusData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalShopsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDAL_SHOPS_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalShopData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalShopData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaMedalShopItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_MEDAL_SHOP_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[GashaMedalShopItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaMedalShopItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMResEarnCampaignApplicationsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_RES_EARN_CAMPAIGN_APPLICATIONS_RESPONSE) + self.result = 1 + self.data_list: List[ResEarnCampaignApplicationData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ResEarnCampaignApplicationData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMResEarnCampaignApplicationProductsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_RES_EARN_CAMPAIGN_APPLICATION_PRODUCTS_RESPONSE) + self.result = 1 + self.data_list: List[ResEarnCampaignApplicationProductData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ResEarnCampaignApplicationProductData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMResEarnCampaignShopsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_RES_EARN_CAMPAIGN_SHOPS_RESPONSE) + self.result = 1 + self.data_list: List[ResEarnCampaignShopData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ResEarnCampaignShopData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMResEarnCampaignShopItemsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_RES_EARN_CAMPAIGN_SHOP_ITEMS_RESPONSE) + self.result = 1 + self.data_list: List[ResEarnCampaignShopItemData] = [] + + if data_list: + for item in data_list: + self.data_list.append(ResEarnCampaignShopItemData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPayingYuiMedalBonusesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PAYING_YUI_MEDAL_BONUSES_RESPONSE) + self.result = 1 + self.data_list: List[PayingYuiMedalBonusData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PayingYuiMedalBonusData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMAcLoginBonusesResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_AC_LOGIN_BONUSES_RESPONSE) + self.result = 1 + self.data_list: List[AcLoginBonusData] = [] + + if data_list: + for item in data_list: + self.data_list.append(AcLoginBonusData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPlayCampaignsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PLAY_CAMPAIGNS_RESPONSE) + self.result = 1 + self.data_list: List[PlayCampaignData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PlayCampaignData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMPlayCampaignRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_PLAY_CAMPAIGN_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[PlayCampaignRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(PlayCampaignRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMGashaFreeCampaignsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_GASHA_FREE_CAMPAIGNS_RESPONSE) + self.result = 1 + self.data_list: List[GashaFreeCampaignData] = [] + + if data_list: + for item in data_list: + self.data_list.append(GashaFreeCampaignData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMQuestDropBoostCampaignsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_QUEST_DROP_BOOST_CAMPAIGNS_RESPONSE) + self.result = 1 + self.data_list: List[QuestDropBoostCampaignData] = [] + + if data_list: + for item in data_list: + self.data_list.append(QuestDropBoostCampaignData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMFirstTicketPurchaseCampaignsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_FIRST_TICKET_PURCHASE_CAMPAIGNS_RESPONSE) + self.result = 1 + self.data_list: List[FirstTicketPurchaseCampaignData] = [] + + if data_list: + for item in data_list: + self.data_list.append(FirstTicketPurchaseCampaignData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMLinkedSiteRegCampaignsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_LINKED_SITE_REG_CAMPAIGNS_RESPONSE) + self.result = 1 + self.data_list: List[LinkedSiteRegCampaignsData] = [] + + if data_list: + for item in data_list: + self.data_list.append(LinkedSiteRegCampaignsData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + +class SaoGetMLinkedSiteRegCampaignRewardsResponse(SaoBaseResponse): + def __init__(self, data_list: List[Dict]) -> None: + super().__init__(GameconnectCmd.GET_M_LINKED_SITE_REG_CAMPAIGN_REWARDS_RESPONSE) + self.result = 1 + self.data_list: List[LinkedSiteRegCampaignRewardData] = [] + + if data_list: + for item in data_list: + self.data_list.append(LinkedSiteRegCampaignRewardData.from_args(item)) + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.result) \ + + encode_arr_cls(self.data_list) + diff --git a/titles/sao/handlers/helpers.py b/titles/sao/handlers/helpers.py index 97a15b2..6b91c7b 100644 --- a/titles/sao/handlers/helpers.py +++ b/titles/sao/handlers/helpers.py @@ -1,7 +1,8 @@ -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Dict import struct import logging from datetime import datetime +from enum import IntEnum BIGINT_OFF = 16 LONG_OFF = 8 @@ -78,31 +79,64 @@ def decode_arr_str(data: bytes, offset: int) -> Tuple[List[str], int]: return (ret, size) +def decode_date_str(data: bytes, offset: int) -> Tuple[datetime, int]: + s, new_o = decode_str(data, offset) + return (prs_dt(s), new_o) + +def decode_bool(data: bytes, offset: int) -> bool: + return bool(decode_byte(data, offset)) + def encode_byte(data: int) -> bytes: - return struct.pack("!b", data) + if data is None: + return b"\0" + try: + return struct.pack("!b", int(data)) + except Exception as e: + logging.getLogger('sao').error(f"Failed to encode {data} as byte! - {e}") + return b"\0" def encode_short(data: int) -> bytes: - return struct.pack("!h", data) + if data is None: + return b"\0\0" + try: + return struct.pack("!h", int(data)) + except Exception as e: + logging.getLogger('sao').error(f"Failed to encode {data} as short! - {e}") + return b"\0\0" def encode_int(data: int) -> bytes: - return struct.pack("!i", data) + if data is None: + return b"\0\0\0\0" + try: + return struct.pack("!i", int(data)) + except Exception as e: + logging.getLogger('sao').error(f"Failed to encode {data} as int! - {e}") + return b"\0\0\0\0" def encode_long(data: int) -> bytes: - return struct.pack("!l", data) + if data is None: + return b"\0\0\0\0\0\0\0\0" + return struct.pack("!l", int(data)) def encode_bigint(data: int) -> bytes: - return struct.pack("!q", data) + if data is None: + return b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + return struct.pack("!q", int(data)) def encode_str(s: str) -> bytes: + if s is None: + return b"\0\0\0\0" try: - str_bytes = s.encode("utf-16-le", errors="replace") + str_bytes = str(s).encode("utf-16-le", errors="replace") str_len_bytes = struct.pack("!I", len(str_bytes)) return str_len_bytes + str_bytes - except: - logging.getLogger('sao').error(f"Failed to encode {s} as bytes!") + except Exception as e: + logging.getLogger('sao').error(f"Failed to encode {s} as bytes! - {e}") return b"" def encode_arr_num(data: List[int], element_size: int) -> bytes: + if data is None: + return b"\0\0\0\0" ret = encode_int(len(data)) if element_size == BYTE_OFF: @@ -126,6 +160,120 @@ def encode_arr_num(data: List[int], element_size: int) -> bytes: return ret +def encode_bool(b: bool) -> bytes: + if b is None: + return b"\0" + return encode_byte(int(b)) + +def encode_date_str(d: datetime) -> bytes: + return encode_str(fmt_dt(d)) + +class PrintType(IntEnum): + NONE = 0 + FromStorage = 1 + FromGasha = 2 + +class AuthType(IntEnum): + UNKNOWN = 0 + CARD = 1 + MOBLE = 2 + +class ActTypeConsumeTicket(IntEnum): + QuestEpisodeBeginner = 1 + QuestEpisode = 2 + QuestTrialTowerBeginner = 3 + QuestTrialTower = 4 + QuestPvEBeginner = 5 + QuestPvE = 6 + QuestContinue = 7 + QuesRarityUp = 8 + QuestEpisodeBoost = 9 + QuestTrialTowerBoost = 10 + QuestPvEBoost = 11 + CustomModeExtend = 21 + CustomModeRetry = 22 + YuiMedalShop = 31 + +class ActTypeChangeParty(IntEnum): + MANUAL = 1 + AUTO = 2 + +class ActTypeConsumeCredit(IntEnum): + PurchaseTicketA1 = 1 + PurchaseTicketA2 = 2 + PurchaseTicketA6 = 3 + PurchaseTicketB1 = 4 + PurchaseTicketB2 = 5 + PurchaseTicketB7 = 6 + PurchaseTicketC1 = 7 + PurchaseTicketC3 = 8 + PurchaseTicketC8 = 9 + PurchaseTicketD1 = 10 + PurchaseTicketD2 = 11 + PurchaseTicketD6 = 12 + PurchaseTicketE1 = 13 + PurchaseTicketE3 = 14 + PurchaseTicketE8 = 15 + PurchaseTicketF1 = 16 + PurchaseTicketF2 = 17 + PurchaseTicketF6 = 18 + QuestContinue = 21 + QuestRarityUp = 22 + GashaSatelite = 31 + GashaSateliteAdd1 = 32 + GashaSateliteAdd2 = 33 + GashaTerminal = 41 + GashaTerminalAdd1 = 42 + GashaTerminalAdd2 = 43 + ResourceDischarge = 44 + ResourceHolo = 45 + ProfileCardDischarge = 46 + ProfileCardHolo = 47 + +class ProtocolErrorNum(IntEnum): # header error_num field + SUCCESS = 0 + + ALREADY_LOGIN_DIFF = 1301 + ALREADY_LOGIN_SAME = 1302 + INVALID_AUTH_TYPE = 1303 + INVALID_CABINET_TYPE = 1304 + AMID_SERVER_CONNECT_ERROR = 1305 + AMID_INFO_REQUEST_ERROR = 1306 + NOT_EXIST_PLAY_DATA = 1307 + HAVE_NEVER_PLAYED_SATELLITE = 1308 + + RESOURCE_CARD_ERR1 = 4831 # ScanQRQuestProfileCard + RESOURCE_CARD_NOT_EXIST = 4832 + RESOURCE_CARD_ERR3 = 4833 # ScanQRQuestResourceCard / ScanQRShopResourceCard + RESOURCE_CARD_ERR6 = 4836 # ScanQRQuestResourceCard / ScanQRShopResourceCard + RESOURCE_CARD_ERR7 = 4837 # ScanQRQuestResourceCard / ScanQRShopResourceCard + RESOURCE_CARD_ERR8 = 4838 # ScanQRShopResourceCard + RESOURCE_CARD_ERR9 = 4839 # ScanQRQuestProfileCard + + CREDIT_GASHA_ERROR = 7111 + + PURCHASE_ERROR = 7711 + + PAYING_PLAY_END_ERROR = 9120 + + CODE_ANALYSIS_API_NOT_RESPONSE = 9201 + CODE_ANALYSIS_API_INIQUITY_SERIAL_CODE = 9202 + CODE_USE_API_USED_SERIAL_CODE = 9203 + CODE_USE_API_NOT_RESPONSE = 9206 + CODE_USE_API_NG = 9207 + CODE_USE_API_LOCK = 9208 + CODE_USE_API_MAINTENANCE = 9209 + CODE_ANALYSIS_API_NOT_MASTER_DATA = 9210 + CODE_ANALYSIS_API_EXPIRED_SERIAL_CODE = 9211 + +class ProtocolResult(IntEnum): # result field, if used + NONE = -1 + FAILED = 0 + SUCCESS = 1 + PARAM_ERROR = 2 + MAINTENANCE = 3 + CONNECT_ERROR = 99 + class BaseHelper: def __init__(self, data: bytes, offset: int) -> None: self._sz = 0 @@ -143,9 +291,10 @@ class BaseHelper: def decode_arr_cls(data: bytes, offset: int, cls: BaseHelper): size = 0 num_cls = decode_int(data, offset + size) + size += INT_OFF cls_type = type(cls) - ret: List[cls_type] = [] + ret: List[cls_type] = [] # type: ignore for _ in range(num_cls): tmp = cls(data, offset + size) size += tmp.get_size() @@ -171,65 +320,1303 @@ class MaterialCommonRewardUserData(BaseHelper): self.user_common_reward_id = user_common_reward_id[0] self._sz += user_common_reward_id[1] +class QuestScenePlayEndUnanalyzedLogRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unanalyzed_log_grade_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_data, new_off = decode_arr_cls(data, off, CommonRewardData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, grade: int, reward: Dict) -> "QuestScenePlayEndUnanalyzedLogRewardData": + ret = cls(b"\x00" * 8, 0) + ret.unanalyzed_log_grade_id = grade + ret.common_reward_data = [CommonRewardData.from_args(reward.get('CommonRewardType'), reward.get('CommonRewardId'), reward.get('CommonRewardNum'))] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unanalyzed_log_grade_id) \ + + encode_arr_cls(self.common_reward_data) + class PartyTeamData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - sz = 0 - user_party_team_id = decode_str(data, offset + sz) - self.user_party_team_id = user_party_team_id[0] - sz += user_party_team_id[1] + def __init__(self, data: bytes, offset: int): + off = offset + self.user_party_team_id, new_off = decode_str(data, off) + off += new_off - self.arrangement_num = decode_byte(data, offset + sz) - sz += BYTE_OFF + self.arrangement_num = decode_byte(data, off) + off += BYTE_OFF - user_hero_log_id = decode_str(data, offset + sz) - self.user_hero_log_id = user_hero_log_id[0] - sz += user_hero_log_id[1] + self.user_hero_log_id, new_off = decode_str(data, off) + off += new_off - main_weapon_user_equipment_id = decode_str(data, offset + sz) - self.main_weapon_user_equipment_id = main_weapon_user_equipment_id[0] - sz += main_weapon_user_equipment_id[1] + self.main_weapon_user_equipment_id, new_off = decode_str(data, off) + off += new_off - sub_equipment_user_equipment_id = decode_str(data, offset + sz) - self.sub_equipment_user_equipment_id = sub_equipment_user_equipment_id[0] - sz += sub_equipment_user_equipment_id[1] + self.sub_equipment_user_equipment_id, new_off = decode_str(data, off) + off += new_off - self.skill_slot1_skill_id = decode_int(data, offset + sz) - sz += INT_OFF + self.skill_slot1_skill_id = decode_int(data, off) + off += INT_OFF - self.skill_slot2_skill_id = decode_int(data, offset + sz) - sz += INT_OFF + self.skill_slot2_skill_id = decode_int(data, off) + off += INT_OFF - self.skill_slot3_skill_id = decode_int(data, offset + sz) - sz += INT_OFF + self.skill_slot3_skill_id = decode_int(data, off) + off += INT_OFF - self.skill_slot4_skill_id = decode_int(data, offset + sz) - sz += INT_OFF + self.skill_slot4_skill_id = decode_int(data, off) + off += INT_OFF - self.skill_slot5_skill_id = decode_int(data, offset + sz) - sz += INT_OFF + self.skill_slot5_skill_id = decode_int(data, off) + off += INT_OFF - self._sz = sz + self._sz = off - offset + + @classmethod + def from_args(cls, party_team_id: str, arr_num: int, data: Dict) -> "PartyTeamData": + ret = cls(b"\x00" * 37, 0) + ret.user_party_team_id = party_team_id + ret.arrangement_num = arr_num + ret.user_hero_log_id = data['id'] + ret.main_weapon_user_equipment_id = data['main_weapon'] + ret.sub_equipment_user_equipment_id = data['sub_equipment'] + ret.skill_slot1_skill_id = data['skill_slot1_skill_id'] + ret.skill_slot2_skill_id = data['skill_slot2_skill_id'] + ret.skill_slot3_skill_id = data['skill_slot3_skill_id'] + ret.skill_slot4_skill_id = data['skill_slot4_skill_id'] + ret.skill_slot5_skill_id = data['skill_slot5_skill_id'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_party_team_id) \ + + encode_byte(self.arrangement_num) \ + + encode_str(self.user_hero_log_id) \ + + encode_str(self.main_weapon_user_equipment_id) \ + + encode_str(self.sub_equipment_user_equipment_id) \ + + encode_int(self.skill_slot1_skill_id) \ + + encode_int(self.skill_slot2_skill_id) \ + + encode_int(self.skill_slot3_skill_id) \ + + encode_int(self.skill_slot4_skill_id) \ + + encode_int(self.skill_slot5_skill_id) + +class CommonRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.common_reward_type = decode_short(data, off) + off += SHORT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, reward_type: int, reward_id: int, reward_num: int) -> "CommonRewardData": + ret = cls(b"\x00" * 10, 0) + ret.common_reward_type = reward_type + ret.common_reward_id = reward_id + ret.common_reward_num = reward_num + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) + +class ReadProfileCardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.profile_card_code, new_off = decode_str(data, off) + off += new_off + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self.rank_num = decode_short(data, off) + off += SHORT_OFF + + self.setting_title_id = decode_int(data, off) + off += INT_OFF + + self.skill_id = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_hero_log_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_log_level = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_equipment_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_enhancement_value = decode_short(data, off) + off += SHORT_OFF + + self.main_weapon_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.main_weapon_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_equipment_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_enhancement_value = decode_short(data, off) + off += SHORT_OFF + + self.sub_equipment_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.sub_equipment_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.holographic_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, user_profile: Dict, hero_data: Dict) -> "ReadProfileCardData": + ret = cls(b"\x00" * 185, 0) + # TODO: real data + ret.profile_card_code = "" + ret.nick_name = "" + ret.rank_num = 0 + ret.setting_title_id = 0 + ret.skill_id = 0 + ret.hero_log_hero_log_id = 0 + ret.hero_log_log_level = 0 + ret.hero_log_awakening_stage = 0 + ret.hero_log_property1_property_id = 0 + ret.hero_log_property1_value1 = 0 + ret.hero_log_property1_value2 = 0 + ret.hero_log_property2_property_id = 0 + ret.hero_log_property2_value1 = 0 + ret.hero_log_property2_value2 = 0 + ret.hero_log_property3_property_id = 0 + ret.hero_log_property3_value1 = 0 + ret.hero_log_property3_value2 = 0 + ret.hero_log_property4_property_id = 0 + ret.hero_log_property4_value1 = 0 + ret.hero_log_property4_value2 = 0 + ret.main_weapon_equipment_id = 0 + ret.main_weapon_enhancement_value = 0 + ret.main_weapon_awakening_stage = 0 + ret.main_weapon_property1_property_id = 0 + ret.main_weapon_property1_value1 = 0 + ret.main_weapon_property1_value2 = 0 + ret.main_weapon_property2_property_id = 0 + ret.main_weapon_property2_value1 = 0 + ret.main_weapon_property2_value2 = 0 + ret.main_weapon_property3_property_id = 0 + ret.main_weapon_property3_value1 = 0 + ret.main_weapon_property3_value2 = 0 + ret.main_weapon_property4_property_id = 0 + ret.main_weapon_property4_value1 = 0 + ret.main_weapon_property4_value2 = 0 + ret.sub_equipment_equipment_id = 0 + ret.sub_equipment_enhancement_value = 0 + ret.sub_equipment_awakening_stage = 0 + ret.sub_equipment_property1_property_id = 0 + ret.sub_equipment_property1_value1 = 0 + ret.sub_equipment_property1_value2 = 0 + ret.sub_equipment_property2_property_id = 0 + ret.sub_equipment_property2_value1 = 0 + ret.sub_equipment_property2_value2 = 0 + ret.sub_equipment_property3_property_id = 0 + ret.sub_equipment_property3_value1 = 0 + ret.sub_equipment_property3_value2 = 0 + ret.sub_equipment_property4_property_id = 0 + ret.sub_equipment_property4_value1 = 0 + ret.sub_equipment_property4_value2 = 0 + ret.holographic_flag = 0 + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.profile_card_code) \ + + encode_str(self.nick_name) \ + + encode_short(self.rank_num) \ + + encode_int(self.setting_title_id) \ + + encode_short(self.skill_id) \ + + encode_int(self.hero_log_hero_log_id) \ + + encode_short(self.hero_log_log_level) \ + + encode_short(self.hero_log_awakening_stage) \ + + encode_int(self.hero_log_property1_property_id) \ + + encode_int(self.hero_log_property1_value1) \ + + encode_int(self.hero_log_property1_value2) \ + + encode_int(self.hero_log_property2_property_id) \ + + encode_int(self.hero_log_property2_value1) \ + + encode_int(self.hero_log_property2_value2) \ + + encode_int(self.hero_log_property3_property_id) \ + + encode_int(self.hero_log_property3_value1) \ + + encode_int(self.hero_log_property3_value2) \ + + encode_int(self.hero_log_property4_property_id) \ + + encode_int(self.hero_log_property4_value1) \ + + encode_int(self.hero_log_property4_value2) \ + + encode_int(self.main_weapon_equipment_id) \ + + encode_short(self.main_weapon_enhancement_value) \ + + encode_short(self.main_weapon_awakening_stage) \ + + encode_int(self.main_weapon_property1_property_id) \ + + encode_int(self.main_weapon_property1_value1) \ + + encode_int(self.main_weapon_property1_value2) \ + + encode_int(self.main_weapon_property2_property_id) \ + + encode_int(self.main_weapon_property2_value1) \ + + encode_int(self.main_weapon_property2_value2) \ + + encode_int(self.main_weapon_property3_property_id) \ + + encode_int(self.main_weapon_property3_value1) \ + + encode_int(self.main_weapon_property3_value2) \ + + encode_int(self.main_weapon_property4_property_id) \ + + encode_int(self.main_weapon_property4_value1) \ + + encode_int(self.main_weapon_property4_value2) \ + + encode_int(self.sub_equipment_equipment_id) \ + + encode_short(self.sub_equipment_enhancement_value) \ + + encode_short(self.sub_equipment_awakening_stage) \ + + encode_int(self.sub_equipment_property1_property_id) \ + + encode_int(self.sub_equipment_property1_value1) \ + + encode_int(self.sub_equipment_property1_value2) \ + + encode_int(self.sub_equipment_property2_property_id) \ + + encode_int(self.sub_equipment_property2_value1) \ + + encode_int(self.sub_equipment_property2_value2) \ + + encode_int(self.sub_equipment_property3_property_id) \ + + encode_int(self.sub_equipment_property3_value1) \ + + encode_int(self.sub_equipment_property3_value2) \ + + encode_int(self.sub_equipment_property4_property_id) \ + + encode_int(self.sub_equipment_property4_value1) \ + + encode_int(self.sub_equipment_property4_value2) \ + + encode_byte(self.holographic_flag) + +class QuestSceneBestScoreUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.clear_time = decode_int(data, off) + off += INT_OFF + + self.combo_num = decode_int(data, off) + off += INT_OFF + + self.total_damage, new_off = decode_str(data, off) + off += new_off + + self.concurrent_destroying_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSceneBestScoreUserData": + ret = cls(b"\x00" * 14, 0) + ret.clear_time = data['clear_time'] + ret.combo_num = data['combo_num'] + ret.total_damage = data['total_damage'] + ret.concurrent_destroying_num = data['concurrent_destroying_num'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.clear_time) \ + + encode_int(self.combo_num) \ + + encode_str(self.total_damage) \ + + encode_short(self.concurrent_destroying_num) + +class QuestSceneExBonusUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_bonus_table_id = decode_int(data, off) + off += INT_OFF + + self.achievement_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, table_id: int = 0, ach_flag: bool = False) -> "QuestSceneExBonusUserData": + ret = cls(b"\x00" * 5, 0) + ret.ex_bonus_table_id = table_id + ret.achievement_flag = ach_flag + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_bonus_table_id) \ + + encode_byte(self.achievement_flag) + +class QuestSceneUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_type = decode_byte(data, off) + off += BYTE_OFF + + self.quest_scene_id = decode_short(data, off) + off += SHORT_OFF + + self.clear_flag = decode_byte(data, off) + off += BYTE_OFF + + self.quest_scene_best_score_user_data: List[QuestSceneBestScoreUserData] = [] + self.quest_scene_best_score_user_data, new_off = decode_arr_cls(data, off, QuestSceneBestScoreUserData) + off += new_off + + self.quest_scene_ex_bonus_user_data_list: List[QuestSceneExBonusUserData] = [] + self.quest_scene_ex_bonus_user_data_list, new_off = decode_arr_cls(data, off, QuestSceneExBonusUserData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSceneUserData": + ret = cls(b"\x00" * 12, 0) + ret.quest_type = data['quest_type'] + ret.quest_scene_id = data['quest_scene_id'] + ret.clear_flag = data['quest_clear_flag'] + ret.quest_scene_best_score_user_data = [QuestSceneBestScoreUserData.from_args(data)] + ret.quest_scene_ex_bonus_user_data_list = [] # TODO + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.quest_type) \ + + encode_short(self.quest_scene_id) \ + + encode_byte(self.clear_flag) \ + + encode_arr_cls(self.quest_scene_best_score_user_data) \ + + encode_arr_cls(self.quest_scene_ex_bonus_user_data_list) + +class QuestScenePlayStartAppearancePlayerTraceData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_quest_scene_player_trace_id, new_off = decode_str(data, off) + off += new_off + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, sesh_id:int, nickname: str) -> "QuestScenePlayStartAppearancePlayerTraceData": + ret = cls(b"\x00" * 8, 0) + ret.user_quest_scene_player_trace_id = str(sesh_id) + ret.nick_name = nickname + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_quest_scene_player_trace_id) \ + + encode_str(self.nick_name) + +class QuestScenePlayStartResponseData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.appearance_player_trace_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayStartAppearancePlayerTraceData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, sesh_id: int, nickname: str) -> "QuestScenePlayStartResponseData": + ret = cls(b"\x00" * 99, 0) + ret.appearance_player_trace_data_list = [QuestScenePlayStartAppearancePlayerTraceData.from_args(sesh_id, nickname)] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_arr_cls(self.appearance_player_trace_data_list) + +class QuestScenePlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_party_id, new_off = decode_str(data, off) + off += new_off + + self.appoint_leader_resource_card_code, new_off = decode_str(data, off) + off += new_off + + self.use_profile_card_code, new_off = decode_str(data, off) + off += new_off + + self.quest_drop_boost_apply_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayStartRequestData": + ret = cls(b"\x00" * 13, 0) + ret.user_party_id = data['UserPartyId'] + ret.appoint_leader_resource_card_code = data['AppointLeaderResourceCardCode'] + ret.use_profile_card_code = data['UseProfileCardCode'] + ret.quest_drop_boost_apply_flag = data['QuestDropBoostApplyFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_party_id) \ + + encode_str(self.appoint_leader_resource_card_code) \ + + encode_str(self.use_profile_card_code) \ + + encode_byte(self.quest_drop_boost_apply_flag) + +class QuestSceneMultiPlayStartEntryUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.host_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSceneMultiPlayStartEntryUserData": + ret = cls(b"\x00" * 9, 0) + ret.store_id = data['StoreId'] + ret.user_id = data['UserId'] + ret.host_flag = data['HostFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.store_id) \ + + encode_str(self.user_id) \ + + encode_byte(self.host_flag) + +class QuestSceneMultiPlayStartRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.room_id, new_off = decode_str(data, off) + off += new_off + + self.matching_mode = decode_byte(data, off) + off += BYTE_OFF + + self.entry_user_data_list: List[QuestSceneMultiPlayStartEntryUserData] = [] + self.entry_user_data_list, new_off = decode_arr_cls(data, off, QuestSceneMultiPlayStartEntryUserData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSceneMultiPlayStartRequestData": + ret = cls(b"\x00" * 9, 0) + ret.room_id = data['RoomId'] + ret.matching_mode = data['MatchingMode'] + ret.entry_user_data_list = [] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.room_id) \ + + encode_byte(self.matching_mode) \ + + encode_arr_cls(self.entry_user_data_list) + +class QuestSceneMultiPlayStartResponseData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.dummy_1 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_2 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_3 = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls) -> "QuestSceneMultiPlayStartResponseData": + ret = cls(b"\x00" * 3, 0) + ret.dummy_1 = 0 + ret.dummy_2 = 0 + ret.dummy_3 = 0 + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.dummy_1) \ + + encode_byte(self.dummy_2) \ + + encode_byte(self.dummy_3) + +class QuestSceneMultiPlayEndResponseData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.dummy_1 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_2 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_3 = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls) -> "QuestSceneMultiPlayEndResponseData": + ret = cls(b"\x00" * 3, 0) + ret.dummy_1 = 0 + ret.dummy_2 = 0 + ret.dummy_3 = 0 + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.dummy_1) \ + + encode_byte(self.dummy_2) \ + + encode_byte(self.dummy_3) + +class QuestScenePlayEndBaseGetData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.get_hero_log_exp = decode_int(data, off) + off += INT_OFF + + self.get_col = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndBaseGetData": + ret = cls(b"\x00" * 99, 0) + ret.get_hero_log_exp = data['GetHeroLogExp'] + ret.get_col = data['GetCol'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.get_hero_log_exp) \ + + encode_int(self.get_col) + +class QuestScenePlayEndDiscoveryEnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self.destroy_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndDiscoveryEnemyData": + ret = cls(b"\x00" * 99, 0) + ret.enemy_kind_id = data['EnemyKindId'] + ret.destroy_num = data['DestroyNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.enemy_kind_id) \ + + encode_short(self.destroy_num) + +class QuestScenePlayEndGetPlayerTraceData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_quest_scene_player_trace_id, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndGetPlayerTraceData": + ret = cls(b"\x00" * 99, 0) + ret.user_quest_scene_player_trace_id = data['UserQuestScenePlayerTraceId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_quest_scene_player_trace_id) + +class QuestScenePlayEndGetRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_rare_drop_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndGetRareDropData": + ret = cls(b"\x00" * 99, 0) + ret.quest_rare_drop_id = data['QuestRareDropId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_rare_drop_id) + +class QuestScenePlayEndGetSpecialRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_special_rare_drop_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndGetSpecialRareDropData": + ret = cls(b"\x00" * 99, 0) + ret.quest_special_rare_drop_id = data['QuestSpecialRareDropId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_special_rare_drop_id) + +class QuestScenePlayEndGetUnanalyzedLogTmpRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unanalyzed_log_grade_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndGetUnanalyzedLogTmpRewardData": + ret = cls(b"\x00" * 99, 0) + ret.unanalyzed_log_grade_id = data['UnanalyzedLogGradeId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unanalyzed_log_grade_id) + +class QuestScenePlayEndScoreData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.clear_time = decode_int(data, off) + off += INT_OFF + + self.combo_num = decode_int(data, off) + off += INT_OFF + + self.total_damage, new_off = decode_str(data, off) + off += new_off + + self.concurrent_destroying_num = decode_short(data, off) + off += SHORT_OFF + + self.reaching_skill_level = decode_short(data, off) + off += SHORT_OFF + + self.ko_chara_num = decode_byte(data, off) + off += BYTE_OFF + + self.acceleration_invocation_num = decode_short(data, off) + off += SHORT_OFF + + self.boss_destroying_num = decode_short(data, off) + off += SHORT_OFF + + self.synchro_skill_used_flag = decode_byte(data, off) + off += BYTE_OFF + + self.used_friend_skill_id = decode_int(data, off) + off += INT_OFF + + self.friend_skill_used_flag = decode_byte(data, off) + off += BYTE_OFF + + self.continue_cnt = decode_short(data, off) + off += SHORT_OFF + + self.total_loss_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndScoreData": + ret = cls(b"\x00" * 99, 0) + ret.clear_time = data['ClearTime'] + ret.combo_num = data['ComboNum'] + ret.total_damage = data['TotalDamage'] + ret.concurrent_destroying_num = data['ConcurrentDestroyingNum'] + ret.reaching_skill_level = data['ReachingSkillLevel'] + ret.ko_chara_num = data['KoCharaNum'] + ret.acceleration_invocation_num = data['AccelerationInvocationNum'] + ret.boss_destroying_num = data['BossDestroyingNum'] + ret.synchro_skill_used_flag = data['SynchroSkillUsedFlag'] + ret.used_friend_skill_id = data['UsedFriendSkillId'] + ret.friend_skill_used_flag = data['FriendSkillUsedFlag'] + ret.continue_cnt = data['ContinueCnt'] + ret.total_loss_num = data['TotalLossNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.clear_time) \ + + encode_int(self.combo_num) \ + + encode_str(self.total_damage) \ + + encode_short(self.concurrent_destroying_num) \ + + encode_short(self.reaching_skill_level) \ + + encode_byte(self.ko_chara_num) \ + + encode_short(self.acceleration_invocation_num) \ + + encode_short(self.boss_destroying_num) \ + + encode_byte(self.synchro_skill_used_flag) \ + + encode_int(self.used_friend_skill_id) \ + + encode_byte(self.friend_skill_used_flag) \ + + encode_short(self.continue_cnt) \ + + encode_short(self.total_loss_num) + +class QuestScenePlayEndMissionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.mission_id = decode_int(data, off) + off += INT_OFF + + self.clear_flag = decode_byte(data, off) + off += BYTE_OFF + + self.mission_difficulty_id = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndMissionData": + ret = cls(b"\x00" * 99, 0) + ret.mission_id = data['MissionId'] + ret.clear_flag = data['ClearFlag'] + ret.mission_difficulty_id = data['MissionDifficultyId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.mission_id) \ + + encode_byte(self.clear_flag) \ + + encode_short(self.mission_difficulty_id) + +class QuestScenePlayEndDestroyBossData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.boss_type = decode_byte(data, off) + off += BYTE_OFF + + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self.destroy_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndDestroyBossData": + ret = cls(b"\x00" * 99, 0) + ret.boss_type = data['BossType'] + ret.enemy_kind_id = data['EnemyKindId'] + ret.destroy_num = data['DestroyNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.boss_type) \ + + encode_int(self.enemy_kind_id) \ + + encode_short(self.destroy_num) + +class QuestScenePlayEndGetEventItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.event_item_id = decode_int(data, off) + off += INT_OFF + + self.get_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndGetEventItemData": + ret = cls(b"\x00" * 99, 0) + ret.event_item_id = data['EventItemId'] + ret.get_num = data['GetNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.event_item_id) \ + + encode_short(self.get_num) + +class QuestTreasureHuntPlayEndResponseData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.get_event_point = decode_int(data, off) + off += INT_OFF + + self.total_event_point = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, get_pt: int = 0, total_pt: int = 0) -> "QuestTreasureHuntPlayEndResponseData": + ret = cls(b"\x00" * 8, 0) + ret.get_event_point = get_pt + ret.total_event_point = total_pt + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.get_event_point) \ + + encode_int(self.total_event_point) + +class QuestTrialTowerPlayEndUpdatedNotificationData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.store_best_score_clear_time_flag = decode_byte(data, off) + off += BYTE_OFF + + self.store_best_score_combo_num_flag = decode_byte(data, off) + off += BYTE_OFF + + self.store_best_score_total_damage_flag = decode_byte(data, off) + off += BYTE_OFF + + self.store_best_score_concurrent_destroying_num_flag = decode_byte(data, off) + off += BYTE_OFF + + self.store_reaching_trial_tower_rank = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, shop_best_time: bool = False, shop_best_combo: bool = False, shop_best_damage: bool = False, shop_best_concurrent: bool = False, shop_tower_rank: int = 0) -> "QuestTrialTowerPlayEndUpdatedNotificationData": + ret = cls(b"\x00" * 8, 0) + ret.store_best_score_clear_time_flag = shop_best_time + ret.store_best_score_combo_num_flag = shop_best_combo + ret.store_best_score_total_damage_flag = shop_best_damage + ret.store_best_score_concurrent_destroying_num_flag = shop_best_concurrent + ret.store_reaching_trial_tower_rank = shop_tower_rank + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.store_best_score_clear_time_flag) \ + + encode_byte(self.store_best_score_combo_num_flag) \ + + encode_byte(self.store_best_score_total_damage_flag) \ + + encode_byte(self.store_best_score_concurrent_destroying_num_flag) \ + + encode_int(self.store_reaching_trial_tower_rank) + +class CommonRewardUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.common_reward_type = decode_short(data, off) + off += SHORT_OFF + + self.user_common_reward_id, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CommonRewardUserData": + ret = cls(b"\x00" * 99, 0) + ret.common_reward_type = data['CommonRewardType'] + ret.user_common_reward_id = data['UserCommonRewardId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.common_reward_type) \ + + encode_str(self.user_common_reward_id) + +class QuestScenePlayEndResponseData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.rarity_up_occurrence_flag = decode_byte(data, off) + off += BYTE_OFF + + self.adventure_ex_area_occurrences_flag = decode_byte(data, off) + off += BYTE_OFF + + self.ex_bonus_data_list: List[QuestScenePlayEndExBonusData] = [] + self.ex_bonus_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndExBonusData) + off += new_off + + self.play_end_player_trace_reward_data_list: List[QuestScenePlayEndPlayerTraceRewardData] = [] + self.play_end_player_trace_reward_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndPlayerTraceRewardData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, rarity_up_flag: bool = False, adventure_ex_area_flg: bool = False, ex_tables: List = []) -> "QuestScenePlayEndResponseData": + ret = cls(b"\x00" * 99, 0) + ret.rarity_up_occurrence_flag = rarity_up_flag + ret.adventure_ex_area_occurrences_flag = adventure_ex_area_flg + for x in ex_tables: + ret.ex_bonus_data_list.append(QuestScenePlayEndExBonusData.from_args(x['table'], x['ach_status'])) + ret.play_end_player_trace_reward_data_list = [] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.rarity_up_occurrence_flag) \ + + encode_byte(self.adventure_ex_area_occurrences_flag) \ + + encode_arr_cls(self.ex_bonus_data_list) \ + + encode_arr_cls(self.play_end_player_trace_reward_data_list) + +class QuestScenePlayEndExBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_bonus_table_id = decode_int(data, off) + off += INT_OFF + + self.achievement_status = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, table_id: int, ach_status: int) -> "QuestScenePlayEndExBonusData": + ret = cls(b"\x00" * 5, 0) + ret.ex_bonus_table_id = table_id + ret.achievement_status = ach_status + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_bonus_table_id) \ + + encode_byte(self.achievement_status) + +class QuestScenePlayEndPlayerTraceRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.common_reward_data: List[CommonRewardData] + self.common_reward_data, new_off = decode_arr_cls(data, off, CommonRewardData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, reward_data: Dict) -> "QuestScenePlayEndPlayerTraceRewardData": + ret = cls(b"\x00" * 4, 0) + ret.common_reward_data.append(CommonRewardData.from_args(reward_data['CommonRewardType'], reward_data['CommonRewardId'], reward_data['CommonRewardNum'])) + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_arr_cls(self.common_reward_data) + +class QuestScenePlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.play_result_flag = decode_byte(data, off) + off += BYTE_OFF + + self.base_get_data: List[QuestScenePlayEndBaseGetData] = [] + self.base_get_data, new_off = decode_arr_cls(data, off, QuestScenePlayEndBaseGetData) + off += new_off + + self.get_player_trace_data_list: List[QuestScenePlayEndGetPlayerTraceData] = [] + self.get_player_trace_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndGetPlayerTraceData) + off += new_off + + self.get_rare_drop_data_list: List[QuestScenePlayEndGetRareDropData] = [] + self.get_rare_drop_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndGetRareDropData) + off += new_off + + self.get_special_rare_drop_data_list: List[QuestScenePlayEndGetSpecialRareDropData] = [] + self.get_special_rare_drop_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndGetSpecialRareDropData) + off += new_off + + self.get_unanalyzed_log_tmp_reward_data_list: List[QuestScenePlayEndGetUnanalyzedLogTmpRewardData] = [] + self.get_unanalyzed_log_tmp_reward_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndGetUnanalyzedLogTmpRewardData) + off += new_off + + self.get_event_item_data_list: List[QuestScenePlayEndGetEventItemData] = [] + self.get_event_item_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndGetEventItemData) + off += new_off + + self.discovery_enemy_data_list: List[QuestScenePlayEndDiscoveryEnemyData] = [] + self.discovery_enemy_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndDiscoveryEnemyData) + off += new_off + + self.destroy_boss_data_list: List[QuestScenePlayEndDestroyBossData] = [] + self.destroy_boss_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndDestroyBossData) + off += new_off + + self.mission_data_list: List[QuestScenePlayEndMissionData] = [] + self.mission_data_list, new_off = decode_arr_cls(data, off, QuestScenePlayEndMissionData) + off += new_off + + self.score_data: List[QuestScenePlayEndScoreData] = [] + self.score_data, new_off = decode_arr_cls(data, off, QuestScenePlayEndScoreData) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestScenePlayEndRequestData": + ret = cls(b"\x00" * 99, 0) + ret.play_result_flag = data['PlayResultFlag'] + ret.base_get_data = [] + ret.get_player_trace_data_list = [] + ret.get_rare_drop_data_list = [] + ret.get_special_rare_drop_data_list = [] + ret.get_unanalyzed_log_tmp_reward_data_list = [] + ret.get_event_item_data_list = [] + ret.discovery_enemy_data_list = [] + ret.destroy_boss_data_list = [] + ret.mission_data_list = [] + ret.score_data = [] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.play_result_flag) \ + + encode_arr_cls(self.base_get_data) \ + + encode_arr_cls(self.get_player_trace_data_list) \ + + encode_arr_cls(self.get_rare_drop_data_list) \ + + encode_arr_cls(self.get_special_rare_drop_data_list) \ + + encode_arr_cls(self.get_unanalyzed_log_tmp_reward_data_list) \ + + encode_arr_cls(self.get_event_item_data_list) \ + + encode_arr_cls(self.discovery_enemy_data_list) \ + + encode_arr_cls(self.destroy_boss_data_list) \ + + encode_arr_cls(self.mission_data_list) \ + + encode_arr_cls(self.score_data) + +class QuestSceneMultiPlayEndRequestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.dummy_1 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_2 = decode_byte(data, off) + off += BYTE_OFF + + self.dummy_3 = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls) -> "QuestSceneMultiPlayEndRequestData": + ret = cls(b"\x00" * 3, 0) + ret.dummy_1 = 0 + ret.dummy_2 = 0 + ret.dummy_3 = 0 + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.dummy_1) \ + + encode_byte(self.dummy_2) \ + + encode_byte(self.dummy_3) + +class ChatSideStoryUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chat_side_story_id = decode_int(data, off) + off += INT_OFF + + self.played_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ChatSideStoryUserData": + ret = cls(b"\x00" * 99, 0) + ret.chat_side_story_id = data['ChatSideStoryId'] + ret.played_flag = data['PlayedFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.chat_side_story_id) \ + + encode_byte(self.played_flag) class PartyData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - sz = 0 - user_party_id = decode_str(data, offset + sz) - self.user_party_id = user_party_id[0] - sz += user_party_id[1] + def __init__(self, data: bytes, offset: int): + off = offset + self.user_party_id, new_off = decode_str(data, off) + off += new_off - self.team_no = decode_byte(data, offset + sz) - sz += BYTE_OFF - - self.party_team_data_count = decode_int(data, offset + sz) - sz += INT_OFF + self.team_no = decode_byte(data, off) + off += BYTE_OFF self.party_team_data_list: List[PartyTeamData] = [] - for _ in range(self.party_team_data_count): - tmp = PartyTeamData(data, offset + sz) - self.party_team_data_list.append(tmp) - sz += tmp.get_size() + self.party_team_data_list, new_off = decode_arr_cls(data, off, PartyTeamData) + off += new_off - self._sz = sz + self._sz = off - offset + + @classmethod + def from_args(cls, user_party_id: int, team_num: int, member1: Dict, member2: Dict, member3: Dict) -> "PartyData": + ret = cls(b"\x00" * 9, 0) + ret.user_party_id = user_party_id + ret.team_no = team_num + ret.party_team_data_list = [ + PartyTeamData.from_args(f"{user_party_id}-{team_num}-1", 1, member1), + PartyTeamData.from_args(f"{user_party_id}-{team_num}-2", 2, member2), + PartyTeamData.from_args(f"{user_party_id}-{team_num}-3", 3, member3), + ] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_party_id) \ + + encode_byte(self.team_no) \ + + encode_arr_cls(self.party_team_data_list) + +class SupportLogPartyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.party_no = decode_byte(data, off) + off += BYTE_OFF + + self.arrangement_num_1_user_support_log_id, new_off = decode_str(data, off) + off += new_off + + self.arrangement_num_2_user_support_log_id, new_off = decode_str(data, off) + off += new_off + + self.arrangement_num_3_user_support_log_id, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SupportLogPartyData": + ret = cls(b"\x00" * 99, 0) + ret.party_no = data['PartyNo'] + ret.arrangement_num_1_user_support_log_id = data['ArrangementNum1UserSupportLogId'] + ret.arrangement_num_2_user_support_log_id = data['ArrangementNum2UserSupportLogId'] + ret.arrangement_num_3_user_support_log_id = data['ArrangementNum3UserSupportLogId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.party_no) \ + + encode_str(self.arrangement_num_1_user_support_log_id) \ + + encode_str(self.arrangement_num_2_user_support_log_id) \ + + encode_str(self.arrangement_num_3_user_support_log_id) class PlayStartRequestData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: @@ -607,7 +1994,7 @@ class ShopResourceSalesData(BaseHelper): ret += encode_str(self.discharge_user_id) ret += encode_short(self.remaining_num) ret += encode_short(self.purchase_num) - ret += encode_str(fmt_dt(self.sales_start_date)) + ret += encode_date_str(self.sales_start_date) ret += encode_arr_cls(self.sales_resource_data_list) return ret @@ -635,7 +2022,7 @@ class YuiMedalShopUserData(BaseHelper): def make(self) -> bytes: ret = encode_int(self.yui_medal_shop_id) ret += encode_int(self.purchase_num) - ret += encode_str(fmt_dt(self.last_purchase_date)) + ret += encode_date_str(self.last_purchase_date) return ret class GashaMedalShopUserData(BaseHelper): @@ -659,263 +2046,6 @@ class GashaMedalShopUserData(BaseHelper): ret += encode_int(self.purchase_num) return ret -class YuiMedalShopData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.yui_medal_shop_id = decode_int(data, offset + self._sz) - - name = decode_str(data, offset + self._sz) - self.name = name[0] - self._sz += name[1] - - description = decode_str(data, offset + self._sz) - self.description = description[0] - self._sz += description[1] - - self.selling_yui_medal = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.selling_col = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.selling_event_item_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.selling_event_item_num = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.selling_ticket_num = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.purchase_limit = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.pick_up_flag = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.product_category = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.sales_type = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.target_days = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.target_hour = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.interval_hour = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - - sales_start_date = decode_str(data, offset + self._sz) - self.sales_start_date = prs_dt(sales_start_date[0]) - self._sz += sales_start_date[1] - - sales_end_date = decode_str(data, offset + self._sz) - self.sales_end_date = prs_dt(sales_end_date[0]) - self._sz += sales_end_date[1] - - self.sort = decode_byte(data, offset + self._sz) - - @classmethod - def from_args(cls, shop_id: int = 0, name: str = "", desc: str = "") -> "YuiMedalShopData": - ret = cls(b"\x00" * 43, 0) - ret.yui_medal_shop_id = shop_id - ret.name = name - ret.description = desc - return ret - - def make(self) -> bytes: - ret = encode_int(self.yui_medal_shop_id) - ret += encode_str(self.name) - ret += encode_str(self.description) - ret += encode_short(self.selling_yui_medal) - ret += encode_int(self.selling_col) - ret += encode_int(self.selling_event_item_id) - ret += encode_int(self.selling_event_item_num) - ret += encode_int(self.selling_ticket_num) - ret += encode_short(self.purchase_limit) - ret += encode_byte(self.pick_up_flag) - ret += encode_byte(self.product_category) - ret += encode_byte(self.sales_type) - ret += encode_byte(self.target_days) - ret += encode_byte(self.target_hour) - ret += encode_byte(self.interval_hour) - ret += encode_str(fmt_dt(self.sales_start_date)) - ret += encode_str(fmt_dt(self.sales_end_date)) - ret += encode_byte(self.sort) - return ret - -class YuiMedalShopItemData(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.yui_medal_shop_item_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.yui_medal_shop_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.common_reward_type = decode_byte(data, offset + self._sz) - self._sz += BYTE_OFF - self.common_reward_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.common_reward_num = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.strength = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - self.property1_property_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property1_value1 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property1_value2 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - self.property2_property_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property2_value1 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property2_value2 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - self.property3_property_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property3_value1 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property3_value2 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - self.property4_property_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property4_value1 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.property4_value2 = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - @classmethod - def from_args(cls, item_id: int = 0, shop_id: int = 0, reward_type: int = 0, reward_id: int = 0, reward_num: int = 0, strength: int = 0) -> "YuiMedalShopItemData": - ret = cls(b"\x00" * 67, 0) - ret.yui_medal_shop_item_id = item_id - ret.yui_medal_shop_id = shop_id - ret.common_reward_type = reward_type - ret.common_reward_id = reward_id - ret.common_reward_num = reward_num - ret.strength = strength - return ret - - def make(self) -> bytes: - ret = encode_int(self.yui_medal_shop_item_id) - ret += encode_int(self.yui_medal_shop_id) - ret += encode_byte(self.common_reward_type) - ret += encode_int(self.common_reward_id) - ret += encode_short(self.common_reward_num) - ret += encode_int(self.strength) - - ret += encode_int(self.property1_property_id) - ret += encode_int(self.property1_value1) - ret += encode_int(self.property1_value2) - - ret += encode_int(self.property2_property_id) - ret += encode_int(self.property2_value1) - ret += encode_int(self.property2_value2) - - ret += encode_int(self.property3_property_id) - ret += encode_int(self.property3_value1) - ret += encode_int(self.property3_value2) - - ret += encode_int(self.property4_property_id) - ret += encode_int(self.property4_value1) - ret += encode_int(self.property4_value2) - return ret - -class ResEarnCampaignShop(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.res_earn_campaign_shop_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.res_earn_campaign_application_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - name = decode_str(data, offset + self._sz) - self.name = name[0] - self._sz += name[1] - - self.selling_yui_medal = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.selling_col = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.selling_event_item_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.selling_event_item_num = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.purchase_limit = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - self.get_application_point = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - - sales_start_date = decode_str(data, offset + self._sz) - self.sales_start_date = prs_dt(sales_start_date[0]) - self._sz += sales_start_date[1] - - sales_end_date = decode_str(data, offset + self._sz) - self.sales_end_date = prs_dt(sales_end_date[0]) - self._sz += sales_end_date[1] - - @classmethod - def from_args(cls, shop_id: int = 0, app_id: int = 0, name: str = "") -> "ResEarnCampaignShop": - ret = cls(b"\x00" * 26, 0) - ret.res_earn_campaign_shop_id = shop_id - ret.res_earn_campaign_application_id = app_id - ret.name = name - return ret - - def make(self) -> bytes: - ret = encode_int(self.res_earn_campaign_shop_id) - ret = encode_int(self.res_earn_campaign_application_id) - ret += encode_str(self.name) - ret += encode_short(self.selling_yui_medal) - ret += encode_int(self.selling_col) - ret += encode_int(self.selling_event_item_id) - ret += encode_int(self.selling_event_item_num) - ret += encode_short(self.purchase_limit) - ret += encode_short(self.get_application_point) - ret += encode_str(fmt_dt(self.sales_start_date)) - ret += encode_str(fmt_dt(self.sales_end_date)) - return ret - -class GashaMedalShop(BaseHelper): - def __init__(self, data: bytes, offset: int) -> None: - super().__init__(data, offset) - self.gasha_medal_shop_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - - name = decode_str(data, offset + self._sz) - self.name = name[0] - self._sz += name[1] - - self.gasha_medal_id = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.use_gasha_medal_num = decode_int(data, offset + self._sz) - self._sz += INT_OFF - self.purchase_limit = decode_short(data, offset + self._sz) - self._sz += SHORT_OFF - - sales_start_date = decode_str(data, offset + self._sz) - self.sales_start_date = prs_dt(sales_start_date[0]) - self._sz += sales_start_date[1] - - sales_end_date = decode_str(data, offset + self._sz) - self.sales_end_date = prs_dt(sales_end_date[0]) - self._sz += sales_end_date[1] - - @classmethod - def from_args(cls, shop_id: int = 0, name: str = "", medal_id: int = 0, medal_num: int = 0, purchase_limit: int = 0) -> "GashaMedalShop": - ret = cls(b"\x00" * 26, 0) - ret.gasha_medal_shop_id = shop_id - ret.name = name - ret.gasha_medal_id = medal_id - ret.use_gasha_medal_num = medal_num - ret.purchase_limit = purchase_limit - return ret - - def make(self) -> bytes: - ret = encode_int(self.gasha_medal_shop_id) - ret += encode_str(self.name) - ret += encode_int(self.gasha_medal_id) - ret += encode_int(self.use_gasha_medal_num) - ret += encode_short(self.purchase_limit) - ret += encode_str(fmt_dt(self.sales_start_date)) - ret += encode_str(fmt_dt(self.sales_end_date)) - return ret - class QuestHierarchyProgressDegreesRankingData(BaseHelper): def __init__(self, data: bytes, offset: int) -> None: super().__init__(data, offset) @@ -977,10 +2107,10 @@ class PopularHeroLogRankingData(BaseHelper): @classmethod def from_args(cls, ranking: int, hero_id: int, used_num: int) -> "PopularHeroLogRankingData": - ret = cls(b"\x00" * 12, 0) - cls.ranking = ranking - cls.hero_log_id = hero_id - cls.used_num = used_num + ret = cls(b"\x00" * 992, 0) + ret.rank = ranking + ret.hero_log_id = hero_id + ret.used_num = used_num return ret def make(self) -> bytes: @@ -988,3 +2118,10564 @@ class PopularHeroLogRankingData(BaseHelper): ret += encode_int(self.hero_log_id) ret += encode_int(self.used_num) return ret + +class DefragMatchBasicUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.seed_flag = decode_short(data, off) + off += SHORT_OFF + + self.ad_confirm_flag = decode_byte(data, off) + off += BYTE_OFF + + self.total_league_point = decode_int(data, off) + off += INT_OFF + + self.have_league_score = decode_short(data, off) + off += SHORT_OFF + + self.class_num = decode_short(data, off) + off += SHORT_OFF + + self.hall_of_fame_confirm_flag = decode_byte(data, off) + off += BYTE_OFF + self._sz = off + + @classmethod + def from_args(cls, seed_flag: int = 0, ad_confirm_flag: int = 0, total_league_point: int = 0, league_score: int = 0, class_num: int = 1, hof_flag: int = 0) -> "DefragMatchBasicUserData": + ret = cls(b"\x00" * 12, 0) + ret.seed_flag = seed_flag + ret.ad_confirm_flag = ad_confirm_flag + ret.total_league_point = total_league_point + ret.have_league_score = league_score + ret.class_num = class_num + ret.hall_of_fame_confirm_flag = hof_flag + + def make(self) -> bytes: + ret = encode_short(self.seed_flag) + ret += encode_byte(self.ad_confirm_flag) + ret += encode_int(self.total_league_point) + ret += encode_short(self.have_league_score) + ret += encode_short(self.class_num) + ret += encode_byte(self.hall_of_fame_confirm_flag) + return ret + +class DefragMatchRankingUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.league_point_rank = decode_int(data, off) + off += INT_OFF + + self.league_score_rank = decode_int(data, off) + off += INT_OFF + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self.setting_title_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.favorite_support_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_support_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.total_league_point = decode_int(data, off) + off += INT_OFF + + self.have_league_score = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, profile: Dict) -> "DefragMatchRankingUserData": + ret = cls(b"\x00" * 34, 0) + ret.league_point_rank = 0 + ret.league_score_rank = 0 + ret.nick_name = profile['nick_name'] + ret.setting_title_id = profile['setting_title_id'] + ret.favorite_hero_log_id = profile['fav_hero'] + ret.favorite_hero_log_awakening_stage = 0 + ret.favorite_support_log_id = 0 + ret.favorite_support_log_awakening_stage = 0 + ret.total_league_point = 0 + ret.have_league_score = 0 + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.league_point_rank) \ + + encode_int(self.league_score_rank) \ + + encode_str(self.nick_name) \ + + encode_int(self.setting_title_id) \ + + encode_int(self.favorite_hero_log_id) \ + + encode_short(self.favorite_hero_log_awakening_stage) \ + + encode_int(self.favorite_support_log_id) \ + + encode_short(self.favorite_support_log_awakening_stage) \ + + encode_int(self.total_league_point) \ + + encode_short(self.have_league_score) + +class DefragMatchLeaguePointRankingData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.rank = decode_int(data, off) + off += INT_OFF + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self.setting_title_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.favorite_support_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_support_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.class_num = decode_short(data, off) + off += SHORT_OFF + + self.total_league_point = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchLeaguePointRankingData": + ret = cls(b"\x00" * 42, 0) + ret.rank = data['Rank'] + ret.user_id = data['UserId'] + ret.store_id = data['StoreId'] + ret.store_name = data['StoreName'] + ret.nick_name = data['NickName'] + ret.setting_title_id = data['SettingTitleId'] + ret.favorite_hero_log_id = data['FavoriteHeroLogId'] + ret.favorite_hero_log_awakening_stage = data['FavoriteHeroLogAwakeningStage'] + ret.favorite_support_log_id = data['FavoriteSupportLogId'] + ret.favorite_support_log_awakening_stage = data['FavoriteSupportLogAwakeningStage'] + ret.class_num = data['ClassNum'] + ret.total_league_point = data['TotalLeaguePoint'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.rank) \ + + encode_str(self.user_id) \ + + encode_str(self.store_id) \ + + encode_str(self.store_name) \ + + encode_str(self.nick_name) \ + + encode_int(self.setting_title_id) \ + + encode_int(self.favorite_hero_log_id) \ + + encode_short(self.favorite_hero_log_awakening_stage) \ + + encode_int(self.favorite_support_log_id) \ + + encode_short(self.favorite_support_log_awakening_stage) \ + + encode_short(self.class_num) \ + + encode_int(self.total_league_point) + +class DefragMatchLeagueScoreRankingList(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.rank = decode_byte(data, off) + off += BYTE_OFF + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.store_id, new_off = decode_str(data, off) + off += new_off + + self.store_name, new_off = decode_str(data, off) + off += new_off + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self.setting_title_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_hero_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.favorite_support_log_id = decode_int(data, off) + off += INT_OFF + + self.favorite_support_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.class_num = decode_short(data, off) + off += SHORT_OFF + + self.have_league_score = decode_short(data, off) + off += SHORT_OFF + + @classmethod + def from_args(cls) -> "DefragMatchLeagueScoreRankingList": + return cls(b"\x00" * 40, 0) + + def make(self) -> bytes: + ret = encode_int(self.rank) + ret += encode_str(self.user_id) + ret += encode_str(self.store_id) + ret += encode_str(self.store_name) + ret += encode_str(self.nick_name) + + ret += encode_int(self.setting_title_id) + ret += encode_int(self.favorite_hero_log_id) + ret += encode_short(self.favorite_hero_log_awakening_stage) + ret += encode_int(self.favorite_support_log_id) + ret += encode_short(self.favorite_support_log_awakening_stage) + ret += encode_short(self.class_num) + ret += encode_short(self.have_league_score) + return ret + +class AppVersionData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.version_app_id = decode_int(data, off) + off += INT_OFF + + self.applying_start_date, new_off = decode_date_str(data, off) + off += new_off + + @classmethod + def from_args(cls, app_ver: int, start_date: datetime) -> BaseHelper: + ret = cls(b"\x00" * 8, 0) + ret.version_app_id = app_ver + ret.applying_start_date = start_date + return ret + + def make(self) -> bytes: + ret = encode_int(self.version_app_id) + ret += encode_date_str(self.applying_start_date) + return ret + +class MatchingErrorData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = 0 + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.param_1, new_off = decode_str(data, off) + off += new_off + + self.param_2, new_off = decode_str(data, off) + off += new_off + + self.param_3, new_off = decode_str(data, off) + off += new_off + + self.param_4, new_off = decode_str(data, off) + off += new_off + + self.param_5, new_off = decode_str(data, off) + off += new_off + + self.param_6, new_off = decode_str(data, off) + off += new_off + + self.param_7, new_off = decode_str(data, off) + off += new_off + + self.param_8, new_off = decode_str(data, off) + off += new_off + + self.param_9, new_off = decode_str(data, off) + off += new_off + + self.param_10, new_off = decode_str(data, off) + off += new_off + + self.error_occurred_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + def __str__(self) -> str: + s = f"User: {self.user_id} || " + s += f"Params: {self.param_1} {self.param_2} {self.param_3} {self.param_4} {self.param_5} {self.param_6} {self.param_7} {self.param_8} {self.param_9} {self.param_10} || " + s += f"Date: {self.error_occurred_date}" + return s + +class ReadProfileCard(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.profile_card_code, new_off = decode_str(data, off) # ID of the QR code + off += new_off + self.nick_name = decode_str(data, off) + off += new_off + + self.rank_num = decode_short(data, off) #short + off += SHORT_OFF + self.setting_title_id = decode_int(data, off) #int + off += INT_OFF + self.skill_id = decode_short(data, off) #short + off += SHORT_OFF + self.hero_log_hero_log_id = decode_int(data, off) #int + off += INT_OFF + self.hero_log_log_level = decode_short(data, off) #short + off += SHORT_OFF + self.hero_log_awakening_stage = decode_short(data, off) #short + off += SHORT_OFF + + self.hero_log_property1_property_id = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property1_value1 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property1_value2 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property2_property_id = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property2_value1 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property2_value2 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property3_property_id = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property3_value1 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property3_value2 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property4_property_id = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property4_value1 = decode_int(data, off) #int + off += INT_OFF + self.hero_log_property4_value2 = decode_int(data, off) #int + off += INT_OFF + + self.main_weapon_equipment_id = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_enhancement_value = decode_short(data, off) #short + off += SHORT_OFF + self.main_weapon_awakening_stage = decode_short(data, off) #short + off += SHORT_OFF + + self.main_weapon_property1_property_id = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property1_value1 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property1_value2 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property2_property_id = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property2_value1 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property2_value2 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property3_property_id = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property3_value1 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property3_value2 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property4_property_id = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property4_value1 = decode_int(data, off) #int + off += INT_OFF + self.main_weapon_property4_value2 = decode_int(data, off) #int + off += INT_OFF + + self.sub_equipment_equipment_id = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_enhancement_value = decode_short(data, off) #short + off += SHORT_OFF + self.sub_equipment_awakening_stage = decode_short(data, off) #short + off += SHORT_OFF + + self.sub_equipment_property1_property_id = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property1_value1 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property1_value2 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property2_property_id = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property2_value1 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property2_value2 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property3_property_id = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property3_value1 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property3_value2 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property4_property_id = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property4_value1 = decode_int(data, off) #int + off += INT_OFF + self.sub_equipment_property4_value2 = decode_int(data, off) #int + off += INT_OFF + + self.holographic_flag = decode_byte(data, off) #byte + off += BYTE_OFF + self._sz = off - offset + + @classmethod + def from_args(cls, code: str, player_name: str) -> "ReadProfileCard": + resp = cls(b"\x00" * 44, 0) + resp.profile_card_code = code # ID of the QR code + resp.nick_name = player_name + resp.rank_num = 1 #short + resp.setting_title_id = 20005 #int + resp.skill_id = 0 #short + resp.hero_log_hero_log_id = 118000230 #int + resp.hero_log_log_level = 1 #short + resp.hero_log_awakening_stage = 1 #short + + resp.hero_log_property1_property_id = 0 #int + resp.hero_log_property1_value1 = 0 #int + resp.hero_log_property1_value2 = 0 #int + resp.hero_log_property2_property_id = 0 #int + resp.hero_log_property2_value1 = 0 #int + resp.hero_log_property2_value2 = 0 #int + resp.hero_log_property3_property_id = 0 #int + resp.hero_log_property3_value1 = 0 #int + resp.hero_log_property3_value2 = 0 #int + resp.hero_log_property4_property_id = 0 #int + resp.hero_log_property4_value1 = 0 #int + resp.hero_log_property4_value2 = 0 #int + + resp.main_weapon_equipment_id = 0 #int + resp.main_weapon_enhancement_value = 0 #short + resp.main_weapon_awakening_stage = 0 #short + + resp.main_weapon_property1_property_id = 0 #int + resp.main_weapon_property1_value1 = 0 #int + resp.main_weapon_property1_value2 = 0 #int + resp.main_weapon_property2_property_id = 0 #int + resp.main_weapon_property2_value1 = 0 #int + resp.main_weapon_property2_value2 = 0 #int + resp.main_weapon_property3_property_id = 0 #int + resp.main_weapon_property3_value1 = 0 #int + resp.main_weapon_property3_value2 = 0 #int + resp.main_weapon_property4_property_id = 0 #int + resp.main_weapon_property4_value1 = 0 #int + resp.main_weapon_property4_value2 = 0 #int + + resp.sub_equipment_equipment_id = 0 #int + resp.sub_equipment_enhancement_value = 0 #short + resp.sub_equipment_awakening_stage = 0 #short + + resp.sub_equipment_property1_property_id = 0 #int + resp.sub_equipment_property1_value1 = 0 #int + resp.sub_equipment_property1_value2 = 0 #int + resp.sub_equipment_property2_property_id = 0 #int + resp.sub_equipment_property2_value1 = 0 #int + resp.sub_equipment_property2_value2 = 0 #int + resp.sub_equipment_property3_property_id = 0 #int + resp.sub_equipment_property3_value1 = 0 #int + resp.sub_equipment_property3_value2 = 0 #int + resp.sub_equipment_property4_property_id = 0 #int + resp.sub_equipment_property4_value1 = 0 #int + resp.sub_equipment_property4_value2 = 0 #int + + resp.holographic_flag = 0 #byte + return resp + +class HeroLogUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.user_hero_log_id, new_off = decode_str(data, off) + off += new_off + self.hero_log_id = decode_int(data, off) + off += INT_OFF + self.log_level = decode_short(data, off) + off += SHORT_OFF + self.max_log_level_extended_num = decode_short(data, off) + off += SHORT_OFF + self.log_exp = decode_int(data, off) + off += INT_OFF + self.possible_awakening_flag = decode_byte(data, off) + off += BYTE_OFF + self.awakening_stage = decode_short(data, off) + off += SHORT_OFF + self.awakening_exp = decode_int(data, off) + off += INT_OFF + self.skill_slot_correction_value = decode_byte(data, off) + off += BYTE_OFF + self.last_set_skill_slot1_skill_id = decode_short(data, off) + off += SHORT_OFF + self.last_set_skill_slot2_skill_id = decode_short(data, off) + off += SHORT_OFF + self.last_set_skill_slot3_skill_id = decode_short(data, off) + off += SHORT_OFF + self.last_set_skill_slot4_skill_id = decode_short(data, off) + off += SHORT_OFF + self.last_set_skill_slot5_skill_id = decode_short(data, off) + off += SHORT_OFF + self.property1_property_id = decode_int(data, off) + off += INT_OFF + self.property1_value1 = decode_int(data, off) + off += INT_OFF + self.property1_value2 = decode_int(data, off) + off += INT_OFF + self.property2_property_id = decode_int(data, off) + off += INT_OFF + self.property2_value1 = decode_int(data, off) + off += INT_OFF + self.property2_value2 = decode_int(data, off) + off += INT_OFF + self.property3_property_id = decode_int(data, off) + off += INT_OFF + self.property3_value1 = decode_int(data, off) + off += INT_OFF + self.property3_value2 = decode_int(data, off) + off += INT_OFF + self.property4_property_id = decode_int(data, off) + off += INT_OFF + self.property4_value1 = decode_int(data, off) + off += INT_OFF + self.property4_value2 = decode_int(data, off) + off += INT_OFF + self.converted_card_num = decode_short(data, off) + off += SHORT_OFF + self.shop_purchase_flag = decode_byte(data, off) + off += BYTE_OFF + self.protect_flag = decode_byte(data, off) + off += BYTE_OFF + + get_date, new_off = decode_str(data, off) + off += new_off + self.get_date = prs_dt(get_date) + self.sz = off - offset + + @classmethod + def from_args(cls, hero_data: Dict) -> "HeroLogUserData": + ret = cls(b"\x00" * 90, 0) + # Seems user_hero_log_id is a globally unique identifier, while hero_log_id identifies the hero + ret.user_hero_log_id = f"{hero_data['id']}" + ret.hero_log_id = hero_data['hero_log_id'] + + ret.log_level = hero_data['log_level'] + ret.max_log_level_extended_num = hero_data['max_level_extend_num'] + ret.log_exp = hero_data['log_exp'] + + ret.possible_awakening_flag = hero_data['is_awakenable'] + ret.awakening_stage = hero_data['awakening_stage'] + ret.awakening_exp = hero_data['awakening_exp'] + + ret.skill_slot_correction_value = 0 # Allows unlocking skill slot 4 and 5 early + ret.last_set_skill_slot1_skill_id = hero_data['skill_slot1_skill_id'] + ret.last_set_skill_slot2_skill_id = hero_data['skill_slot2_skill_id'] + ret.last_set_skill_slot3_skill_id = hero_data['skill_slot3_skill_id'] + ret.last_set_skill_slot4_skill_id = hero_data['skill_slot4_skill_id'] + ret.last_set_skill_slot5_skill_id = hero_data['skill_slot5_skill_id'] + + ret.property1_property_id = hero_data['property1_property_id'] + ret.property1_value1 = hero_data['property1_value1'] + ret.property1_value2 = hero_data['property1_value2'] + ret.property2_property_id = hero_data['property2_property_id'] + ret.property2_value1 = hero_data['property2_value1'] + ret.property2_value2 = hero_data['property2_value2'] + ret.property3_property_id = hero_data['property2_property_id'] + ret.property3_value1 = hero_data['property3_value1'] + ret.property3_value2 = hero_data['property3_value2'] + ret.property4_property_id = hero_data['property2_property_id'] + ret.property4_value1 = hero_data['property4_value1'] + ret.property4_value2 = hero_data['property4_value2'] + + ret.converted_card_num = hero_data['converted_card_num'] + ret.shop_purchase_flag = hero_data['is_shop_purchase'] + ret.protect_flag = hero_data['is_protect'] + ret.get_date = hero_data['get_date'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_hero_log_id) \ + + encode_int(self.hero_log_id) \ + + encode_short(self.log_level) \ + + encode_short(self.max_log_level_extended_num) \ + + encode_int(self.log_exp) \ + + encode_byte(self.possible_awakening_flag) \ + + encode_short(self.awakening_stage) \ + + encode_int(self.awakening_exp) \ + + encode_byte(self.skill_slot_correction_value) \ + + encode_short(self.last_set_skill_slot1_skill_id) \ + + encode_short(self.last_set_skill_slot2_skill_id) \ + + encode_short(self.last_set_skill_slot3_skill_id) \ + + encode_short(self.last_set_skill_slot4_skill_id) \ + + encode_short(self.last_set_skill_slot5_skill_id) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) \ + + encode_short(self.converted_card_num) \ + + encode_byte(self.shop_purchase_flag) \ + + encode_byte(self.protect_flag) \ + + encode_date_str(self.get_date) + +class YuiMedalBonusUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.elapsed_days = decode_int(data, off) + off += INT_OFF + self.loop_num = decode_int(data, off) + off += INT_OFF + + last_check_date, new_off = decode_str(data, off) + off += new_off + self.last_check_date = prs_dt(last_check_date) + + last_get_date, new_off = decode_str(data, off) + off += new_off + self.last_get_date = prs_dt(last_get_date) + + self._sz = off - offset + + @classmethod + def from_args(cls, elapsed_days: int = 0, loop_num: int = 0) -> BaseHelper: + ret = cls(b"\x00" * 996, 0) + ret.elapsed_days = elapsed_days + ret.loop_num = loop_num + ret.last_check_date = datetime.fromtimestamp(0) + ret.last_get_date = datetime.fromtimestamp(0) + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.elapsed_days) \ + + encode_int(self.loop_num) \ + + encode_date_str(self.last_check_date) \ + + encode_date_str(self.last_get_date) + +class UserBasicData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.user_type = decode_short(data, off) + off += SHORT_OFF + + self.nick_name, new_off = decode_str(data, off) + off += new_off + + self.rank_num = decode_short(data, off) + off += SHORT_OFF + + self.rank_exp = decode_int(data, off) + off += INT_OFF + + self.own_col = decode_int(data, off) + off += INT_OFF + + self.own_vp = decode_int(data, off) + off += INT_OFF + + self.own_yui_medal = decode_int(data, off) + off += INT_OFF + + self.setting_title_id = decode_int(data, off) + off += INT_OFF + + self.favorite_user_hero_log_id, new_off = decode_str(data, off) + off += new_off + + self.favorite_user_support_log_id, new_off = decode_str(data, off) + off += new_off + + self.my_store_id, new_off = decode_str(data, off) + off += new_off + + self.my_store_name, new_off = decode_str(data, off) + off += new_off + + user_reg_date, new_off = decode_str(data, off) + self.user_reg_date = prs_dt(user_reg_date) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, profile_data: Dict, shop_name: str = "ARTEMiS") -> "UserBasicData": + ret = cls(b"\0" * 52, 0) + ret.user_type = profile_data['user_type'] + ret.nick_name = profile_data['nick_name'] + ret.rank_num = profile_data['rank_num'] + ret.rank_exp = profile_data['rank_exp'] + ret.own_col = profile_data['own_col'] + ret.own_vp = profile_data['own_vp'] + ret.own_yui_medal = profile_data['own_yui_medal'] + ret.setting_title_id = profile_data['setting_title_id'] + ret.favorite_user_hero_log_id = profile_data['fav_hero'] + ret.favorite_user_support_log_id = "" # TODO: Supports + ret.my_store_id = "JPN0" + f"{profile_data['my_shop']:04X}" if profile_data["my_shop"] else "0" + ret.my_store_name = shop_name + ret.user_reg_date = profile_data['when_register'] if profile_data['when_register'] else prs_dt("20230101120000") + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.user_type) \ + + encode_str(self.nick_name) \ + + encode_short(self.rank_num) \ + + encode_int(self.rank_exp) \ + + encode_int(self.own_col) \ + + encode_int(self.own_vp) \ + + encode_int(self.own_yui_medal) \ + + encode_int(self.setting_title_id) \ + + encode_str(self.favorite_user_hero_log_id) \ + + encode_str(self.favorite_user_support_log_id) \ + + encode_str(self.my_store_id) \ + + encode_str(self.my_store_name) \ + + encode_date_str(self.user_reg_date) \ + +class VpGashaTicketData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.remaining_own_vp_gasha_ticket = decode_int(data, off) + off += INT_OFF + + self.expire_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, ticket_remain: int, exp_date: datetime) -> "VpGashaTicketData": + ret = cls(b"\x00" * 8, 0) + ret.remaining_own_vp_gasha_ticket = ticket_remain + ret.expire_date = exp_date + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.remaining_own_vp_gasha_ticket) \ + + encode_date_str(self.expire_date) + +class BeginnerMissionUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ad_confirm_flag = decode_byte(data, off) + off += BYTE_OFF + + self.ad_confirm_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, confirm_date: datetime, confirm_flg: bool = False) -> "BeginnerMissionUserData": + ret = cls(b"\x00" * 99, 0) + ret.ad_confirm_flag = confirm_flg + ret.ad_confirm_date = confirm_date + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.ad_confirm_flag) \ + + encode_date_str(self.ad_confirm_date) + +class EquipmentUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.user_equipment_id, new_off = decode_str(data, off) + off += new_off + + self.equipment_id = decode_int(data, off) + off += INT_OFF + self.enhancement_value = decode_short(data, off) + off += SHORT_OFF + self.max_enhancement_value_extended_num = decode_short(data, off) + off += SHORT_OFF + self.enhancement_exp = decode_int(data, off) + off += INT_OFF + self.possible_awakening_flag = decode_byte(data, off) + off += BYTE_OFF + self.awakening_stage = decode_short(data, off) + off += SHORT_OFF + self.awakening_exp = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + self.property1_value1 = decode_int(data, off) + off += INT_OFF + self.property1_value2 = decode_int(data, off) + off += INT_OFF + self.property2_property_id = decode_int(data, off) + off += INT_OFF + self.property2_value1 = decode_int(data, off) + off += INT_OFF + self.property2_value2 = decode_int(data, off) + off += INT_OFF + self.property3_property_id = decode_int(data, off) + off += INT_OFF + self.property3_value1 = decode_int(data, off) + off += INT_OFF + self.property3_value2 = decode_int(data, off) + off += INT_OFF + self.property4_property_id = decode_int(data, off) + off += INT_OFF + self.property4_value1 = decode_int(data, off) + off += INT_OFF + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self.converted_card_num = decode_short(data, off) + off += SHORT_OFF + self.shop_purchase_flag = decode_byte(data, off) + off += BYTE_OFF + self.protect_flag = decode_byte(data, off) + off += BYTE_OFF + + self.get_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, equip_data: Dict) -> "EquipmentUserData": + ret = cls(b"\x00" * 79, 0) + ret.user_equipment_id = str(equip_data['id']) + ret.equipment_id = equip_data['equipment_id'] + ret.enhancement_value = equip_data['enhancement_value'] + ret.max_enhancement_value_extended_num = 0 #equip_data['max_enhancement_value_extended_num'] TODO: This + ret.enhancement_exp = equip_data['enhancement_exp'] + ret.possible_awakening_flag = equip_data['possible_awakening_flag'] + ret.awakening_stage = equip_data['awakening_stage'] + ret.awakening_exp = equip_data['awakening_exp'] + ret.property1_property_id = equip_data['property1_property_id'] + ret.property1_value1 = equip_data['property1_value1'] + ret.property1_value2 = equip_data['property1_value2'] + ret.property2_property_id = equip_data['property2_property_id'] + ret.property2_value1 = equip_data['property2_value1'] + ret.property2_value2 = equip_data['property2_value2'] + ret.property3_property_id = equip_data['property2_property_id'] + ret.property3_value1 = equip_data['property3_value1'] + ret.property3_value2 = equip_data['property3_value2'] + ret.property4_property_id = equip_data['property2_property_id'] + ret.property4_value1 = equip_data['property4_value1'] + ret.property4_value2 = equip_data['property4_value2'] + ret.converted_card_num = equip_data['converted_card_num'] + ret.shop_purchase_flag = equip_data['is_shop_purchase'] + ret.protect_flag = equip_data['is_protect'] + ret.get_date = equip_data['get_date'] + return ret + + def make(self) -> bytes: + return encode_str(self.user_equipment_id) \ + + encode_int(self.equipment_id) \ + + encode_short(self.enhancement_value) \ + + encode_short(self.max_enhancement_value_extended_num) \ + + encode_int(self.enhancement_exp) \ + + encode_byte(self.possible_awakening_flag) \ + + encode_short(self.awakening_stage) \ + + encode_int(self.awakening_exp) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) \ + + encode_short(self.converted_card_num) \ + + encode_byte(self.shop_purchase_flag) \ + + encode_byte(self.protect_flag) \ + + encode_date_str(self.get_date) + +class ItemUserData(BaseHelper): + def __init__(self, data: bytes, offset: int) -> None: + super().__init__(data, offset) + off = offset + self.user_item_id, new_off = decode_str(data, off) + off += new_off + self.item_id = decode_int(data, off) + off += INT_OFF + self.protect_flag = decode_byte(data, off) + off += BYTE_OFF + self.get_date = decode_date_str(data, off) + + self._sz = off - offset + + @classmethod + def from_args(cls, item_data: Dict) -> BaseHelper: + ret = cls(b"\x00" * 993, 0) + ret.user_item_id = str(item_data['id']) + ret.item_id = item_data['item_id'] + ret.protect_flag = 0 + ret.get_date = item_data['get_date'] + return ret + + def make(self) -> bytes: + return encode_str(self.user_item_id) \ + + encode_int(self.item_id) \ + + encode_byte(self.protect_flag) \ + + encode_date_str(self.get_date) + +class SupportLogUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_support_log_id, new_off = decode_str(data, off) + off += new_off + + self.support_log_id = decode_int(data, off) + off += INT_OFF + + self.possible_awakening_flag = decode_byte(data, off) + off += BYTE_OFF + + self.awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.awakening_exp = decode_int(data, off) + off += INT_OFF + + self.converted_card_num = decode_short(data, off) + off += SHORT_OFF + + self.shop_purchase_flag = decode_byte(data, off) + off += BYTE_OFF + + self.protect_flag = decode_byte(data, off) + off += BYTE_OFF + + self.get_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, user_support_log: str, support_log: int) -> "SupportLogUserData": + ret = cls(b"\x00" * 23, 0) + ret.user_support_log_id = user_support_log + ret.support_log_id = support_log + ret.possible_awakening_flag = 0 + ret.awakening_stage = 0 + ret.awakening_exp = 0 + ret.converted_card_num = 0 + ret.shop_purchase_flag = 0 + ret.protect_flag = 0 + ret.get_date = prs_dt() + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_support_log_id) \ + + encode_int(self.support_log_id) \ + + encode_byte(self.possible_awakening_flag) \ + + encode_short(self.awakening_stage) \ + + encode_int(self.awakening_exp) \ + + encode_short(self.converted_card_num) \ + + encode_byte(self.shop_purchase_flag) \ + + encode_byte(self.protect_flag) \ + + encode_date_str(self.get_date) + +class EventItemUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_event_item_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.event_item_id = decode_int(data, off) + off += INT_OFF + + self.own_num = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EventItemUserData": + ret = cls(b"\x00" * 996, 0) + ret.user_event_item_id = data['UserEventItemId'] + ret.user_id = data['UserId'] + ret.event_item_id = data['EventItemId'] + ret.own_num = data['OwnNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_event_item_id) \ + + encode_str(self.user_id) \ + + encode_int(self.event_item_id) \ + + encode_int(self.own_num) + +class GashaMedalUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_gasha_medal_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.gasha_medal_id = decode_int(data, off) + off += INT_OFF + + self.own_num = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalUserData": + ret = cls(b"\x00" * 996, 0) + ret.user_gasha_medal_id = data['UserGashaMedalId'] + ret.user_id = data['UserId'] + ret.gasha_medal_id = data['GashaMedalId'] + ret.own_num = data['OwnNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_gasha_medal_id) \ + + encode_str(self.user_id) \ + + encode_int(self.gasha_medal_id) \ + + encode_int(self.own_num) + +class TitleUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_title_id, new_off = decode_str(data, off) + off += new_off + + self.title_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, user_title_id: str, title_id: int) -> "TitleUserData": + ret = cls(b"\x00" * 8, 0) + ret.user_title_id = user_title_id + ret.title_id = title_id + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_title_id) \ + + encode_int(self.title_id) + +class PlayerRankData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.player_rank_id = decode_short(data, off) + off += SHORT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self.storage = decode_short(data, off) + off += SHORT_OFF + + self.team_preset = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PlayerRankData": + ret = cls(b"\x00" * 99, 0) + ret.player_rank_id = data['PlayerRankId'] + ret.total_exp = data['TotalExp'] + ret.storage = data['Storage'] + ret.team_preset = data['TeamPreset'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.player_rank_id) \ + + encode_int(self.total_exp) \ + + encode_short(self.storage) \ + + encode_short(self.team_preset) + +class TitleData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.title_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.requirement = decode_int(data, off) + off += INT_OFF + + self.value1 = decode_int(data, off) + off += INT_OFF + + self.value2 = decode_int(data, off) + off += INT_OFF + + self.rank = decode_int(data, off) + off += INT_OFF + + self.image_file_path, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TitleData": + ret = cls(b"\x00" * 99, 0) + ret.title_id = data['TitleId'] + ret.display_name = data['DisplayName'] + ret.requirement = data['Requirement'] + ret.value1 = data['Value1'] + ret.value2 = data['Value2'] + ret.rank = data['Rank'] + ret.image_file_path = data['ImageFilePath'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.title_id) \ + + encode_str(self.display_name) \ + + encode_int(self.requirement) \ + + encode_int(self.value1) \ + + encode_int(self.value2) \ + + encode_int(self.rank) \ + + encode_str(self.image_file_path) + +class FragmentData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.fragment_id = decode_short(data, off) + off += SHORT_OFF + + self.exp = decode_int(data, off) + off += INT_OFF + + self.comment_id, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "FragmentData": + ret = cls(b"\x00" * 99, 0) + ret.fragment_id = data['FragmentId'] + ret.exp = data['Exp'] + ret.comment_id = data['CommentId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.fragment_id) \ + + encode_int(self.exp) \ + + encode_str(self.comment_id) + +class RewardTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.reward_table_id = decode_int(data, off) + off += INT_OFF + + self.reward_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.unanalyzed_log_grade_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.strength_min = decode_int(data, off) + off += INT_OFF + + self.strength_max = decode_int(data, off) + off += INT_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.rate = decode_int(data, off) + off += INT_OFF + + self.quest_info_display_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "RewardTableData": + ret = cls(b"\x00" * 99, 0) + ret.reward_table_id = data['RewardTableId'] + ret.reward_table_sub_id = data['RewardTableSubId'] + ret.unanalyzed_log_grade_id = data['UnanalyzedLogGradeId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength_min = data['StrengthMin'] + ret.strength_max = data['StrengthMax'] + ret.property_table_sub_id = data['PropertyTableSubId'] + ret.rate = data['Rate'] + ret.quest_info_display_flag = data['QuestInfoDisplayFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.reward_table_id) \ + + encode_int(self.reward_table_sub_id) \ + + encode_int(self.unanalyzed_log_grade_id) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.strength_min) \ + + encode_int(self.strength_max) \ + + encode_int(self.property_table_sub_id) \ + + encode_int(self.rate) \ + + encode_byte(self.quest_info_display_flag) + +class RewardSetData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.reward_set_id = decode_int(data, off) + off += INT_OFF + + self.reward_set_sub_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "RewardSetData": + ret = cls(b"\x00" * 99, 0) + ret.reward_set_id = data['RewardSetId'] + ret.reward_set_sub_id = data['RewardSetSubId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.reward_set_id) \ + + encode_int(self.reward_set_sub_id) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class UnanalyzedLogGradeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unanalyzed_log_grade_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.comment_id, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "UnanalyzedLogGradeData": + ret = cls(b"\x00" * 99, 0) + ret.unanalyzed_log_grade_id = data['UnanalyzedLogGradeId'] + ret.name = data['Name'] + ret.comment_id = data['CommentId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unanalyzed_log_grade_id) \ + + encode_str(self.name) \ + + encode_str(self.comment_id) + +class AppointLeaderParamData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.appoint_leader_param_id = decode_short(data, off) + off += SHORT_OFF + + self.initial_synchro_rate = decode_int(data, off) + off += INT_OFF + + self.appoint_leader_increment_synchro_rate = decode_int(data, off) + off += INT_OFF + + self.awakening_increment_synchro_rate = decode_int(data, off) + off += INT_OFF + + self.foil_add_synchro_rate = decode_int(data, off) + off += INT_OFF + + self.appoint_leader_trust_bonus = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AppointLeaderParamData": + ret = cls(b"\x00" * 99, 0) + ret.appoint_leader_param_id = data['AppointLeaderParamId'] + ret.initial_synchro_rate = data['InitialSynchroRate'] + ret.appoint_leader_increment_synchro_rate = data['AppointLeaderIncrementSynchroRate'] + ret.awakening_increment_synchro_rate = data['AwakeningIncrementSynchroRate'] + ret.foil_add_synchro_rate = data['FoilAddSynchroRate'] + ret.appoint_leader_trust_bonus = data['AppointLeaderTrustBonus'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.appoint_leader_param_id) \ + + encode_int(self.initial_synchro_rate) \ + + encode_int(self.appoint_leader_increment_synchro_rate) \ + + encode_int(self.awakening_increment_synchro_rate) \ + + encode_int(self.foil_add_synchro_rate) \ + + encode_int(self.appoint_leader_trust_bonus) + +class AppointLeaderEffectData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.appoint_leader_effect_id = decode_short(data, off) + off += SHORT_OFF + + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.info_text_format, new_off = decode_str(data, off) + off += new_off + + self.appoint_leader_effect_type_id = decode_short(data, off) + off += SHORT_OFF + + self.low_effect_value, new_off = decode_str(data, off) + off += new_off + + self.middle_effect_value, new_off = decode_str(data, off) + off += new_off + + self.high_effect_value, new_off = decode_str(data, off) + off += new_off + + self.max_effect_value, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AppointLeaderEffectData": + ret = cls(b"\x00" * 99, 0) + ret.appoint_leader_effect_id = data['AppointLeaderEffectId'] + ret.chara_id = data['CharaId'] + ret.info_text_format = data['InfoTextFormat'] + ret.appoint_leader_effect_type_id = data['AppointLeaderEffectTypeId'] + ret.low_effect_value = data['LowEffectValue'] + ret.middle_effect_value = data['MiddleEffectValue'] + ret.high_effect_value = data['HighEffectValue'] + ret.max_effect_value = data['MaxEffectValue'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.appoint_leader_effect_id) \ + + encode_short(self.chara_id) \ + + encode_str(self.info_text_format) \ + + encode_short(self.appoint_leader_effect_type_id) \ + + encode_str(self.low_effect_value) \ + + encode_str(self.middle_effect_value) \ + + encode_str(self.high_effect_value) \ + + encode_str(self.max_effect_value) + +class AppointLeaderEffectTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.appoint_leader_effect_type_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AppointLeaderEffectTypeData": + ret = cls(b"\x00" * 99, 0) + ret.appoint_leader_effect_type_id = data['AppointLeaderEffectTypeId'] + ret.name = data['Name'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.appoint_leader_effect_type_id) \ + + encode_str(self.name) + +class RarityData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.rarity_id = decode_short(data, off) + off += SHORT_OFF + + self.require_col_my_card = decode_int(data, off) + off += INT_OFF + + self.require_col_other_card = decode_int(data, off) + off += INT_OFF + + self.require_medal_other_card = decode_int(data, off) + off += INT_OFF + + self.synthesis_exp_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "RarityData": + ret = cls(b"\x00" * 99, 0) + ret.rarity_id = data['RarityId'] + ret.require_col_my_card = data['RequireColMyCard'] + ret.require_col_other_card = data['RequireColOtherCard'] + ret.require_medal_other_card = data['RequireMedalOtherCard'] + ret.synthesis_exp_rate = data['SynthesisExpRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.rarity_id) \ + + encode_int(self.require_col_my_card) \ + + encode_int(self.require_col_other_card) \ + + encode_int(self.require_medal_other_card) \ + + encode_str(self.synthesis_exp_rate) + +class CompositionEventData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.composition_event_id = decode_short(data, off) + off += SHORT_OFF + + self.composition_exp_rate, new_off = decode_str(data, off) + off += new_off + + self.awakening_exp_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CompositionEventData": + ret = cls(b"\x00" * 99, 0) + ret.composition_event_id = data['CompositionEventId'] + ret.composition_exp_rate = data['CompositionExpRate'] + ret.awakening_exp_rate = data['AwakeningExpRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.composition_event_id) \ + + encode_str(self.composition_exp_rate) \ + + encode_str(self.awakening_exp_rate) + +class CompositionParamData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.composition_param_id = decode_short(data, off) + off += SHORT_OFF + + self.use_value = decode_int(data, off) + off += INT_OFF + + self.max_extended_use_coef, new_off = decode_str(data, off) + off += new_off + + self.awakening_coef, new_off = decode_str(data, off) + off += new_off + + self.use_value_support_log = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CompositionParamData": + ret = cls(b"\x00" * 99, 0) + ret.composition_param_id = data['CompositionParamId'] + ret.use_value = data['UseValue'] + ret.max_extended_use_coef = data['MaxExtendedUseCoef'] + ret.awakening_coef = data['AwakeningCoef'] + ret.use_value_support_log = data['UseValueSupportLog'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.composition_param_id) \ + + encode_int(self.use_value) \ + + encode_str(self.max_extended_use_coef) \ + + encode_str(self.awakening_coef) \ + + encode_int(self.use_value_support_log) + +class GamePlayPriceData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.game_play_price_id = decode_short(data, off) + off += SHORT_OFF + + self.player_rank_id = decode_short(data, off) + off += SHORT_OFF + + self.episode_ticket = decode_int(data, off) + off += INT_OFF + + self.trial_tower_ticket = decode_short(data, off) + off += SHORT_OFF + + self.custom_retry_ticket = decode_short(data, off) + off += SHORT_OFF + + self.subdue_ticket = decode_short(data, off) + off += SHORT_OFF + + self.continue_ticket = decode_short(data, off) + off += SHORT_OFF + + self.continue_credit = decode_short(data, off) + off += SHORT_OFF + + self.extend_time_ticket = decode_short(data, off) + off += SHORT_OFF + + self.reward_grade_up_ticket = decode_short(data, off) + off += SHORT_OFF + + self.reward_grade_up_credit = decode_short(data, off) + off += SHORT_OFF + + self.give_free_ticket = decode_short(data, off) + off += SHORT_OFF + + self.free_continue_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GamePlayPriceData": + ret = cls(b"\x00" * 99, 0) + ret.game_play_price_id = data['GamePlayPriceId'] + ret.player_rank_id = data['PlayerRankId'] + ret.episode_ticket = data['EpisodeTicket'] + ret.trial_tower_ticket = data['TrialTowerTicket'] + ret.custom_retry_ticket = data['CustomRetryTicket'] + ret.subdue_ticket = data['SubdueTicket'] + ret.continue_ticket = data['ContinueTicket'] + ret.continue_credit = data['ContinueCredit'] + ret.extend_time_ticket = data['ExtendTimeTicket'] + ret.reward_grade_up_ticket = data['RewardGradeUpTicket'] + ret.reward_grade_up_credit = data['RewardGradeUpCredit'] + ret.give_free_ticket = data['GiveFreeTicket'] + ret.free_continue_num = data['FreeContinueNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.game_play_price_id) \ + + encode_short(self.player_rank_id) \ + + encode_int(self.episode_ticket) \ + + encode_short(self.trial_tower_ticket) \ + + encode_short(self.custom_retry_ticket) \ + + encode_short(self.subdue_ticket) \ + + encode_short(self.continue_ticket) \ + + encode_short(self.continue_credit) \ + + encode_short(self.extend_time_ticket) \ + + encode_short(self.reward_grade_up_ticket) \ + + encode_short(self.reward_grade_up_credit) \ + + encode_short(self.give_free_ticket) \ + + encode_short(self.free_continue_num) + +class BuyTicketData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.buy_ticket_id = decode_int(data, off) + off += INT_OFF + + self.buy_ticket_pattern = decode_int(data, off) + off += INT_OFF + + self.credit_cnt = decode_int(data, off) + off += INT_OFF + + self.get_ticket_cnt = decode_int(data, off) + off += INT_OFF + + self.get_bonus_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BuyTicketData": + ret = cls(b"\x00" * 99, 0) + ret.buy_ticket_id = data['BuyTicketId'] + ret.buy_ticket_pattern = data['BuyTicketPattern'] + ret.credit_cnt = data['CreditCnt'] + ret.get_ticket_cnt = data['GetTicketCnt'] + ret.get_bonus_vp = data['GetBonusVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.buy_ticket_id) \ + + encode_int(self.buy_ticket_pattern) \ + + encode_int(self.credit_cnt) \ + + encode_int(self.get_ticket_cnt) \ + + encode_int(self.get_bonus_vp) + +class TipsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.tips_id = decode_int(data, off) + off += INT_OFF + + self.tips_category = decode_byte(data, off) + off += BYTE_OFF + + self.tips_text, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TipsData": + ret = cls(b"\x00" * 99, 0) + ret.tips_id = data['TipsId'] + ret.tips_category = data['TipsCategory'] + ret.tips_text = data['TipsText'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.tips_id) \ + + encode_byte(self.tips_category) \ + + encode_str(self.tips_text) + +class CapData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.cap_id = decode_short(data, off) + off += SHORT_OFF + + self.trust1_cap = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CapData": + ret = cls(b"\x00" * 99, 0) + ret.cap_id = data['CapId'] + ret.trust1_cap = data['Trust1Cap'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.cap_id) \ + + encode_int(self.trust1_cap) + +class HeroLogData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.hero_log_id = decode_int(data, off) + off += INT_OFF + + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.nickname, new_off = decode_str(data, off) + off += new_off + + self.rarity = decode_byte(data, off) + off += BYTE_OFF + + self.weapon_type_id = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_role_id = decode_short(data, off) + off += SHORT_OFF + + self.costume_type_id = decode_short(data, off) + off += SHORT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.default_equipment_id1 = decode_int(data, off) + off += INT_OFF + + self.default_equipment_id2 = decode_int(data, off) + off += INT_OFF + + self.skill_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.hp_min = decode_int(data, off) + off += INT_OFF + + self.hp_max = decode_int(data, off) + off += INT_OFF + + self.str_min = decode_int(data, off) + off += INT_OFF + + self.str_max = decode_int(data, off) + off += INT_OFF + + self.vit_min = decode_int(data, off) + off += INT_OFF + + self.vit_max = decode_int(data, off) + off += INT_OFF + + self.int_min = decode_int(data, off) + off += INT_OFF + + self.int_max = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.sale_price = decode_int(data, off) + off += INT_OFF + + self.composition_exp = decode_int(data, off) + off += INT_OFF + + self.awakening_exp = decode_int(data, off) + off += INT_OFF + + self.slot4_unlock_level = decode_int(data, off) + off += INT_OFF + + self.slot5_unlock_level = decode_int(data, off) + off += INT_OFF + + self.cut_in_image, new_off = decode_str(data, off) + off += new_off + + self.cut_in_image_awake, new_off = decode_str(data, off) + off += new_off + + self.cut_in_upper_side_text = decode_byte(data, off) + off += BYTE_OFF + + self.chara_comment_image, new_off = decode_str(data, off) + off += new_off + + self.chara_comment_image_awake, new_off = decode_str(data, off) + off += new_off + + self.quest_start_introduce, new_off = decode_str(data, off) + off += new_off + + self.quest_start_introduce_awake, new_off = decode_str(data, off) + off += new_off + + self.quest_chara_icon, new_off = decode_str(data, off) + off += new_off + + self.quest_chara_icon_awake, new_off = decode_str(data, off) + off += new_off + + self.quest_chara_icon_loss, new_off = decode_str(data, off) + off += new_off + + self.quest_chara_icon_awake_loss, new_off = decode_str(data, off) + off += new_off + + self.collection_display_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.collection_empty_frame_display_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "HeroLogData": + ret = cls(b"\x00" * 99, 0) + ret.hero_log_id = data['HeroLogId'] + ret.chara_id = data['CharaId'] + ret.name = data['Name'] + ret.nickname = data['Nickname'] + ret.rarity = data['Rarity'] + ret.weapon_type_id = data['WeaponTypeId'] + ret.hero_log_role_id = data['HeroLogRoleId'] + ret.costume_type_id = data['CostumeTypeId'] + ret.unit_id = data['UnitId'] + ret.default_equipment_id1 = data['DefaultEquipmentId1'] + ret.default_equipment_id2 = data['DefaultEquipmentId2'] + ret.skill_table_sub_id = data['SkillTableSubId'] + ret.hp_min = data['HpMin'] + ret.hp_max = data['HpMax'] + ret.str_min = data['StrMin'] + ret.str_max = data['StrMax'] + ret.vit_min = data['VitMin'] + ret.vit_max = data['VitMax'] + ret.int_min = data['IntMin'] + ret.int_max = data['IntMax'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + ret.flavor_text = data['FlavorText'] + ret.sale_price = data['SalePrice'] + ret.composition_exp = data['CompositionExp'] + ret.awakening_exp = data['AwakeningExp'] + ret.slot4_unlock_level = data['Slot4UnlockLevel'] + ret.slot5_unlock_level = data['Slot5UnlockLevel'] + ret.cut_in_image = data['CutinImage'] + ret.cut_in_image_awake = data['CutinImageAwake'] + ret.cut_in_upper_side_text = data['CutinUpperSideText'] + ret.chara_comment_image = data['CharaCommentImage'] + ret.chara_comment_image_awake = data['CharaCommentImageAwake'] + ret.quest_start_introduce = data['QuestStartIntroduce'] + ret.quest_start_introduce_awake = data['QuestStartIntroduceAwake'] + ret.quest_chara_icon = data['QuestCharaIcon'] + ret.quest_chara_icon_awake = data['QuestCharaIconAwake'] + ret.quest_chara_icon_loss = data['QuestCharaIconLoss'] + ret.quest_chara_icon_awake_loss = data['QuestCharaIconAwakeLoss'] + ret.collection_display_start_date = data['CollectionDisplayStartDate'] + ret.collection_empty_frame_display_flag = data['CollectionEmptyFrameDisplayFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.hero_log_id) \ + + encode_short(self.chara_id) \ + + encode_str(self.name) \ + + encode_str(self.nickname) \ + + encode_byte(self.rarity) \ + + encode_short(self.weapon_type_id) \ + + encode_short(self.hero_log_role_id) \ + + encode_short(self.costume_type_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.default_equipment_id1) \ + + encode_int(self.default_equipment_id2) \ + + encode_int(self.skill_table_sub_id) \ + + encode_int(self.hp_min) \ + + encode_int(self.hp_max) \ + + encode_int(self.str_min) \ + + encode_int(self.str_max) \ + + encode_int(self.vit_min) \ + + encode_int(self.vit_max) \ + + encode_int(self.int_min) \ + + encode_int(self.int_max) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) \ + + encode_str(self.flavor_text) \ + + encode_int(self.sale_price) \ + + encode_int(self.composition_exp) \ + + encode_int(self.awakening_exp) \ + + encode_int(self.slot4_unlock_level) \ + + encode_int(self.slot5_unlock_level) \ + + encode_str(self.cut_in_image) \ + + encode_str(self.cut_in_image_awake) \ + + encode_byte(self.cut_in_upper_side_text) \ + + encode_str(self.chara_comment_image) \ + + encode_str(self.chara_comment_image_awake) \ + + encode_str(self.quest_start_introduce) \ + + encode_str(self.quest_start_introduce_awake) \ + + encode_str(self.quest_chara_icon) \ + + encode_str(self.quest_chara_icon_awake) \ + + encode_str(self.quest_chara_icon_loss) \ + + encode_str(self.quest_chara_icon_awake_loss) \ + + encode_date_str(self.collection_display_start_date) \ + + encode_byte(self.collection_empty_frame_display_flag) + +class HeroLogLevelData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.level = decode_short(data, off) + off += SHORT_OFF + + self.require_exp = decode_int(data, off) + off += INT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "HeroLogLevelData": + ret = cls(b"\x00" * 99, 0) + ret.level = data['HeroLogLevelId'] + ret.require_exp = data['RequireExp'] + ret.total_exp = data['TotalExp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.level) \ + + encode_int(self.require_exp) \ + + encode_int(self.total_exp) + +class HeroLogRoleData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.hero_log_role_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "HeroLogRoleData": + ret = cls(b"\x00" * 99, 0) + ret.hero_log_role_id = data['HeroLogRoleId'] + ret.name = data['Name'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.hero_log_role_id) \ + + encode_str(self.name) + +class HeroLogTrustRankData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.rank = decode_short(data, off) + off += SHORT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "HeroLogTrustRankData": + ret = cls(b"\x00" * 99, 0) + ret.rank = data['HeroLogTrustRankId'] + ret.total_exp = data['TotalExp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.rank) \ + + encode_int(self.total_exp) + +class CharaData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.roma, new_off = decode_str(data, off) + off += new_off + + self.gender = decode_short(data, off) + off += SHORT_OFF + + self.real_name, new_off = decode_str(data, off) + off += new_off + + self.comment, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CharaData": + ret = cls(b"\x00" * 99, 0) + ret.chara_id = data['CharaId'] + ret.name = data['Name'] + ret.roma = data['Roma'] + ret.gender = data['Gender'] + ret.real_name = data['RealName'] + ret.comment = data['Comment'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.chara_id) \ + + encode_str(self.name) \ + + encode_str(self.roma) \ + + encode_short(self.gender) \ + + encode_str(self.real_name) \ + + encode_str(self.comment) + +class CharaFriendlyRankData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.rank = decode_short(data, off) + off += SHORT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CharaFriendlyRankData": + ret = cls(b"\x00" * 99, 0) + ret.rank = data['CharaFriendlyRankId'] + ret.total_exp = data['TotalExp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.rank) \ + + encode_int(self.total_exp) + +class EquipmentData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.equipment_id = decode_int(data, off) + off += INT_OFF + + self.equipment_type = decode_byte(data, off) + off += BYTE_OFF + + self.weapon_type_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.rarity = decode_byte(data, off) + off += BYTE_OFF + + self.prefab, new_off = decode_str(data, off) + off += new_off + + self.power = decode_int(data, off) + off += INT_OFF + + self.strength_increment = decode_int(data, off) + off += INT_OFF + + self.skill_condition = decode_short(data, off) + off += SHORT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self.sale_price = decode_int(data, off) + off += INT_OFF + + self.composition_exp = decode_int(data, off) + off += INT_OFF + + self.awakening_exp = decode_int(data, off) + off += INT_OFF + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.collection_display_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.collection_empty_frame_display_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EquipmentData": + ret = cls(b"\x00" * 99, 0) + ret.equipment_id = data['EquipmentId'] + ret.equipment_type = data['EquipmentType'] + ret.weapon_type_id = data['WeaponTypeId'] + ret.name = data['Name'] + ret.rarity = data['Rarity'] + ret.prefab = data['Prefab'] + ret.power = data['Power'] + ret.strength_increment = data['StrengthIncrement'] + ret.skill_condition = data['SkillCondition'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + ret.sale_price = data['SalePrice'] + ret.composition_exp = data['CompositionExp'] + ret.awakening_exp = data['AwakeningExp'] + ret.flavor_text = data['FlavorText'] + ret.collection_display_start_date = data['CollectionDisplayStartDate'] + ret.collection_empty_frame_display_flag = data['CollectionEmptyFrameDisplayFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.equipment_id) \ + + encode_byte(self.equipment_type) \ + + encode_short(self.weapon_type_id) \ + + encode_str(self.name) \ + + encode_byte(self.rarity) \ + + encode_str(self.prefab) \ + + encode_int(self.power) \ + + encode_int(self.strength_increment) \ + + encode_short(self.skill_condition) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) \ + + encode_int(self.sale_price) \ + + encode_int(self.composition_exp) \ + + encode_int(self.awakening_exp) \ + + encode_str(self.flavor_text) \ + + encode_date_str(self.collection_display_start_date) \ + + encode_byte(self.collection_empty_frame_display_flag) + +class EquipmentLevelData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.level = decode_short(data, off) + off += SHORT_OFF + + self.require_exp = decode_int(data, off) + off += INT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EquipmentLevelData": + ret = cls(b"\x00" * 99, 0) + ret.level = data['EquipmentLevelId'] + ret.require_exp = data['RequireExp'] + ret.total_exp = data['TotalExp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.level) \ + + encode_int(self.require_exp) \ + + encode_int(self.total_exp) + +class WeaponTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.weapon_type_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.action_behaviour, new_off = decode_str(data, off) + off += new_off + + self.sound_behaviour, new_off = decode_str(data, off) + off += new_off + + self.physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.main_equip = decode_short(data, off) + off += SHORT_OFF + + self.sub_equip1 = decode_short(data, off) + off += SHORT_OFF + + self.sub_equip2 = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "WeaponTypeData": + ret = cls(b"\x00" * 99, 0) + ret.weapon_type_id = data['WeaponTypeId'] + ret.name = data['Name'] + ret.action_behaviour = data['ActionBehaviour'] + ret.sound_behaviour = data['SoundBehaviour'] + ret.physics_attr = data['PhysicsAttr'] + ret.main_equip = data['MainEquip'] + ret.sub_equip1 = data['SubEquip1'] + ret.sub_equip2 = data['SubEquip2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.weapon_type_id) \ + + encode_str(self.name) \ + + encode_str(self.action_behaviour) \ + + encode_str(self.sound_behaviour) \ + + encode_short(self.physics_attr) \ + + encode_short(self.main_equip) \ + + encode_short(self.sub_equip1) \ + + encode_short(self.sub_equip2) + +class ItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.item_id = decode_int(data, off) + off += INT_OFF + + self.item_type = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.rarity = decode_byte(data, off) + off += BYTE_OFF + + self.value = decode_int(data, off) + off += INT_OFF + + self.property_id = decode_int(data, off) + off += INT_OFF + + self.property_value1_min, new_off = decode_str(data, off) + off += new_off + + self.property_value1_max, new_off = decode_str(data, off) + off += new_off + + self.property_value2_min, new_off = decode_str(data, off) + off += new_off + + self.property_value2_max, new_off = decode_str(data, off) + off += new_off + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.sale_price = decode_int(data, off) + off += INT_OFF + + self.item_icon, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ItemData": + ret = cls(b"\x00" * 99, 0) + ret.item_id = data['ItemId'] + ret.item_type = data['ItemTypeId'] + ret.name = data['Name'] + ret.rarity = data['Rarity'] + ret.value = data['Value'] + ret.property_id = data['PropertyId'] + ret.property_value1_min = data['PropertyValue1Min'] + ret.property_value1_max = data['PropertyValue1Max'] + ret.property_value2_min = data['PropertyValue2Min'] + ret.property_value2_max = data['PropertyValue2Max'] + ret.flavor_text = data['FlavorText'] + ret.sale_price = data['SalePrice'] + ret.item_icon = data['ItemIcon'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.item_id) \ + + encode_int(self.item_type) \ + + encode_str(self.name) \ + + encode_byte(self.rarity) \ + + encode_int(self.value) \ + + encode_int(self.property_id) \ + + encode_str(self.property_value1_min) \ + + encode_str(self.property_value1_max) \ + + encode_str(self.property_value2_min) \ + + encode_str(self.property_value2_max) \ + + encode_str(self.flavor_text) \ + + encode_int(self.sale_price) \ + + encode_str(self.item_icon) + +class ItemTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.item_type_id = decode_int(data, off) + off += INT_OFF + + self.item_type_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ItemTypeData": + ret = cls(b"\x00" * 99, 0) + ret.item_type_id = data['ItemTypeId'] + ret.item_type_name = data['ItemTypeName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.item_type_id) \ + + encode_str(self.item_type_name) + +class BuffItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.buff_item_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BuffItemData": + ret = cls(b"\x00" * 99, 0) + ret.buff_item_id = data['BuffItemId'] + ret.name = data['Name'] + ret.flavor_text = data['FlavorText'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.buff_item_id) \ + + encode_str(self.name) \ + + encode_str(self.flavor_text) + +class EnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.enemy_id = decode_short(data, off) + off += SHORT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EnemyData": + ret = cls(b"\x00" * 99, 0) + ret.enemy_id = data['EnemyId'] + ret.unit_id = data['UnitId'] + ret.name = data['Name'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.enemy_id) \ + + encode_int(self.unit_id) \ + + encode_str(self.name) + +class EnemySetData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.enemy_set_id = decode_int(data, off) + off += INT_OFF + + self.enemy_set_sub_id = decode_int(data, off) + off += INT_OFF + + self.enemy_id = decode_int(data, off) + off += INT_OFF + + self.enemy_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EnemySetData": + ret = cls(b"\x00" * 99, 0) + ret.enemy_set_id = data['EnemySetId'] + ret.enemy_set_sub_id = data['EnemySetSubId'] + ret.enemy_id = data['EnemyId'] + ret.enemy_num = data['EnemyNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.enemy_set_id) \ + + encode_int(self.enemy_set_sub_id) \ + + encode_int(self.enemy_id) \ + + encode_short(self.enemy_num) + +class EnemyKindData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self.enemy_category_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.icon_filepath, new_off = decode_str(data, off) + off += new_off + + self.weak_physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.weak_magic_attr = decode_short(data, off) + off += SHORT_OFF + + self.weak_text, new_off = decode_str(data, off) + off += new_off + + self.resist_text, new_off = decode_str(data, off) + off += new_off + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.collection_display_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.collection_empty_frame_display_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EnemyKindData": + ret = cls(b"\x00" * 99, 0) + ret.enemy_kind_id = data['EnemyKindId'] + ret.enemy_category_id = data['EnemyCategoryId'] + ret.name = data['Name'] + ret.icon_filepath = data['IconFilepath'] + ret.weak_physics_attr = data['WeakPhysicsAttr'] + ret.weak_magic_attr = data['WeakMagicAttr'] + ret.weak_text = data['WeakText'] + ret.resist_text = data['ResistText'] + ret.flavor_text = data['FlavorText'] + ret.collection_display_start_date = data['CollectionDisplayStartDate'] + ret.collection_empty_frame_display_flag = data['CollectionEmptyFrameDisplayFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.enemy_kind_id) \ + + encode_short(self.enemy_category_id) \ + + encode_str(self.name) \ + + encode_str(self.icon_filepath) \ + + encode_short(self.weak_physics_attr) \ + + encode_short(self.weak_magic_attr) \ + + encode_str(self.weak_text) \ + + encode_str(self.resist_text) \ + + encode_str(self.flavor_text) \ + + encode_date_str(self.collection_display_start_date) \ + + encode_byte(self.collection_empty_frame_display_flag) + +class EnemyCategoryData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.enemy_category_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.detail_text, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EnemyCategoryData": + ret = cls(b"\x00" * 99, 0) + ret.enemy_category_id = data['EnemyCategoryId'] + ret.name = data['Name'] + ret.detail_text = data['DetailText'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.enemy_category_id) \ + + encode_str(self.name) \ + + encode_str(self.detail_text) + +class UnitData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.prefab, new_off = decode_str(data, off) + off += new_off + + self.animator, new_off = decode_str(data, off) + off += new_off + + self.collision, new_off = decode_str(data, off) + off += new_off + + self.radius, new_off = decode_str(data, off) + off += new_off + + self.child_unit_id = decode_int(data, off) + off += INT_OFF + + self.stage_start_prefab, new_off = decode_str(data, off) + off += new_off + + self.stage_start_y, new_off = decode_str(data, off) + off += new_off + + self.stage_start_z, new_off = decode_str(data, off) + off += new_off + + self.stage_touch_y, new_off = decode_str(data, off) + off += new_off + + self.stage_touch_z, new_off = decode_str(data, off) + off += new_off + + self.playable = decode_byte(data, off) + off += BYTE_OFF + + self.monster = decode_byte(data, off) + off += BYTE_OFF + + self.gimmick = decode_byte(data, off) + off += BYTE_OFF + + self.anger = decode_byte(data, off) + off += BYTE_OFF + + self.commander = decode_byte(data, off) + off += BYTE_OFF + + self.isolated = decode_byte(data, off) + off += BYTE_OFF + + self.base_behaviour, new_off = decode_str(data, off) + off += new_off + + self.sound_behaviour, new_off = decode_str(data, off) + off += new_off + + self.weapon_type = decode_int(data, off) + off += INT_OFF + + self.poison_time_coef, new_off = decode_str(data, off) + off += new_off + + self.paralysis_time_coef, new_off = decode_str(data, off) + off += new_off + + self.sealed_time_coef, new_off = decode_str(data, off) + off += new_off + + self.question_time_coef, new_off = decode_str(data, off) + off += new_off + + self.blue_rose_time_coef, new_off = decode_str(data, off) + off += new_off + + self.charm_time_coef, new_off = decode_str(data, off) + off += new_off + + self.burning_time_coef, new_off = decode_str(data, off) + off += new_off + + self.invalid_quake = decode_byte(data, off) + off += BYTE_OFF + + self.guard_coef, new_off = decode_str(data, off) + off += new_off + + self.just_guard_time, new_off = decode_str(data, off) + off += new_off + + self.flip_hp = decode_short(data, off) + off += SHORT_OFF + + self.down_hp = decode_short(data, off) + off += SHORT_OFF + + self.flip_recover_time, new_off = decode_str(data, off) + off += new_off + + self.down_recover_time, new_off = decode_str(data, off) + off += new_off + + self.down_time, new_off = decode_str(data, off) + off += new_off + + self.loss_damage = decode_short(data, off) + off += SHORT_OFF + + self.loss_time, new_off = decode_str(data, off) + off += new_off + + self.move_speed, new_off = decode_str(data, off) + off += new_off + + self.move_speed_angry, new_off = decode_str(data, off) + off += new_off + + self.rot_speed, new_off = decode_str(data, off) + off += new_off + + self.rot_speed_angry, new_off = decode_str(data, off) + off += new_off + + self.range_min, new_off = decode_str(data, off) + off += new_off + + self.range_max, new_off = decode_str(data, off) + off += new_off + + self.range_min_angry, new_off = decode_str(data, off) + off += new_off + + self.range_max_angry, new_off = decode_str(data, off) + off += new_off + + self.back_step_speed, new_off = decode_str(data, off) + off += new_off + + self.back_step_speed_angry, new_off = decode_str(data, off) + off += new_off + + self.back_step_cool_time, new_off = decode_str(data, off) + off += new_off + + self.monster_type = decode_short(data, off) + off += SHORT_OFF + + self.monster_type_angry = decode_short(data, off) + off += SHORT_OFF + + self.vs_boss_type = decode_short(data, off) + off += SHORT_OFF + + self.boss_camera_height, new_off = decode_str(data, off) + off += new_off + + self.boss_camera_near_distance, new_off = decode_str(data, off) + off += new_off + + self.boss_camera_far_distance, new_off = decode_str(data, off) + off += new_off + + self.boss_camera_near_range, new_off = decode_str(data, off) + off += new_off + + self.boss_camera_far_range, new_off = decode_str(data, off) + off += new_off + + self.search_range, new_off = decode_str(data, off) + off += new_off + + self.search_angle, new_off = decode_str(data, off) + off += new_off + + self.lost_range, new_off = decode_str(data, off) + off += new_off + + self.home_range, new_off = decode_str(data, off) + off += new_off + + self.roar_id, new_off = decode_str(data, off) + off += new_off + + self.auto_attack_range, new_off = decode_str(data, off) + off += new_off + + self.auto_attack_interval, new_off = decode_str(data, off) + off += new_off + + self.find_priority = decode_short(data, off) + off += SHORT_OFF + + self.find_range_min = decode_short(data, off) + off += SHORT_OFF + + self.find_range_max = decode_short(data, off) + off += SHORT_OFF + + self.leave_range = decode_short(data, off) + off += SHORT_OFF + + self.show_arrow = decode_byte(data, off) + off += BYTE_OFF + + self.find_id, new_off = decode_str(data, off) + off += new_off + + self.leave_id, new_off = decode_str(data, off) + off += new_off + + self.defeat_id, new_off = decode_str(data, off) + off += new_off + + self.warfare_id, new_off = decode_str(data, off) + off += new_off + + self.angry_id, new_off = decode_str(data, off) + off += new_off + + self.bad_state_bone, new_off = decode_str(data, off) + off += new_off + + self.bad_state_offset_x, new_off = decode_str(data, off) + off += new_off + + self.bad_state_offset_y, new_off = decode_str(data, off) + off += new_off + + self.bad_state_offset_z, new_off = decode_str(data, off) + off += new_off + + self.drop_offset_y, new_off = decode_str(data, off) + off += new_off + + self.col_drop_percentage, new_off = decode_str(data, off) + off += new_off + + self.col_drop_amount = decode_short(data, off) + off += SHORT_OFF + + self.vp_drop_percentage, new_off = decode_str(data, off) + off += new_off + + self.vp_drop_amount = decode_short(data, off) + off += SHORT_OFF + + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_league_point = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "UnitData": + ret = cls(b"\x00" * 99, 0) + ret.unit_id = data['UnitId'] + ret.name = data['Name'] + ret.prefab = data['Prefab'] + ret.animator = data['Animator'] + ret.collision = data['Collision'] + ret.radius = data['Radius'] + ret.child_unit_id = data['ChildUnitId'] + ret.stage_start_prefab = data['StageStartPrefab'] + ret.stage_start_y = data['StageStartY'] + ret.stage_start_z = data['StageStartZ'] + ret.stage_touch_y = data['StageTouchY'] + ret.stage_touch_z = data['StageTouchZ'] + ret.playable = data['Playable'] + ret.monster = data['Monster'] + ret.gimmick = data['Gimmick'] + ret.anger = data['Anger'] + ret.commander = data['Commander'] + ret.isolated = data['Isolated'] + ret.base_behaviour = data['BaseBehaviour'] + ret.sound_behaviour = data['SoundBehaviour'] + ret.weapon_type = data['WeaponType'] + ret.poison_time_coef = data['PoisonTimeCoef'] + ret.paralysis_time_coef = data['ParalysisTimeCoef'] + ret.sealed_time_coef = data['SealedTimeCoef'] + ret.question_time_coef = data['QuestionTimeCoef'] + ret.blue_rose_time_coef = data['BlueRoseTimeCoef'] + ret.charm_time_coef = data['CharmTimeCoef'] + ret.burning_time_coef = data['BurningTimeCoef'] + ret.invalid_quake = data['InvalidQuake'] + ret.guard_coef = data['GuardCoef'] + ret.just_guard_time = data['JustGuardTime'] + ret.flip_hp = data['FlipHp'] + ret.down_hp = data['DownHp'] + ret.flip_recover_time = data['FlipRecoverTime'] + ret.down_recover_time = data['DownRecoverTime'] + ret.down_time = data['DownTime'] + ret.loss_damage = data['LossDamage'] + ret.loss_time = data['LossTime'] + ret.move_speed = data['MoveSpeed'] + ret.move_speed_angry = data['MoveSpeedAngry'] + ret.rot_speed = data['RotSpeed'] + ret.rot_speed_angry = data['RotSpeedAngry'] + ret.range_min = data['RangeMin'] + ret.range_max = data['RangeMax'] + ret.range_min_angry = data['RangeMinAngry'] + ret.range_max_angry = data['RangeMaxAngry'] + ret.back_step_speed = data['BackStepSpeed'] + ret.back_step_speed_angry = data['BackStepSpeedAngry'] + ret.back_step_cool_time = data['BackStepCoolTime'] + ret.monster_type = data['MonsterType'] + ret.monster_type_angry = data['MonsterTypeAngry'] + ret.vs_boss_type = data['VsBossType'] + ret.boss_camera_height = data['BossCameraHeight'] + ret.boss_camera_near_distance = data['BossCameraNearDistance'] + ret.boss_camera_far_distance = data['BossCameraFarDistance'] + ret.boss_camera_near_range = data['BossCameraNearRange'] + ret.boss_camera_far_range = data['BossCameraFarRange'] + ret.search_range = data['SearchRange'] + ret.search_angle = data['SearchAngle'] + ret.lost_range = data['LostRange'] + ret.home_range = data['HomeRange'] + ret.roar_id = data['RoarId'] + ret.auto_attack_range = data['AutoAttackRange'] + ret.auto_attack_interval = data['AutoAttackInterval'] + ret.find_priority = data['FindPriority'] + ret.find_range_min = data['FindRangeMin'] + ret.find_range_max = data['FindRangeMax'] + ret.leave_range = data['LeaveRange'] + ret.show_arrow = data['ShowArrow'] + ret.find_id = data['FindId'] + ret.leave_id = data['LeaveId'] + ret.defeat_id = data['DefeatId'] + ret.warfare_id = data['WarfareId'] + ret.angry_id = data['AngryId'] + ret.bad_state_bone = data['BadStateBone'] + ret.bad_state_offset_x = data['BadStateOffsetX'] + ret.bad_state_offset_y = data['BadStateOffsetY'] + ret.bad_state_offset_z = data['BadStateOffsetZ'] + ret.drop_offset_y = data['DropOffsetY'] + ret.col_drop_percentage = data['ColDropPercentage'] + ret.col_drop_amount = data['ColDropAmount'] + ret.vp_drop_percentage = data['VpDropPercentage'] + ret.vp_drop_amount = data['VpDropAmount'] + ret.enemy_kind_id = data['EnemyKindId'] + ret.defrag_match_league_point = data['DefragMatchLeaguePoint'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unit_id) \ + + encode_str(self.name) \ + + encode_str(self.prefab) \ + + encode_str(self.animator) \ + + encode_str(self.collision) \ + + encode_str(self.radius) \ + + encode_int(self.child_unit_id) \ + + encode_str(self.stage_start_prefab) \ + + encode_str(self.stage_start_y) \ + + encode_str(self.stage_start_z) \ + + encode_str(self.stage_touch_y) \ + + encode_str(self.stage_touch_z) \ + + encode_byte(self.playable) \ + + encode_byte(self.monster) \ + + encode_byte(self.gimmick) \ + + encode_byte(self.anger) \ + + encode_byte(self.commander) \ + + encode_byte(self.isolated) \ + + encode_str(self.base_behaviour) \ + + encode_str(self.sound_behaviour) \ + + encode_int(self.weapon_type) \ + + encode_str(self.poison_time_coef) \ + + encode_str(self.paralysis_time_coef) \ + + encode_str(self.sealed_time_coef) \ + + encode_str(self.question_time_coef) \ + + encode_str(self.blue_rose_time_coef) \ + + encode_str(self.charm_time_coef) \ + + encode_str(self.burning_time_coef) \ + + encode_byte(self.invalid_quake) \ + + encode_str(self.guard_coef) \ + + encode_str(self.just_guard_time) \ + + encode_short(self.flip_hp) \ + + encode_short(self.down_hp) \ + + encode_str(self.flip_recover_time) \ + + encode_str(self.down_recover_time) \ + + encode_str(self.down_time) \ + + encode_short(self.loss_damage) \ + + encode_str(self.loss_time) \ + + encode_str(self.move_speed) \ + + encode_str(self.move_speed_angry) \ + + encode_str(self.rot_speed) \ + + encode_str(self.rot_speed_angry) \ + + encode_str(self.range_min) \ + + encode_str(self.range_max) \ + + encode_str(self.range_min_angry) \ + + encode_str(self.range_max_angry) \ + + encode_str(self.back_step_speed) \ + + encode_str(self.back_step_speed_angry) \ + + encode_str(self.back_step_cool_time) \ + + encode_short(self.monster_type) \ + + encode_short(self.monster_type_angry) \ + + encode_short(self.vs_boss_type) \ + + encode_str(self.boss_camera_height) \ + + encode_str(self.boss_camera_near_distance) \ + + encode_str(self.boss_camera_far_distance) \ + + encode_str(self.boss_camera_near_range) \ + + encode_str(self.boss_camera_far_range) \ + + encode_str(self.search_range) \ + + encode_str(self.search_angle) \ + + encode_str(self.lost_range) \ + + encode_str(self.home_range) \ + + encode_str(self.roar_id) \ + + encode_str(self.auto_attack_range) \ + + encode_str(self.auto_attack_interval) \ + + encode_short(self.find_priority) \ + + encode_short(self.find_range_min) \ + + encode_short(self.find_range_max) \ + + encode_short(self.leave_range) \ + + encode_byte(self.show_arrow) \ + + encode_str(self.find_id) \ + + encode_str(self.leave_id) \ + + encode_str(self.defeat_id) \ + + encode_str(self.warfare_id) \ + + encode_str(self.angry_id) \ + + encode_str(self.bad_state_bone) \ + + encode_str(self.bad_state_offset_x) \ + + encode_str(self.bad_state_offset_y) \ + + encode_str(self.bad_state_offset_z) \ + + encode_str(self.drop_offset_y) \ + + encode_str(self.col_drop_percentage) \ + + encode_short(self.col_drop_amount) \ + + encode_str(self.vp_drop_percentage) \ + + encode_short(self.vp_drop_amount) \ + + encode_int(self.enemy_kind_id) \ + + encode_int(self.defrag_match_league_point) + +class UnitGimmickData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unit_gimmick_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.gimmick_type = decode_int(data, off) + off += INT_OFF + + self.range, new_off = decode_str(data, off) + off += new_off + + self.crystal_grade = decode_int(data, off) + off += INT_OFF + + self.col = decode_int(data, off) + off += INT_OFF + + self.skill_exp = decode_int(data, off) + off += INT_OFF + + self.heal_rate, new_off = decode_str(data, off) + off += new_off + + self.gimmick_attack_id = decode_short(data, off) + off += SHORT_OFF + + self.interval, new_off = decode_str(data, off) + off += new_off + + self.summon_count = decode_int(data, off) + off += INT_OFF + + self.effect, new_off = decode_str(data, off) + off += new_off + + self.effect_bone, new_off = decode_str(data, off) + off += new_off + + self.effect_x, new_off = decode_str(data, off) + off += new_off + + self.effect_y, new_off = decode_str(data, off) + off += new_off + + self.effect_z, new_off = decode_str(data, off) + off += new_off + + self.se, new_off = decode_str(data, off) + off += new_off + + self.ground_se, new_off = decode_str(data, off) + off += new_off + + self.sub_state_idle, new_off = decode_str(data, off) + off += new_off + + self.sub_state_reaction, new_off = decode_str(data, off) + off += new_off + + self.sub_state_active, new_off = decode_str(data, off) + off += new_off + + self.sub_state_break, new_off = decode_str(data, off) + off += new_off + + self.time_sub_state_break, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "UnitGimmickData": + ret = cls(b"\x00" * 99, 0) + ret.unit_gimmick_id = data['UnitGimmickId'] + ret.name = data['Name'] + ret.gimmick_type = data['GimmickType'] + ret.range = data['Range'] + ret.crystal_grade = data['CrystalGrade'] + ret.col = data['Col'] + ret.skill_exp = data['SkillExp'] + ret.heal_rate = data['HealRate'] + ret.gimmick_attack_id = data['GimmickAttackId'] + ret.interval = data['Interval'] + ret.summon_count = data['SummonCount'] + ret.effect = data['Effect'] + ret.effect_bone = data['EffectBone'] + ret.effect_x = data['EffectX'] + ret.effect_y = data['EffectY'] + ret.effect_z = data['EffectZ'] + ret.se = data['Se'] + ret.ground_se = data['GroundSe'] + ret.sub_state_idle = data['SubStateIdle'] + ret.sub_state_reaction = data['SubStateReaction'] + ret.sub_state_active = data['SubStateActive'] + ret.sub_state_break = data['SubStateBreak'] + ret.time_sub_state_break = data['TimeSubStateBreak'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unit_gimmick_id) \ + + encode_str(self.name) \ + + encode_int(self.gimmick_type) \ + + encode_str(self.range) \ + + encode_int(self.crystal_grade) \ + + encode_int(self.col) \ + + encode_int(self.skill_exp) \ + + encode_str(self.heal_rate) \ + + encode_short(self.gimmick_attack_id) \ + + encode_str(self.interval) \ + + encode_int(self.summon_count) \ + + encode_str(self.effect) \ + + encode_str(self.effect_bone) \ + + encode_str(self.effect_x) \ + + encode_str(self.effect_y) \ + + encode_str(self.effect_z) \ + + encode_str(self.se) \ + + encode_str(self.ground_se) \ + + encode_str(self.sub_state_idle) \ + + encode_str(self.sub_state_reaction) \ + + encode_str(self.sub_state_active) \ + + encode_str(self.sub_state_break) \ + + encode_str(self.time_sub_state_break) + +class UnitCollisionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unit_collision_id = decode_int(data, off) + off += INT_OFF + + self.keyword, new_off = decode_str(data, off) + off += new_off + + self.name, new_off = decode_str(data, off) + off += new_off + + self.bone, new_off = decode_str(data, off) + off += new_off + + self.usually = decode_byte(data, off) + off += BYTE_OFF + + self.angry = decode_byte(data, off) + off += BYTE_OFF + + self.type_index = decode_short(data, off) + off += SHORT_OFF + + self.begin_x, new_off = decode_str(data, off) + off += new_off + + self.begin_y, new_off = decode_str(data, off) + off += new_off + + self.begin_z, new_off = decode_str(data, off) + off += new_off + + self.end_x, new_off = decode_str(data, off) + off += new_off + + self.end_y, new_off = decode_str(data, off) + off += new_off + + self.end_z, new_off = decode_str(data, off) + off += new_off + + self.radius, new_off = decode_str(data, off) + off += new_off + + self.slash_coef, new_off = decode_str(data, off) + off += new_off + + self.strike_coef, new_off = decode_str(data, off) + off += new_off + + self.thrust_coef, new_off = decode_str(data, off) + off += new_off + + self.fire_coef, new_off = decode_str(data, off) + off += new_off + + self.water_coef, new_off = decode_str(data, off) + off += new_off + + self.air_coef, new_off = decode_str(data, off) + off += new_off + + self.earth_coef, new_off = decode_str(data, off) + off += new_off + + self.holy_coef, new_off = decode_str(data, off) + off += new_off + + self.dark_coef, new_off = decode_str(data, off) + off += new_off + + self.poison_coef, new_off = decode_str(data, off) + off += new_off + + self.paralysis_coef, new_off = decode_str(data, off) + off += new_off + + self.sealed_coef, new_off = decode_str(data, off) + off += new_off + + self.question_coef, new_off = decode_str(data, off) + off += new_off + + self.blue_rose_coef, new_off = decode_str(data, off) + off += new_off + + self.charm_coef, new_off = decode_str(data, off) + off += new_off + + self.burning_coef, new_off = decode_str(data, off) + off += new_off + + self.flip_coef, new_off = decode_str(data, off) + off += new_off + + self.down_coef, new_off = decode_str(data, off) + off += new_off + + self.knock_back_coef, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "UnitCollisionData": + ret = cls(b"\x00" * 99, 0) + ret.unit_collision_id = data['UnitCollisionId'] + ret.keyword = data['Keyword'] + ret.name = data['Name'] + ret.bone = data['Bone'] + ret.usually = data['Usually'] + ret.angry = data['Angry'] + ret.type_index = data['TypeIndex'] + ret.begin_x = data['BeginX'] + ret.begin_y = data['BeginY'] + ret.begin_z = data['BeginZ'] + ret.end_x = data['EndX'] + ret.end_y = data['EndY'] + ret.end_z = data['EndZ'] + ret.radius = data['Radius'] + ret.slash_coef = data['SlashCoef'] + ret.strike_coef = data['StrikeCoef'] + ret.thrust_coef = data['ThrustCoef'] + ret.fire_coef = data['FireCoef'] + ret.water_coef = data['WaterCoef'] + ret.air_coef = data['AirCoef'] + ret.earth_coef = data['EarthCoef'] + ret.holy_coef = data['HolyCoef'] + ret.dark_coef = data['DarkCoef'] + ret.poison_coef = data['PoisonCoef'] + ret.paralysis_coef = data['ParalysisCoef'] + ret.sealed_coef = data['SealedCoef'] + ret.question_coef = data['QuestionCoef'] + ret.blue_rose_coef = data['BlueRoseCoef'] + ret.charm_coef = data['CharmCoef'] + ret.burning_coef = data['BurningCoef'] + ret.flip_coef = data['FlipCoef'] + ret.down_coef = data['DownCoef'] + ret.knock_back_coef = data['KnockBackCoef'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unit_collision_id) \ + + encode_str(self.keyword) \ + + encode_str(self.name) \ + + encode_str(self.bone) \ + + encode_byte(self.usually) \ + + encode_byte(self.angry) \ + + encode_short(self.type_index) \ + + encode_str(self.begin_x) \ + + encode_str(self.begin_y) \ + + encode_str(self.begin_z) \ + + encode_str(self.end_x) \ + + encode_str(self.end_y) \ + + encode_str(self.end_z) \ + + encode_str(self.radius) \ + + encode_str(self.slash_coef) \ + + encode_str(self.strike_coef) \ + + encode_str(self.thrust_coef) \ + + encode_str(self.fire_coef) \ + + encode_str(self.water_coef) \ + + encode_str(self.air_coef) \ + + encode_str(self.earth_coef) \ + + encode_str(self.holy_coef) \ + + encode_str(self.dark_coef) \ + + encode_str(self.poison_coef) \ + + encode_str(self.paralysis_coef) \ + + encode_str(self.sealed_coef) \ + + encode_str(self.question_coef) \ + + encode_str(self.blue_rose_coef) \ + + encode_str(self.charm_coef) \ + + encode_str(self.burning_coef) \ + + encode_str(self.flip_coef) \ + + encode_str(self.down_coef) \ + + encode_str(self.knock_back_coef) + +class UnitPowerData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.unit_power_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.lv = decode_int(data, off) + off += INT_OFF + + self.param_hp = decode_int(data, off) + off += INT_OFF + + self.param_str = decode_int(data, off) + off += INT_OFF + + self.param_vit = decode_int(data, off) + off += INT_OFF + + self.param_int = decode_int(data, off) + off += INT_OFF + + self.exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "UnitPowerData": + ret = cls(b"\x00" * 99, 0) + ret.unit_power_id = data['UnitPowerId'] + ret.unit_id = data['UnitId'] + ret.lv = data['Lv'] + ret.param_hp = data['ParamHp'] + ret.param_str = data['ParamStr'] + ret.param_vit = data['ParamVit'] + ret.param_int = data['ParamInt'] + ret.exp = data['Exp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.unit_power_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.lv) \ + + encode_int(self.param_hp) \ + + encode_int(self.param_str) \ + + encode_int(self.param_vit) \ + + encode_int(self.param_int) \ + + encode_int(self.exp) + +class GimmickAttackData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gimmick_attack_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.hit_enemy = decode_byte(data, off) + off += BYTE_OFF + + self.hit_friend = decode_byte(data, off) + off += BYTE_OFF + + self.attack_coef, new_off = decode_str(data, off) + off += new_off + + self.heal_coef, new_off = decode_str(data, off) + off += new_off + + self.flip_damage = decode_short(data, off) + off += SHORT_OFF + + self.down_damage = decode_short(data, off) + off += SHORT_OFF + + self.poison_incidence, new_off = decode_str(data, off) + off += new_off + + self.paralysis_incidence, new_off = decode_str(data, off) + off += new_off + + self.sealed_incidence, new_off = decode_str(data, off) + off += new_off + + self.question_incidence, new_off = decode_str(data, off) + off += new_off + + self.blue_rose_incidence, new_off = decode_str(data, off) + off += new_off + + self.charm_incidence, new_off = decode_str(data, off) + off += new_off + + self.burning_incidence, new_off = decode_str(data, off) + off += new_off + + self.knock_back, new_off = decode_str(data, off) + off += new_off + + self.knock_back_g, new_off = decode_str(data, off) + off += new_off + + self.forward_knock_back = decode_byte(data, off) + off += BYTE_OFF + + self.break_interval, new_off = decode_str(data, off) + off += new_off + + self.attack_type = decode_short(data, off) + off += SHORT_OFF + + self.physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.magic_attr = decode_short(data, off) + off += SHORT_OFF + + self.hit_se, new_off = decode_str(data, off) + off += new_off + + self.weak_se, new_off = decode_str(data, off) + off += new_off + + self.resist_se, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GimmickAttackData": + ret = cls(b"\x00" * 99, 0) + ret.gimmick_attack_id = data['GimmickAttackId'] + ret.name = data['Name'] + ret.hit_enemy = data['HitEnemy'] + ret.hit_friend = data['HitFriend'] + ret.attack_coef = data['AttackCoef'] + ret.heal_coef = data['HealCoef'] + ret.flip_damage = data['FlipDamage'] + ret.down_damage = data['DownDamage'] + ret.poison_incidence = data['PoisonIncidence'] + ret.paralysis_incidence = data['ParalysisIncidence'] + ret.sealed_incidence = data['SealedIncidence'] + ret.question_incidence = data['QuestionIncidence'] + ret.blue_rose_incidence = data['BlueRoseIncidence'] + ret.charm_incidence = data['CharmIncidence'] + ret.burning_incidence = data['BurningIncidence'] + ret.knock_back = data['KnockBack'] + ret.knock_back_g = data['KnockBackG'] + ret.forward_knock_back = data['ForwardKnockBack'] + ret.break_interval = data['BreakInterval'] + ret.attack_type = data['AttackType'] + ret.physics_attr = data['PhysicsAttr'] + ret.magic_attr = data['MagicAttr'] + ret.hit_se = data['HitSe'] + ret.weak_se = data['WeakSe'] + ret.resist_se = data['ResistSe'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.gimmick_attack_id) \ + + encode_str(self.name) \ + + encode_byte(self.hit_enemy) \ + + encode_byte(self.hit_friend) \ + + encode_str(self.attack_coef) \ + + encode_str(self.heal_coef) \ + + encode_short(self.flip_damage) \ + + encode_short(self.down_damage) \ + + encode_str(self.poison_incidence) \ + + encode_str(self.paralysis_incidence) \ + + encode_str(self.sealed_incidence) \ + + encode_str(self.question_incidence) \ + + encode_str(self.blue_rose_incidence) \ + + encode_str(self.charm_incidence) \ + + encode_str(self.burning_incidence) \ + + encode_str(self.knock_back) \ + + encode_str(self.knock_back_g) \ + + encode_byte(self.forward_knock_back) \ + + encode_str(self.break_interval) \ + + encode_short(self.attack_type) \ + + encode_short(self.physics_attr) \ + + encode_short(self.magic_attr) \ + + encode_str(self.hit_se) \ + + encode_str(self.weak_se) \ + + encode_str(self.resist_se) + +class CharaAttackData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chara_attack_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.attack_coef, new_off = decode_str(data, off) + off += new_off + + self.flip_damage = decode_short(data, off) + off += SHORT_OFF + + self.down_damage = decode_short(data, off) + off += SHORT_OFF + + self.poison_incidence, new_off = decode_str(data, off) + off += new_off + + self.paralysis_incidence, new_off = decode_str(data, off) + off += new_off + + self.sealed_incidence, new_off = decode_str(data, off) + off += new_off + + self.question_incidence, new_off = decode_str(data, off) + off += new_off + + self.blue_rose_incidence, new_off = decode_str(data, off) + off += new_off + + self.charm_incidence, new_off = decode_str(data, off) + off += new_off + + self.knock_back, new_off = decode_str(data, off) + off += new_off + + self.forward_knock_back = decode_byte(data, off) + off += BYTE_OFF + + self.break_interval, new_off = decode_str(data, off) + off += new_off + + self.weapon_slot = decode_short(data, off) + off += SHORT_OFF + + self.attack_type = decode_short(data, off) + off += SHORT_OFF + + self.physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.magic_attr = decode_short(data, off) + off += SHORT_OFF + + self.spell_blast = decode_byte(data, off) + off += BYTE_OFF + + self.hit_se, new_off = decode_str(data, off) + off += new_off + + self.weak_se, new_off = decode_str(data, off) + off += new_off + + self.resist_se, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CharaAttackData": + ret = cls(b"\x00" * 99, 0) + ret.chara_attack_id = data['CharaAttackId'] + ret.name = data['Name'] + ret.attack_coef = data['AttackCoef'] + ret.flip_damage = data['FlipDamage'] + ret.down_damage = data['DownDamage'] + ret.poison_incidence = data['PoisonIncidence'] + ret.paralysis_incidence = data['ParalysisIncidence'] + ret.sealed_incidence = data['SealedIncidence'] + ret.question_incidence = data['QuestionIncidence'] + ret.blue_rose_incidence = data['BlueRoseIncidence'] + ret.charm_incidence = data['CharmIncidence'] + ret.knock_back = data['KnockBack'] + ret.forward_knock_back = data['ForwardKnockBack'] + ret.break_interval = data['BreakInterval'] + ret.weapon_slot = data['WeaponSlot'] + ret.attack_type = data['AttackType'] + ret.physics_attr = data['PhysicsAttr'] + ret.magic_attr = data['MagicAttr'] + ret.spell_blast = data['SpellBlast'] + ret.hit_se = data['HitSe'] + ret.weak_se = data['WeakSe'] + ret.resist_se = data['ResistSe'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.chara_attack_id) \ + + encode_str(self.name) \ + + encode_str(self.attack_coef) \ + + encode_short(self.flip_damage) \ + + encode_short(self.down_damage) \ + + encode_str(self.poison_incidence) \ + + encode_str(self.paralysis_incidence) \ + + encode_str(self.sealed_incidence) \ + + encode_str(self.question_incidence) \ + + encode_str(self.blue_rose_incidence) \ + + encode_str(self.charm_incidence) \ + + encode_str(self.knock_back) \ + + encode_byte(self.forward_knock_back) \ + + encode_str(self.break_interval) \ + + encode_short(self.weapon_slot) \ + + encode_short(self.attack_type) \ + + encode_short(self.physics_attr) \ + + encode_short(self.magic_attr) \ + + encode_byte(self.spell_blast) \ + + encode_str(self.hit_se) \ + + encode_str(self.weak_se) \ + + encode_str(self.resist_se) + +class BossAttackData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.boss_attack_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.attack_coef, new_off = decode_str(data, off) + off += new_off + + self.flip_damage = decode_short(data, off) + off += SHORT_OFF + + self.down_damage = decode_short(data, off) + off += SHORT_OFF + + self.poison_incidence, new_off = decode_str(data, off) + off += new_off + + self.sealed_incidence, new_off = decode_str(data, off) + off += new_off + + self.question_incidence, new_off = decode_str(data, off) + off += new_off + + self.reduce_max_hp_incidence, new_off = decode_str(data, off) + off += new_off + + self.burning_incidence, new_off = decode_str(data, off) + off += new_off + + self.quake = decode_byte(data, off) + off += BYTE_OFF + + self.can_parry = decode_byte(data, off) + off += BYTE_OFF + + self.knock_back, new_off = decode_str(data, off) + off += new_off + + self.knock_back_g, new_off = decode_str(data, off) + off += new_off + + self.forward_knock_back = decode_byte(data, off) + off += BYTE_OFF + + self.attack_type = decode_short(data, off) + off += SHORT_OFF + + self.physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.magic_attr = decode_short(data, off) + off += SHORT_OFF + + self.spell_blast = decode_byte(data, off) + off += BYTE_OFF + + self.hit_se, new_off = decode_str(data, off) + off += new_off + + self.weak_se, new_off = decode_str(data, off) + off += new_off + + self.resist_se, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BossAttackData": + ret = cls(b"\x00" * 99, 0) + ret.boss_attack_id = data['BossAttackId'] + ret.name = data['Name'] + ret.attack_coef = data['AttackCoef'] + ret.flip_damage = data['FlipDamage'] + ret.down_damage = data['DownDamage'] + ret.poison_incidence = data['PoisonIncidence'] + ret.sealed_incidence = data['SealedIncidence'] + ret.question_incidence = data['QuestionIncidence'] + ret.reduce_max_hp_incidence = data['ReduceMaxHpIncidence'] + ret.burning_incidence = data['BurningIncidence'] + ret.quake = data['Quake'] + ret.can_parry = data['CanParry'] + ret.knock_back = data['KnockBack'] + ret.knock_back_g = data['KnockBackG'] + ret.forward_knock_back = data['ForwardKnockBack'] + ret.attack_type = data['AttackType'] + ret.physics_attr = data['PhysicsAttr'] + ret.magic_attr = data['MagicAttr'] + ret.spell_blast = data['SpellBlast'] + ret.hit_se = data['HitSe'] + ret.weak_se = data['WeakSe'] + ret.resist_se = data['ResistSe'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.boss_attack_id) \ + + encode_str(self.name) \ + + encode_str(self.attack_coef) \ + + encode_short(self.flip_damage) \ + + encode_short(self.down_damage) \ + + encode_str(self.poison_incidence) \ + + encode_str(self.sealed_incidence) \ + + encode_str(self.question_incidence) \ + + encode_str(self.reduce_max_hp_incidence) \ + + encode_str(self.burning_incidence) \ + + encode_byte(self.quake) \ + + encode_byte(self.can_parry) \ + + encode_str(self.knock_back) \ + + encode_str(self.knock_back_g) \ + + encode_byte(self.forward_knock_back) \ + + encode_short(self.attack_type) \ + + encode_short(self.physics_attr) \ + + encode_short(self.magic_attr) \ + + encode_byte(self.spell_blast) \ + + encode_str(self.hit_se) \ + + encode_str(self.weak_se) \ + + encode_str(self.resist_se) + +class MonsterAttackData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.monster_attack_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.attack_coef, new_off = decode_str(data, off) + off += new_off + + self.flip_damage = decode_short(data, off) + off += SHORT_OFF + + self.down_damage = decode_short(data, off) + off += SHORT_OFF + + self.poison_incidence, new_off = decode_str(data, off) + off += new_off + + self.sealed_incidence, new_off = decode_str(data, off) + off += new_off + + self.question_incidence, new_off = decode_str(data, off) + off += new_off + + self.reduce_max_hp_incidence, new_off = decode_str(data, off) + off += new_off + + self.burning_incidence, new_off = decode_str(data, off) + off += new_off + + self.quake = decode_byte(data, off) + off += BYTE_OFF + + self.can_parry = decode_byte(data, off) + off += BYTE_OFF + + self.knock_back, new_off = decode_str(data, off) + off += new_off + + self.knock_back_g, new_off = decode_str(data, off) + off += new_off + + self.forward_knock_back = decode_byte(data, off) + off += BYTE_OFF + + self.attack_type = decode_short(data, off) + off += SHORT_OFF + + self.physics_attr = decode_short(data, off) + off += SHORT_OFF + + self.magic_attr = decode_short(data, off) + off += SHORT_OFF + + self.spell_blast = decode_byte(data, off) + off += BYTE_OFF + + self.hit_se, new_off = decode_str(data, off) + off += new_off + + self.weak_se, new_off = decode_str(data, off) + off += new_off + + self.resist_se, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MonsterAttackData": + ret = cls(b"\x00" * 99, 0) + ret.monster_attack_id = data['MonsterAttackId'] + ret.name = data['Name'] + ret.attack_coef = data['AttackCoef'] + ret.flip_damage = data['FlipDamage'] + ret.down_damage = data['DownDamage'] + ret.poison_incidence = data['PoisonIncidence'] + ret.sealed_incidence = data['SealedIncidence'] + ret.question_incidence = data['QuestionIncidence'] + ret.reduce_max_hp_incidence = data['ReduceMaxHpIncidence'] + ret.burning_incidence = data['BurningIncidence'] + ret.quake = data['Quake'] + ret.can_parry = data['CanParry'] + ret.knock_back = data['KnockBack'] + ret.knock_back_g = data['KnockBackG'] + ret.forward_knock_back = data['ForwardKnockBack'] + ret.attack_type = data['AttackType'] + ret.physics_attr = data['PhysicsAttr'] + ret.magic_attr = data['MagicAttr'] + ret.spell_blast = data['SpellBlast'] + ret.hit_se = data['HitSe'] + ret.weak_se = data['WeakSe'] + ret.resist_se = data['ResistSe'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.monster_attack_id) \ + + encode_str(self.name) \ + + encode_str(self.attack_coef) \ + + encode_short(self.flip_damage) \ + + encode_short(self.down_damage) \ + + encode_str(self.poison_incidence) \ + + encode_str(self.sealed_incidence) \ + + encode_str(self.question_incidence) \ + + encode_str(self.reduce_max_hp_incidence) \ + + encode_str(self.burning_incidence) \ + + encode_byte(self.quake) \ + + encode_byte(self.can_parry) \ + + encode_str(self.knock_back) \ + + encode_str(self.knock_back_g) \ + + encode_byte(self.forward_knock_back) \ + + encode_short(self.attack_type) \ + + encode_short(self.physics_attr) \ + + encode_short(self.magic_attr) \ + + encode_byte(self.spell_blast) \ + + encode_str(self.hit_se) \ + + encode_str(self.weak_se) \ + + encode_str(self.resist_se) + +class MonsterActionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.monster_action_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.slot = decode_int(data, off) + off += INT_OFF + + self.usable_lv = decode_int(data, off) + off += INT_OFF + + self.use_count = decode_int(data, off) + off += INT_OFF + + self.not_angry = decode_byte(data, off) + off += BYTE_OFF + + self.is_angry = decode_byte(data, off) + off += BYTE_OFF + + self.can_sealed = decode_byte(data, off) + off += BYTE_OFF + + self.multi_play_only = decode_byte(data, off) + off += BYTE_OFF + + self.angle_min, new_off = decode_str(data, off) + off += new_off + + self.angle_max, new_off = decode_str(data, off) + off += new_off + + self.range_min, new_off = decode_str(data, off) + off += new_off + + self.range_max, new_off = decode_str(data, off) + off += new_off + + self.attack_time, new_off = decode_str(data, off) + off += new_off + + self.delay_time, new_off = decode_str(data, off) + off += new_off + + self.cool_time, new_off = decode_str(data, off) + off += new_off + + self.state, new_off = decode_str(data, off) + off += new_off + + self.sub_state, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MonsterActionData": + ret = cls(b"\x00" * 99, 0) + ret.monster_action_id = data['MonsterActionId'] + ret.name = data['Name'] + ret.unit_id = data['UnitId'] + ret.slot = data['Slot'] + ret.usable_lv = data['UsableLv'] + ret.use_count = data['UseCount'] + ret.not_angry = data['NotAngry'] + ret.is_angry = data['IsAngry'] + ret.can_sealed = data['CanSealed'] + ret.multi_play_only = data['MultiPlayOnly'] + ret.angle_min = data['AngleMin'] + ret.angle_max = data['AngleMax'] + ret.range_min = data['RangeMin'] + ret.range_max = data['RangeMax'] + ret.attack_time = data['AttackTime'] + ret.delay_time = data['DelayTime'] + ret.cool_time = data['CoolTime'] + ret.state = data['State'] + ret.sub_state = data['SubState'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.monster_action_id) \ + + encode_str(self.name) \ + + encode_int(self.unit_id) \ + + encode_int(self.slot) \ + + encode_int(self.usable_lv) \ + + encode_int(self.use_count) \ + + encode_byte(self.not_angry) \ + + encode_byte(self.is_angry) \ + + encode_byte(self.can_sealed) \ + + encode_byte(self.multi_play_only) \ + + encode_str(self.angle_min) \ + + encode_str(self.angle_max) \ + + encode_str(self.range_min) \ + + encode_str(self.range_max) \ + + encode_str(self.attack_time) \ + + encode_str(self.delay_time) \ + + encode_str(self.cool_time) \ + + encode_str(self.state) \ + + encode_str(self.sub_state) + +class PropertyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.property_id = decode_int(data, off) + off += INT_OFF + + self.property_target_type = decode_short(data, off) + off += SHORT_OFF + + self.property_name, new_off = decode_str(data, off) + off += new_off + + self.property_name_format, new_off = decode_str(data, off) + off += new_off + + self.property_type_id = decode_int(data, off) + off += INT_OFF + + self.value1_min = decode_int(data, off) + off += INT_OFF + + self.value1_max = decode_int(data, off) + off += INT_OFF + + self.value2_min = decode_int(data, off) + off += INT_OFF + + self.value2_max = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PropertyData": + ret = cls(b"\x00" * 99, 0) + ret.property_id = data['PropertyId'] + ret.property_target_type = data['PropertyTargetType'] + ret.property_name = data['PropertyName'] + ret.property_name_format = data['PropertyNameFormat'] + ret.property_type_id = data['PropertyTypeId'] + ret.value1_min = data['Value1Min'] + ret.value1_max = data['Value1Max'] + ret.value2_min = data['Value2Min'] + ret.value2_max = data['Value2Max'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.property_id) \ + + encode_short(self.property_target_type) \ + + encode_str(self.property_name) \ + + encode_str(self.property_name_format) \ + + encode_int(self.property_type_id) \ + + encode_int(self.value1_min) \ + + encode_int(self.value1_max) \ + + encode_int(self.value2_min) \ + + encode_int(self.value2_max) + +class PropertyTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.property_table_id = decode_int(data, off) + off += INT_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.property_id = decode_int(data, off) + off += INT_OFF + + self.value1_min = decode_int(data, off) + off += INT_OFF + + self.value1_max = decode_int(data, off) + off += INT_OFF + + self.value2_min = decode_int(data, off) + off += INT_OFF + + self.value2_max = decode_int(data, off) + off += INT_OFF + + self.rate = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PropertyTableData": + ret = cls(b"\x00" * 99, 0) + ret.property_table_id = data['PropertyTableId'] + ret.property_table_sub_id = data['PropertyTableSubId'] + ret.property_id = data['PropertyId'] + ret.value1_min = data['Value1Min'] + ret.value1_max = data['Value1Max'] + ret.value2_min = data['Value2Min'] + ret.value2_max = data['Value2Max'] + ret.rate = data['Rate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.property_table_id) \ + + encode_int(self.property_table_sub_id) \ + + encode_int(self.property_id) \ + + encode_int(self.value1_min) \ + + encode_int(self.value1_max) \ + + encode_int(self.value2_min) \ + + encode_int(self.value2_max) \ + + encode_int(self.rate) + +class PropertyTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.property_type_id = decode_int(data, off) + off += INT_OFF + + self.property_type_name, new_off = decode_str(data, off) + off += new_off + + self.physics_attr = decode_int(data, off) + off += INT_OFF + + self.magic_attr = decode_int(data, off) + off += INT_OFF + + self.permission_same_id_flag = decode_byte(data, off) + off += BYTE_OFF + + self.auto_equip_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PropertyTypeData": + ret = cls(b"\x00" * 99, 0) + ret.property_type_id = data['PropertyTypeId'] + ret.property_type_name = data['PropertyTypeName'] + ret.physics_attr = data['PhysicsAttr'] + ret.magic_attr = data['MagicAttr'] + ret.permission_same_id_flag = data['PermissionSameIdFlag'] + ret.auto_equip_flag = data['AutoEquipFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.property_type_id) \ + + encode_str(self.property_type_name) \ + + encode_int(self.physics_attr) \ + + encode_int(self.magic_attr) \ + + encode_byte(self.permission_same_id_flag) \ + + encode_byte(self.auto_equip_flag) + +class SkillData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.skill_id = decode_short(data, off) + off += SHORT_OFF + + self.weapon_type_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.attack = decode_byte(data, off) + off += BYTE_OFF + + self.passive = decode_byte(data, off) + off += BYTE_OFF + + self.pet = decode_byte(data, off) + off += BYTE_OFF + + self.level = decode_short(data, off) + off += SHORT_OFF + + self.skill_condition = decode_short(data, off) + off += SHORT_OFF + + self.cool_time, new_off = decode_str(data, off) + off += new_off + + self.sword_color_r, new_off = decode_str(data, off) + off += new_off + + self.sword_color_g, new_off = decode_str(data, off) + off += new_off + + self.sword_color_b, new_off = decode_str(data, off) + off += new_off + + self.motion_index = decode_short(data, off) + off += SHORT_OFF + + self.state, new_off = decode_str(data, off) + off += new_off + + self.sub_state, new_off = decode_str(data, off) + off += new_off + + self.skill_icon, new_off = decode_str(data, off) + off += new_off + + self.friend_skill_icon, new_off = decode_str(data, off) + off += new_off + + self.info_text, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SkillData": + ret = cls(b"\x00" * 99, 0) + ret.skill_id = data['SkillId'] + ret.weapon_type_id = data['WeaponTypeId'] + ret.name = data['Name'] + ret.attack = data['Attack'] + ret.passive = data['Passive'] + ret.pet = data['Pet'] + ret.level = data['Level'] + ret.skill_condition = data['SkillCondition'] + ret.cool_time = data['CoolTime'] + ret.sword_color_r = data['SwordColorR'] + ret.sword_color_g = data['SwordColorG'] + ret.sword_color_b = data['SwordColorB'] + ret.motion_index = data['MotionIndex'] + ret.state = data['State'] + ret.sub_state = data['SubState'] + ret.skill_icon = data['SkillIcon'] + ret.friend_skill_icon = data['FriendSkillIcon'] + ret.info_text = data['InfoText'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.skill_id) \ + + encode_short(self.weapon_type_id) \ + + encode_str(self.name) \ + + encode_byte(self.attack) \ + + encode_byte(self.passive) \ + + encode_byte(self.pet) \ + + encode_short(self.level) \ + + encode_short(self.skill_condition) \ + + encode_str(self.cool_time) \ + + encode_str(self.sword_color_r) \ + + encode_str(self.sword_color_g) \ + + encode_str(self.sword_color_b) \ + + encode_short(self.motion_index) \ + + encode_str(self.state) \ + + encode_str(self.sub_state) \ + + encode_str(self.skill_icon) \ + + encode_str(self.friend_skill_icon) \ + + encode_str(self.info_text) + +class SkillTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.skill_table_id = decode_int(data, off) + off += INT_OFF + + self.skill_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.level = decode_int(data, off) + off += INT_OFF + + self.awakening_id = decode_int(data, off) + off += INT_OFF + + self.skill_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SkillTableData": + ret = cls(b"\x00" * 99, 0) + ret.skill_table_id = data['SkillTableId'] + ret.skill_table_sub_id = data['SkillTableSubId'] + ret.level = data['Level'] + ret.awakening_id = data['AwakeningId'] + ret.skill_id = data['SkillId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.skill_table_id) \ + + encode_int(self.skill_table_sub_id) \ + + encode_int(self.level) \ + + encode_int(self.awakening_id) \ + + encode_int(self.skill_id) + +class SkillLevelData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.skill_level_id = decode_short(data, off) + off += SHORT_OFF + + self.exp = decode_int(data, off) + off += INT_OFF + + self.exp_total = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SkillLevelData": + ret = cls(b"\x00" * 99, 0) + ret.skill_level_id = data['SkillLevelId'] + ret.exp = data['Exp'] + ret.exp_total = data['ExpTotal'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.skill_level_id) \ + + encode_int(self.exp) \ + + encode_int(self.exp_total) + +class AwakeningData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.awakening_id = decode_short(data, off) + off += SHORT_OFF + + self.total_exp = decode_int(data, off) + off += INT_OFF + + self.bonus_hero_log, new_off = decode_str(data, off) + off += new_off + + self.bonus_weapon, new_off = decode_str(data, off) + off += new_off + + self.bonus_armor, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AwakeningData": + ret = cls(b"\x00" * 99, 0) + ret.awakening_id = data['AwakeningId'] + ret.total_exp = data['TotalExp'] + ret.bonus_hero_log = data['BonusHeroLog'] + ret.bonus_weapon = data['BonusWeapon'] + ret.bonus_armor = data['BonusArmor'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.awakening_id) \ + + encode_int(self.total_exp) \ + + encode_str(self.bonus_hero_log) \ + + encode_str(self.bonus_weapon) \ + + encode_str(self.bonus_armor) + +class SynchroSkillData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.synchro_skill_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.name_in_skill_cutin, new_off = decode_str(data, off) + off += new_off + + self.sub_state, new_off = decode_str(data, off) + off += new_off + + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.icon_file_path, new_off = decode_str(data, off) + off += new_off + + self.info_text, new_off = decode_str(data, off) + off += new_off + + self.se_name, new_off = decode_str(data, off) + off += new_off + + self.voice_id, new_off = decode_str(data, off) + off += new_off + + self.delay_time = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SynchroSkillData": + ret = cls(b"\x00" * 99, 0) + ret.synchro_skill_id = data['SynchroSkillId'] + ret.name = data['Name'] + ret.name_in_skill_cutin = data['NameInSkillCutin'] + ret.sub_state = data['SubState'] + ret.chara_id = data['CharaId'] + ret.icon_file_path = data['IconFilePath'] + ret.info_text = data['InfoText'] + ret.se_name = data['SeName'] + ret.voice_id = data['VoiceId'] + ret.delay_time = data['DelayTime'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.synchro_skill_id) \ + + encode_str(self.name) \ + + encode_str(self.name_in_skill_cutin) \ + + encode_str(self.sub_state) \ + + encode_short(self.chara_id) \ + + encode_str(self.icon_file_path) \ + + encode_str(self.info_text) \ + + encode_str(self.se_name) \ + + encode_str(self.voice_id) \ + + encode_int(self.delay_time) + +class SoundSkillCutInVoiceData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.sound_skill_cut_in_voice_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_id = decode_int(data, off) + off += INT_OFF + + self.chara_id = decode_int(data, off) + off += INT_OFF + + self.record_id, new_off = decode_str(data, off) + off += new_off + + self.que_name, new_off = decode_str(data, off) + off += new_off + + self.delay_time = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SoundSkillCutInVoiceData": + ret = cls(b"\x00" * 99, 0) + ret.sound_skill_cut_in_voice_id = data['SoundSkillCutInVoiceId'] + ret.skill_id = data['SkillId'] + ret.chara_id = data['CharaId'] + ret.record_id = data['RecordId'] + ret.que_name = data['QueName'] + ret.delay_time = data['DelayTime'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.sound_skill_cut_in_voice_id) \ + + encode_int(self.skill_id) \ + + encode_int(self.chara_id) \ + + encode_str(self.record_id) \ + + encode_str(self.que_name) \ + + encode_int(self.delay_time) + +class QuestSceneData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_scene_id = decode_short(data, off) + off += SHORT_OFF + + self.sort_no = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.map_data, new_off = decode_str(data, off) + off += new_off + + self.unit_data, new_off = decode_str(data, off) + off += new_off + + self.demo_map, new_off = decode_str(data, off) + off += new_off + + self.bgm_basic, new_off = decode_str(data, off) + off += new_off + + self.bgm_boss, new_off = decode_str(data, off) + off += new_off + + self.tutorial = decode_byte(data, off) + off += BYTE_OFF + + self.chara_comment_id_0 = decode_int(data, off) + off += INT_OFF + + self.chara_comment_id_1 = decode_int(data, off) + off += INT_OFF + + self.chara_comment_id_2 = decode_int(data, off) + off += INT_OFF + + self.chara_comment_id_3 = decode_int(data, off) + off += INT_OFF + + self.chara_comment_id_4 = decode_int(data, off) + off += INT_OFF + + self.col_rate, new_off = decode_str(data, off) + off += new_off + + self.limit_default = decode_int(data, off) + off += INT_OFF + + self.limit_time_dec, new_off = decode_str(data, off) + off += new_off + + self.limit_chara_dec, new_off = decode_str(data, off) + off += new_off + + self.limit_resurrection = decode_int(data, off) + off += INT_OFF + + self.mission_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.mission_enemy_lv_limit = decode_short(data, off) + off += SHORT_OFF + + self.reward_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.player_trace_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.success_player_exp = decode_int(data, off) + off += INT_OFF + + self.failed_player_exp = decode_int(data, off) + off += INT_OFF + + self.greed_spawn_wait_count = decode_int(data, off) + off += INT_OFF + + self.honey_spawn_wait_count = decode_int(data, off) + off += INT_OFF + + self.menu_display_enemy_set_id = decode_int(data, off) + off += INT_OFF + + self.stage_filepath, new_off = decode_str(data, off) + off += new_off + + self.rarity_up_chance_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_mob_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_mob_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_leader_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_leader_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_boss_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_boss_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.pair_exp_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_mob_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_mob_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_leader_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_leader_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_boss_hp_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_boss_atk_rate, new_off = decode_str(data, off) + off += new_off + + self.trio_exp_rate, new_off = decode_str(data, off) + off += new_off + + self.single_reward_vp = decode_int(data, off) + off += INT_OFF + + self.pair_reward_vp = decode_int(data, off) + off += INT_OFF + + self.trio_reward_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSceneData": + ret = cls(b"\x00" * 99, 0) + ret.quest_scene_id = data['QuestSceneId'] + ret.sort_no = data['SortNo'] + ret.name = data['Name'] + ret.map_data = data['MapData'] + ret.unit_data = data['UnitData'] + ret.demo_map = data['DemoMap'] + ret.bgm_basic = data['BgmBasic'] + ret.bgm_boss = data['BgmBoss'] + ret.tutorial = data['Tutorial'] + ret.chara_comment_id_0 = data['CharaCommentId0'] + ret.chara_comment_id_1 = data['CharaCommentId1'] + ret.chara_comment_id_2 = data['CharaCommentId2'] + ret.chara_comment_id_3 = data['CharaCommentId3'] + ret.chara_comment_id_4 = data['CharaCommentId4'] + ret.col_rate = data['ColRate'] + ret.limit_default = data['LimitDefault'] + ret.limit_time_dec = data['LimitTimeDec'] + ret.limit_chara_dec = data['LimitCharaDec'] + ret.limit_resurrection = data['LimitResurrection'] + ret.mission_table_sub_id = data['MissionTableSubId'] + ret.mission_enemy_lv_limit = data['MissionEnemyLvLimit'] + ret.reward_table_sub_id = data['RewardTableSubId'] + ret.player_trace_table_sub_id = data['PlayerTraceTableSubId'] + ret.success_player_exp = data['SuccessPlayerExp'] + ret.failed_player_exp = data['FailedPlayerExp'] + ret.greed_spawn_wait_count = data['GreedSpawnWaitCount'] + ret.honey_spawn_wait_count = data['HoneySpawnWaitCount'] + ret.menu_display_enemy_set_id = data['MenuDisplayEnemySetId'] + ret.stage_filepath = data['StageFilepath'] + ret.rarity_up_chance_rate = data['RarityUpChanceRate'] + ret.pair_mob_hp_rate = data['PairMobHpRate'] + ret.pair_mob_atk_rate = data['PairMobAtkRate'] + ret.pair_leader_hp_rate = data['PairLeaderHpRate'] + ret.pair_leader_atk_rate = data['PairLeaderAtkRate'] + ret.pair_boss_hp_rate = data['PairBossHpRate'] + ret.pair_boss_atk_rate = data['PairBossAtkRate'] + ret.pair_exp_rate = data['PairExpRate'] + ret.trio_mob_hp_rate = data['TrioMobHpRate'] + ret.trio_mob_atk_rate = data['TrioMobAtkRate'] + ret.trio_leader_hp_rate = data['TrioLeaderHpRate'] + ret.trio_leader_atk_rate = data['TrioLeaderAtkRate'] + ret.trio_boss_hp_rate = data['TrioBossHpRate'] + ret.trio_boss_atk_rate = data['TrioBossAtkRate'] + ret.trio_exp_rate = data['TrioExpRate'] + ret.single_reward_vp = data['SingleRewardVp'] + ret.pair_reward_vp = data['PairRewardVp'] + ret.trio_reward_vp = data['TrioRewardVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.quest_scene_id) \ + + encode_short(self.sort_no) \ + + encode_str(self.name) \ + + encode_str(self.map_data) \ + + encode_str(self.unit_data) \ + + encode_str(self.demo_map) \ + + encode_str(self.bgm_basic) \ + + encode_str(self.bgm_boss) \ + + encode_byte(self.tutorial) \ + + encode_int(self.chara_comment_id_0) \ + + encode_int(self.chara_comment_id_1) \ + + encode_int(self.chara_comment_id_2) \ + + encode_int(self.chara_comment_id_3) \ + + encode_int(self.chara_comment_id_4) \ + + encode_str(self.col_rate) \ + + encode_int(self.limit_default) \ + + encode_str(self.limit_time_dec) \ + + encode_str(self.limit_chara_dec) \ + + encode_int(self.limit_resurrection) \ + + encode_int(self.mission_table_sub_id) \ + + encode_short(self.mission_enemy_lv_limit) \ + + encode_int(self.reward_table_sub_id) \ + + encode_int(self.player_trace_table_sub_id) \ + + encode_int(self.success_player_exp) \ + + encode_int(self.failed_player_exp) \ + + encode_int(self.greed_spawn_wait_count) \ + + encode_int(self.honey_spawn_wait_count) \ + + encode_int(self.menu_display_enemy_set_id) \ + + encode_str(self.stage_filepath) \ + + encode_str(self.rarity_up_chance_rate) \ + + encode_str(self.pair_mob_hp_rate) \ + + encode_str(self.pair_mob_atk_rate) \ + + encode_str(self.pair_leader_hp_rate) \ + + encode_str(self.pair_leader_atk_rate) \ + + encode_str(self.pair_boss_hp_rate) \ + + encode_str(self.pair_boss_atk_rate) \ + + encode_str(self.pair_exp_rate) \ + + encode_str(self.trio_mob_hp_rate) \ + + encode_str(self.trio_mob_atk_rate) \ + + encode_str(self.trio_leader_hp_rate) \ + + encode_str(self.trio_leader_atk_rate) \ + + encode_str(self.trio_boss_hp_rate) \ + + encode_str(self.trio_boss_atk_rate) \ + + encode_str(self.trio_exp_rate) \ + + encode_int(self.single_reward_vp) \ + + encode_int(self.pair_reward_vp) \ + + encode_int(self.trio_reward_vp) + +class QuestExistUnitData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_exist_unit_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestExistUnitData": + ret = cls(b"\x00" * 99, 0) + ret.quest_exist_unit_id = data['QuestExistUnitId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.enemy_kind_id = data['EnemyKindId'] + ret.unit_id = data['UnitId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_exist_unit_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.enemy_kind_id) \ + + encode_int(self.unit_id) + +class QuestEpisodeAppendRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_episode_append_reward_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.episode_append_id = decode_int(data, off) + off += INT_OFF + + self.episode_append_num = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestEpisodeAppendRewardData": + ret = cls(b"\x00" * 99, 0) + ret.quest_episode_append_reward_id = data['QuestEpisodeAppendRewardId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.episode_append_id = data['EpisodeAppendId'] + ret.episode_append_num = data['EpisodeAppendNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_episode_append_reward_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.episode_append_id) \ + + encode_int(self.episode_append_num) + +class SideQuestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.side_quest_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.episode_num = decode_int(data, off) + off += INT_OFF + + self.ex_bonus_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.recommend_lv = decode_int(data, off) + off += INT_OFF + + self.comment_summary, new_off = decode_str(data, off) + off += new_off + + self.comment_introduction, new_off = decode_str(data, off) + off += new_off + + self.start_adv_name, new_off = decode_str(data, off) + off += new_off + + self.end_adv_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SideQuestData": + ret = cls(b"\x00" * 99, 0) + ret.side_quest_id = data['SideQuestId'] + ret.display_name = data['DisplayName'] + ret.episode_num = data['EpisodeNum'] + ret.ex_bonus_table_sub_id = data['ExBonusTableSubId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.recommend_lv = data['RecommendLv'] + ret.comment_summary = data['CommentSummary'] + ret.comment_introduction = data['CommentIntroduction'] + ret.start_adv_name = data['StartAdvName'] + ret.end_adv_name = data['EndAdvName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.side_quest_id) \ + + encode_str(self.display_name) \ + + encode_int(self.episode_num) \ + + encode_int(self.ex_bonus_table_sub_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.recommend_lv) \ + + encode_str(self.comment_summary) \ + + encode_str(self.comment_introduction) \ + + encode_str(self.start_adv_name) \ + + encode_str(self.end_adv_name) + +class EpisodeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.episode_id = decode_int(data, off) + off += INT_OFF + + self.episode_chapter_id = decode_int(data, off) + off += INT_OFF + + self.release_episode_id = decode_int(data, off) + off += INT_OFF + + self.episode_num = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.comment_summary, new_off = decode_str(data, off) + off += new_off + + self.comment_introduction, new_off = decode_str(data, off) + off += new_off + + self.recommend_lv = decode_int(data, off) + off += INT_OFF + + self.ex_bonus_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.start_adv_name, new_off = decode_str(data, off) + off += new_off + + self.end_adv_name, new_off = decode_str(data, off) + off += new_off + + self.unlock_still_id = decode_int(data, off) + off += INT_OFF + + self.required_release_episode_append_id = decode_int(data, off) + off += INT_OFF + + self.required_release_episode_append_num = decode_short(data, off) + off += SHORT_OFF + + self.release_reward_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EpisodeData": + ret = cls(b"\x00" * 99, 0) + ret.episode_id = data['EpisodeId'] + ret.episode_chapter_id = data['EpisodeChapterId'] + ret.release_episode_id = data['ReleaseEpisodeId'] + ret.episode_num = data['EpisodeNum'] + ret.title = data['Title'] + ret.comment_summary = data['CommentSummary'] + ret.comment_introduction = data['CommentIntroduction'] + ret.recommend_lv = data['RecommendLv'] + ret.ex_bonus_table_sub_id = data['ExBonusTableSubId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.start_adv_name = data['StartAdvName'] + ret.end_adv_name = data['EndAdvName'] + ret.unlock_still_id = data['UnlockStillId'] + ret.required_release_episode_append_id = data['RequiredReleaseEpisodeAppendId'] + ret.required_release_episode_append_num = data['RequiredReleaseEpisodeAppendNum'] + ret.release_reward_vp = data['ReleaseRewardVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.episode_id) \ + + encode_int(self.episode_chapter_id) \ + + encode_int(self.release_episode_id) \ + + encode_int(self.episode_num) \ + + encode_str(self.title) \ + + encode_str(self.comment_summary) \ + + encode_str(self.comment_introduction) \ + + encode_int(self.recommend_lv) \ + + encode_int(self.ex_bonus_table_sub_id) \ + + encode_int(self.quest_scene_id) \ + + encode_str(self.start_adv_name) \ + + encode_str(self.end_adv_name) \ + + encode_int(self.unlock_still_id) \ + + encode_int(self.required_release_episode_append_id) \ + + encode_short(self.required_release_episode_append_num) \ + + encode_int(self.release_reward_vp) + +class EpisodeChapterData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.episode_chapter_id = decode_int(data, off) + off += INT_OFF + + self.episode_part_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EpisodeChapterData": + ret = cls(b"\x00" * 99, 0) + ret.episode_chapter_id = data['EpisodeChapterId'] + ret.episode_part_id = data['EpisodePartId'] + ret.display_name = data['DisplayName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.episode_chapter_id) \ + + encode_int(self.episode_part_id) \ + + encode_str(self.display_name) + +class EpisodePartData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.episode_part_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EpisodePartData": + ret = cls(b"\x00" * 99, 0) + ret.episode_part_id = data['EpisodePartId'] + ret.display_name = data['DisplayName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.episode_part_id) \ + + encode_str(self.display_name) + +class TrialTowerData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.trial_tower_id = decode_short(data, off) + off += SHORT_OFF + + self.release_trial_tower_id = decode_int(data, off) + off += INT_OFF + + self.ex_bonus_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.recommend_lv = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TrialTowerData": + ret = cls(b"\x00" * 99, 0) + ret.trial_tower_id = data['TrialTowerId'] + ret.release_trial_tower_id = data['ReleaseTrialTowerId'] + ret.ex_bonus_table_sub_id = data['ExBonusTableSubId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.recommend_lv = data['RecommendLv'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.trial_tower_id) \ + + encode_int(self.release_trial_tower_id) \ + + encode_int(self.ex_bonus_table_sub_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.recommend_lv) + +class ExTowerData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_tower_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.release_trial_tower_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ExTowerData": + ret = cls(b"\x00" * 99, 0) + ret.ex_tower_id = data['ExTowerId'] + ret.title = data['Title'] + ret.release_trial_tower_id = data['ReleaseTrialTowerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_tower_id) \ + + encode_str(self.title) \ + + encode_int(self.release_trial_tower_id) + +class ExTowerQuestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_tower_quest_id = decode_int(data, off) + off += INT_OFF + + self.ex_tower_id = decode_int(data, off) + off += INT_OFF + + self.release_ex_tower_quest_id = decode_int(data, off) + off += INT_OFF + + self.level_num = decode_short(data, off) + off += SHORT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.ex_bonus_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.recommend_lv = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ExTowerQuestData": + ret = cls(b"\x00" * 99, 0) + ret.ex_tower_quest_id = data['ExTowerQuestId'] + ret.ex_tower_id = data['ExTowerId'] + ret.release_ex_tower_quest_id = data['ReleaseExTowerQuestId'] + ret.level_num = data['LevelNum'] + ret.title = data['Title'] + ret.ex_bonus_table_sub_id = data['ExBonusTableSubId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.recommend_lv = data['RecommendLv'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_tower_quest_id) \ + + encode_int(self.ex_tower_id) \ + + encode_int(self.release_ex_tower_quest_id) \ + + encode_short(self.level_num) \ + + encode_str(self.title) \ + + encode_int(self.ex_bonus_table_sub_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.recommend_lv) + +class MenuDisplayEnemyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.menu_display_enemy_id = decode_short(data, off) + off += SHORT_OFF + + self.menu_display_enemy_set_id = decode_int(data, off) + off += INT_OFF + + self.boss_flag = decode_byte(data, off) + off += BYTE_OFF + + self.enemy_kind_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MenuDisplayEnemyData": + ret = cls(b"\x00" * 99, 0) + ret.menu_display_enemy_id = data['MenuDisplayEnemyId'] + ret.menu_display_enemy_set_id = data['MenuDisplayEnemySetId'] + ret.boss_flag = data['BossFlag'] + ret.enemy_kind_id = data['EnemyKindId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.menu_display_enemy_id) \ + + encode_int(self.menu_display_enemy_set_id) \ + + encode_byte(self.boss_flag) \ + + encode_int(self.enemy_kind_id) + +class MissionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.mission_id = decode_short(data, off) + off += SHORT_OFF + + self.mission_difficulty_id = decode_int(data, off) + off += INT_OFF + + self.mission_name, new_off = decode_str(data, off) + off += new_off + + self.time_limit = decode_int(data, off) + off += INT_OFF + + self.condition = decode_short(data, off) + off += SHORT_OFF + + self.enemy_set_id = decode_int(data, off) + off += INT_OFF + + self.value1 = decode_int(data, off) + off += INT_OFF + + self.value2 = decode_int(data, off) + off += INT_OFF + + self.enemy_hide = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MissionData": + ret = cls(b"\x00" * 99, 0) + ret.mission_id = data['MissionId'] + ret.mission_difficulty_id = data['MissionDifficultyId'] + ret.mission_name = data['MissionName'] + ret.time_limit = data['TimeLimit'] + ret.condition = data['Condition'] + ret.enemy_set_id = data['EnemySetId'] + ret.value1 = data['Value1'] + ret.value2 = data['Value2'] + ret.enemy_hide = data['EnemyHide'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.mission_id) \ + + encode_int(self.mission_difficulty_id) \ + + encode_str(self.mission_name) \ + + encode_int(self.time_limit) \ + + encode_short(self.condition) \ + + encode_int(self.enemy_set_id) \ + + encode_int(self.value1) \ + + encode_int(self.value2) \ + + encode_byte(self.enemy_hide) + +class MissionTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.mission_table_id = decode_int(data, off) + off += INT_OFF + + self.mission_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.mission_id = decode_int(data, off) + off += INT_OFF + + self.rate = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MissionTableData": + ret = cls(b"\x00" * 99, 0) + ret.mission_table_id = data['MissionTableId'] + ret.mission_table_sub_id = data['MissionTableSubId'] + ret.mission_id = data['MissionId'] + ret.rate = data['Rate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.mission_table_id) \ + + encode_int(self.mission_table_sub_id) \ + + encode_int(self.mission_id) \ + + encode_int(self.rate) + +class MissionDifficultyData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.mission_difficulty_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_exp = decode_int(data, off) + off += INT_OFF + + self.unanalyzed_log_exp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "MissionDifficultyData": + ret = cls(b"\x00" * 99, 0) + ret.mission_difficulty_id = data['MissionDifficultyId'] + ret.skill_exp = data['SkillExp'] + ret.unanalyzed_log_exp = data['UnanalyzedLogExp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.mission_difficulty_id) \ + + encode_int(self.skill_exp) \ + + encode_int(self.unanalyzed_log_exp) + +class BattleCameraData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.battle_camera_id = decode_short(data, off) + off += SHORT_OFF + + self.offset_x, new_off = decode_str(data, off) + off += new_off + + self.offset_y, new_off = decode_str(data, off) + off += new_off + + self.offset_z, new_off = decode_str(data, off) + off += new_off + + self.rot_h, new_off = decode_str(data, off) + off += new_off + + self.rot_v, new_off = decode_str(data, off) + off += new_off + + self.distance, new_off = decode_str(data, off) + off += new_off + + self.near, new_off = decode_str(data, off) + off += new_off + + self.far, new_off = decode_str(data, off) + off += new_off + + self.fov, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BattleCameraData": + ret = cls(b"\x00" * 99, 0) + ret.battle_camera_id = data['BattleCameraId'] + ret.offset_x = data['OffsetX'] + ret.offset_y = data['OffsetY'] + ret.offset_z = data['OffsetZ'] + ret.rot_h = data['RotH'] + ret.rot_v = data['RotV'] + ret.distance = data['Distance'] + ret.near = data['Near'] + ret.far = data['Far'] + ret.fov = data['Fov'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.battle_camera_id) \ + + encode_str(self.offset_x) \ + + encode_str(self.offset_y) \ + + encode_str(self.offset_z) \ + + encode_str(self.rot_h) \ + + encode_str(self.rot_v) \ + + encode_str(self.distance) \ + + encode_str(self.near) \ + + encode_str(self.far) \ + + encode_str(self.fov) + +class ChatMainStoryData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chat_main_story_id = decode_int(data, off) + off += INT_OFF + + self.release_condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.release_condition_value = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.first_reward_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ChatMainStoryData": + ret = cls(b"\x00" * 99, 0) + ret.chat_main_story_id = data['ChatMainStoryId'] + ret.release_condition_type = data['ReleaseConditionType'] + ret.release_condition_value = data['ReleaseConditionValue'] + ret.display_name = data['DisplayName'] + ret.first_reward_vp = data['FirstRewardVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.chat_main_story_id) \ + + encode_byte(self.release_condition_type) \ + + encode_int(self.release_condition_value) \ + + encode_str(self.display_name) \ + + encode_int(self.first_reward_vp) + +class ChatSideStoryData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chat_side_story_id = decode_int(data, off) + off += INT_OFF + + self.unlock_side_quest_id = decode_int(data, off) + off += INT_OFF + + self.release_condition_chara_id = decode_short(data, off) + off += SHORT_OFF + + self.release_condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.release_condition_value1 = decode_int(data, off) + off += INT_OFF + + self.release_condition_value2 = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.first_reward_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ChatSideStoryData": + ret = cls(b"\x00" * 99, 0) + ret.chat_side_story_id = data['ChatSideStoryId'] + ret.unlock_side_quest_id = data['UnlockSideQuestId'] + ret.release_condition_chara_id = data['ReleaseConditionCharaId'] + ret.release_condition_type = data['ReleaseConditionType'] + ret.release_condition_value1 = data['ReleaseConditionValue1'] + ret.release_condition_value2 = data['ReleaseConditionValue2'] + ret.display_name = data['DisplayName'] + ret.first_reward_vp = data['FirstRewardVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.chat_side_story_id) \ + + encode_int(self.unlock_side_quest_id) \ + + encode_short(self.release_condition_chara_id) \ + + encode_byte(self.release_condition_type) \ + + encode_int(self.release_condition_value1) \ + + encode_int(self.release_condition_value2) \ + + encode_str(self.display_name) \ + + encode_int(self.first_reward_vp) + +class ChatEventStoryData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chat_event_story_id = decode_int(data, off) + off += INT_OFF + + self.event_id = decode_int(data, off) + off += INT_OFF + + self.release_condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.release_condition_value = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.first_reward_vp = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ChatEventStoryData": + ret = cls(b"\x00" * 99, 0) + ret.chat_event_story_id = data['ChatEventStoryId'] + ret.event_id = data['EventId'] + ret.release_condition_type = data['ReleaseConditionType'] + ret.release_condition_value = data['ReleaseConditionValue'] + ret.display_name = data['DisplayName'] + ret.first_reward_vp = data['FirstRewardVp'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.chat_event_story_id) \ + + encode_int(self.event_id) \ + + encode_byte(self.release_condition_type) \ + + encode_int(self.release_condition_value) \ + + encode_str(self.display_name) \ + + encode_int(self.first_reward_vp) + +class NavigatorCharaData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.navigator_chara_id = decode_int(data, off) + off += INT_OFF + + self.condition_start_episode_id = decode_int(data, off) + off += INT_OFF + + self.condition_end_episode_id = decode_int(data, off) + off += INT_OFF + + self.disp_chara = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "NavigatorCharaData": + ret = cls(b"\x00" * 99, 0) + ret.navigator_chara_id = data['NavigatorCharaId'] + ret.condition_start_episode_id = data['ConditionStartEpisodeId'] + ret.condition_end_episode_id = data['ConditionEndEpisodeId'] + ret.disp_chara = data['DispChara'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.navigator_chara_id) \ + + encode_int(self.condition_start_episode_id) \ + + encode_int(self.condition_end_episode_id) \ + + encode_int(self.disp_chara) + +class NavigatorCommentData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.navigator_comment_id = decode_int(data, off) + off += INT_OFF + + self.menu_id = decode_int(data, off) + off += INT_OFF + + self.draw_type = decode_int(data, off) + off += INT_OFF + + self.condition_past_menu = decode_int(data, off) + off += INT_OFF + + self.priority = decode_int(data, off) + off += INT_OFF + + self.condition_start_episode_id = decode_int(data, off) + off += INT_OFF + + self.condition_end_episode_id = decode_int(data, off) + off += INT_OFF + + self.only_once = decode_byte(data, off) + off += BYTE_OFF + + self.only_rico = decode_byte(data, off) + off += BYTE_OFF + + self.comment_ai, new_off = decode_str(data, off) + off += new_off + + self.comment_rico, new_off = decode_str(data, off) + off += new_off + + self.expression = decode_int(data, off) + off += INT_OFF + + self.voice_id, new_off = decode_str(data, off) + off += new_off + + self.show_start_time, new_off = decode_str(data, off) + off += new_off + + self.show_end_time, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "NavigatorCommentData": + ret = cls(b"\x00" * 99, 0) + ret.navigator_comment_id = data['NavigatorCommentId'] + ret.menu_id = data['MenuId'] + ret.draw_type = data['DrawType'] + ret.condition_past_menu = data['ConditionPastMenu'] + ret.priority = data['Priority'] + ret.condition_start_episode_id = data['ConditionStartEpisodeId'] + ret.condition_end_episode_id = data['ConditionEndEpisodeId'] + ret.only_once = data['OnlyOnce'] + ret.only_rico = data['OnlyRico'] + ret.comment_ai = data['CommentAi'] + ret.comment_rico = data['CommentRico'] + ret.expression = data['Expression'] + ret.voice_id = data['VoiceId'] + ret.show_start_time = data['ShowStartTime'] + ret.show_end_time = data['ShowEndTime'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.navigator_comment_id) \ + + encode_int(self.menu_id) \ + + encode_int(self.draw_type) \ + + encode_int(self.condition_past_menu) \ + + encode_int(self.priority) \ + + encode_int(self.condition_start_episode_id) \ + + encode_int(self.condition_end_episode_id) \ + + encode_byte(self.only_once) \ + + encode_byte(self.only_rico) \ + + encode_str(self.comment_ai) \ + + encode_str(self.comment_rico) \ + + encode_int(self.expression) \ + + encode_str(self.voice_id) \ + + encode_str(self.show_start_time) \ + + encode_str(self.show_end_time) + +class ExBonusTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_bonus_table_id = decode_int(data, off) + off += INT_OFF + + self.ex_bonus_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.ex_bonus_condition_id = decode_int(data, off) + off += INT_OFF + + self.condition_value1 = decode_int(data, off) + off += INT_OFF + + self.condition_value2 = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ExBonusTableData": + ret = cls(b"\x00" * 99, 0) + ret.ex_bonus_table_id = data['ExBonusTableId'] + ret.ex_bonus_table_sub_id = data['ExBonusTableSubId'] + ret.ex_bonus_condition_id = data['ExBonusConditionId'] + ret.condition_value1 = data['ConditionValue1'] + ret.condition_value2 = data['ConditionValue2'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_bonus_table_id) \ + + encode_int(self.ex_bonus_table_sub_id) \ + + encode_int(self.ex_bonus_condition_id) \ + + encode_int(self.condition_value1) \ + + encode_int(self.condition_value2) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class ExBonusConditionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ex_bonus_condition_id = decode_int(data, off) + off += INT_OFF + + self.format, new_off = decode_str(data, off) + off += new_off + + self.hud_format, new_off = decode_str(data, off) + off += new_off + + self.format_param_size = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ExBonusConditionData": + ret = cls(b"\x00" * 99, 0) + ret.ex_bonus_condition_id = data['ExBonusConditionId'] + ret.format = data['Format'] + ret.hud_format = data['HudFormat'] + ret.format_param_size = data['FomatParamSize'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ex_bonus_condition_id) \ + + encode_str(self.format) \ + + encode_str(self.hud_format) \ + + encode_int(self.format_param_size) + +class BeginnerMissionProgressesUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_condition_id = decode_int(data, off) + off += INT_OFF + + self.achievement_num = decode_short(data, off) + off += SHORT_OFF + + self.complete_flag = decode_byte(data, off) + off += BYTE_OFF + + self.complete_date, new_off = decode_date_str(data, off) + off += new_off + + self.reward_received_flag = decode_byte(data, off) + off += BYTE_OFF + + self.reward_received_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionProgressesUserData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_condition_id = data['BeginnerMissionConditionId'] + ret.achievement_num = data['AchievementNum'] + ret.complete_flag = data['CompleteFlag'] + ret.complete_date = data['CompleteDate'] + ret.reward_received_flag = data['RewardReceivedFlag'] + ret.reward_received_date = data['RewardReceivedDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_condition_id) \ + + encode_short(self.achievement_num) \ + + encode_byte(self.complete_flag) \ + + encode_date_str(self.complete_date) \ + + encode_byte(self.reward_received_flag) \ + + encode_date_str(self.reward_received_date) + +class BeginnerMissionSeatProgressesUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_seat_condition_id = decode_int(data, off) + off += INT_OFF + + self.achievement_num = decode_short(data, off) + off += SHORT_OFF + + self.complete_flag = decode_byte(data, off) + off += BYTE_OFF + + self.complete_date, new_off = decode_date_str(data, off) + off += new_off + + self.reward_received_flag = decode_byte(data, off) + off += BYTE_OFF + + self.reward_received_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionSeatProgressesUserData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_seat_condition_id = data['BeginnerMissionSeatConditionId'] + ret.achievement_num = data['AchievementNum'] + ret.complete_flag = data['CompleteFlag'] + ret.complete_date = data['CompleteDate'] + ret.reward_received_flag = data['RewardReceivedFlag'] + ret.reward_received_date = data['RewardReceivedDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_seat_condition_id) \ + + encode_short(self.achievement_num) \ + + encode_byte(self.complete_flag) \ + + encode_date_str(self.complete_date) \ + + encode_byte(self.reward_received_flag) \ + + encode_date_str(self.reward_received_date) + +class LinkedSiteRegCampaignUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.reward_received_flag = decode_byte(data, off) + off += BYTE_OFF + + self.reward_received_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "LinkedSiteRegCampaignUserData": + ret = cls(b"\x00" * 99, 0) + ret.reward_received_flag = data['RewardReceivedFlag'] + ret.reward_received_date = data['RewardReceivedDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.reward_received_flag) \ + + encode_date_str(self.reward_received_date) + +class HeroLogUnitUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.hero_log_id = decode_int(data, off) + off += INT_OFF + + self.leader_appointment_normal_card_num = decode_int(data, off) + off += INT_OFF + + self.leader_appointment_holographic_card_num = decode_int(data, off) + off += INT_OFF + + self.skill_slot_max_release_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "HeroLogUnitUserData": + ret = cls(b"\x00" * 13, 0) + ret.hero_log_id = data['HeroLogId'] + ret.leader_appointment_normal_card_num = data['LeaderAppointmentNormalCardNum'] + ret.leader_appointment_holographic_card_num = data['LeaderAppointmentHolographicCardNum'] + ret.skill_slot_max_release_flag = data['SkillSlotMaxReleaseFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.hero_log_id) \ + + encode_int(self.leader_appointment_normal_card_num) \ + + encode_int(self.leader_appointment_holographic_card_num) \ + + encode_byte(self.skill_slot_max_release_flag) + +class CharaUnitUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.reliability_1 = decode_int(data, off) + off += INT_OFF + + self.reliability_2 = decode_int(data, off) + off += INT_OFF + + self.motivation_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "CharaUnitUserData": + ret = cls(b"\x00" * 99, 0) + ret.chara_id = data['CharaId'] + ret.reliability_1 = data['Reliability1'] + ret.reliability_2 = data['Reliability2'] + ret.motivation_flag = data['MotivationFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.chara_id) \ + + encode_int(self.reliability_1) \ + + encode_int(self.reliability_2) \ + + encode_byte(self.motivation_flag) + +class AdventureExecUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.status = decode_byte(data, off) + off += BYTE_OFF + + self.adventure_id = decode_int(data, off) + off += INT_OFF + + self.adventure_difficulty_id = decode_int(data, off) + off += INT_OFF + + self.last_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.remaining_sec_to_complete = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AdventureExecUserData": + ret = cls(b"\x00" * 99, 0) + ret.status = data['Status'] + ret.adventure_id = data['AdventureId'] + ret.adventure_difficulty_id = data['AdventureDifficultyId'] + ret.last_start_date = data['LastStartDate'] + ret.remaining_sec_to_complete = data['RemainingSecToComplete'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.status) \ + + encode_int(self.adventure_id) \ + + encode_int(self.adventure_difficulty_id) \ + + encode_date_str(self.last_start_date) \ + + encode_int(self.remaining_sec_to_complete) + +class QuestRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_rare_drop_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.strength_min = decode_int(data, off) + off += INT_OFF + + self.strength_max = decode_int(data, off) + off += INT_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.drop_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestRareDropData": + ret = cls(b"\x00" * 99, 0) + ret.quest_rare_drop_id = data['QuestRareDropId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.unit_id = data['UnitId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength_min = data['StrengthMin'] + ret.strength_max = data['StrengthMax'] + ret.property_table_sub_id = data['PropertyTableSubId'] + ret.drop_rate = data['DropRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_rare_drop_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.strength_min) \ + + encode_int(self.strength_max) \ + + encode_int(self.property_table_sub_id) \ + + encode_str(self.drop_rate) + +class QuestSpecialRareDropSettingData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_special_rare_drop_setting_id = decode_int(data, off) + off += INT_OFF + + self.tower_type = decode_byte(data, off) + off += BYTE_OFF + + self.start_trial_tower_id = decode_short(data, off) + off += SHORT_OFF + + self.end_trial_tower_id = decode_short(data, off) + off += SHORT_OFF + + self.rare_drop_upward_rate, new_off = decode_str(data, off) + off += new_off + + self.quest_special_rare_drop_sub_id = decode_int(data, off) + off += INT_OFF + + self.generic_campaign_period_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSpecialRareDropSettingData": + ret = cls(b"\x00" * 99, 0) + ret.quest_special_rare_drop_setting_id = data['QuestSpecialRareDropSettingId'] + ret.tower_type = data['TowerType'] + ret.start_trial_tower_id = data['StartTrialTowerId'] + ret.end_trial_tower_id = data['EndTrialTowerId'] + ret.rare_drop_upward_rate = data['RareDropUpwardRate'] + ret.quest_special_rare_drop_sub_id = data['QuestSpecialRareDropSubId'] + ret.generic_campaign_period_id = data['GenericCampaignPeriodId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_special_rare_drop_setting_id) \ + + encode_byte(self.tower_type) \ + + encode_short(self.start_trial_tower_id) \ + + encode_short(self.end_trial_tower_id) \ + + encode_str(self.rare_drop_upward_rate) \ + + encode_int(self.quest_special_rare_drop_sub_id) \ + + encode_int(self.generic_campaign_period_id) + +class QuestSpecialRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_special_rare_drop_id = decode_int(data, off) + off += INT_OFF + + self.quest_special_rare_drop_sub_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.strength_min = decode_int(data, off) + off += INT_OFF + + self.strength_max = decode_int(data, off) + off += INT_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.drop_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestSpecialRareDropData": + ret = cls(b"\x00" * 99, 0) + ret.quest_special_rare_drop_id = data['QuestSpecialRareDropId'] + ret.quest_special_rare_drop_sub_id = data['QuestSpecialRareDropSubId'] + ret.unit_id = data['UnitId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength_min = data['StrengthMin'] + ret.strength_max = data['StrengthMax'] + ret.property_table_sub_id = data['PropertyTableSubId'] + ret.drop_rate = data['DropRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_special_rare_drop_id) \ + + encode_int(self.quest_special_rare_drop_sub_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.strength_min) \ + + encode_int(self.strength_max) \ + + encode_int(self.property_table_sub_id) \ + + encode_str(self.drop_rate) + +class QuestTutorialData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_tutorial_id = decode_short(data, off) + off += SHORT_OFF + + self.trigger = decode_int(data, off) + off += INT_OFF + + self.trigger_delay, new_off = decode_str(data, off) + off += new_off + + self.trigger_during, new_off = decode_str(data, off) + off += new_off + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.unit_range, new_off = decode_str(data, off) + off += new_off + + self.comment_text, new_off = decode_str(data, off) + off += new_off + + self.voice_id, new_off = decode_str(data, off) + off += new_off + + self.comment_pos, new_off = decode_str(data, off) + off += new_off + + self.yui_face, new_off = decode_str(data, off) + off += new_off + + self.arrow_direct, new_off = decode_str(data, off) + off += new_off + + self.arrow_position, new_off = decode_str(data, off) + off += new_off + + self.image_plus, new_off = decode_str(data, off) + off += new_off + + self.image_plus_pos, new_off = decode_str(data, off) + off += new_off + + self.stick_info, new_off = decode_str(data, off) + off += new_off + + self.mark_effect, new_off = decode_str(data, off) + off += new_off + + self.mark_pos, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestTutorialData": + ret = cls(b"\x00" * 99, 0) + ret.quest_tutorial_id = data['Id'] + ret.trigger = data['Trigger'] + ret.trigger_delay = data['TriggerDelay'] + ret.trigger_during = data['TriggerDuring'] + ret.unit_id = data['UnitId'] + ret.unit_range = data['UnitRange'] + ret.comment_text = data['CommentText'] + ret.voice_id = data['VoiceID'] + ret.comment_pos = data['CommentPos'] + ret.yui_face = data['YuiFace'] + ret.arrow_direct = data['ArrowDirect'] + ret.arrow_position = data['ArrowPosition'] + ret.image_plus = data['ImagePlus'] + ret.image_plus_pos = data['ImagePlusPos'] + ret.stick_info = data['StickInfo'] + ret.mark_effect = data['MarkEffect'] + ret.mark_pos = data['MarkPos'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.quest_tutorial_id) \ + + encode_int(self.trigger) \ + + encode_str(self.trigger_delay) \ + + encode_str(self.trigger_during) \ + + encode_int(self.unit_id) \ + + encode_str(self.unit_range) \ + + encode_str(self.comment_text) \ + + encode_str(self.voice_id) \ + + encode_str(self.comment_pos) \ + + encode_str(self.yui_face) \ + + encode_str(self.arrow_direct) \ + + encode_str(self.arrow_position) \ + + encode_str(self.image_plus) \ + + encode_str(self.image_plus_pos) \ + + encode_str(self.stick_info) \ + + encode_str(self.mark_effect) \ + + encode_str(self.mark_pos) + +class QuestPlayerTraceTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.player_trace_table_id = decode_short(data, off) + off += SHORT_OFF + + self.player_trace_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.rate = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestPlayerTraceTableData": + ret = cls(b"\x00" * 99, 0) + ret.player_trace_table_id = data['PlayerTraceTableId'] + ret.player_trace_table_sub_id = data['PlayerTraceTableSubId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.rate = data['Rate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.player_trace_table_id) \ + + encode_int(self.player_trace_table_sub_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.rate) + +class QuestStillData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.still_id = decode_int(data, off) + off += INT_OFF + + self.file_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestStillData": + ret = cls(b"\x00" * 99, 0) + ret.still_id = data['StillId'] + ret.file_name = data['FileName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.still_id) \ + + encode_str(self.file_name) + +class GashaData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.description, new_off = decode_str(data, off) + off += new_off + + self.gasha_type = decode_byte(data, off) + off += BYTE_OFF + + self.open_type = decode_byte(data, off) + off += BYTE_OFF + + self.free_target_flag = decode_byte(data, off) + off += BYTE_OFF + + self.reset_hour_list, new_off = decode_str(data, off) + off += new_off + + self.limit_num = decode_short(data, off) + off += SHORT_OFF + + self.open_days = decode_short(data, off) + off += SHORT_OFF + + self.sort_num = decode_int(data, off) + off += INT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_id = data['GashaId'] + ret.name = data['Name'] + ret.description = data['Description'] + ret.gasha_type = data['GashaType'] + ret.open_type = data['OpenType'] + ret.free_target_flag = data['FreeTargetFlag'] + ret.reset_hour_list = data['ResetHourList'] + ret.limit_num = data['LimitNum'] + ret.open_days = data['OpenDays'] + ret.sort_num = data['SortNum'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_id) \ + + encode_str(self.name) \ + + encode_str(self.description) \ + + encode_byte(self.gasha_type) \ + + encode_byte(self.open_type) \ + + encode_byte(self.free_target_flag) \ + + encode_str(self.reset_hour_list) \ + + encode_short(self.limit_num) \ + + encode_short(self.open_days) \ + + encode_int(self.sort_num) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class GashaHeaderData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_header_id = decode_int(data, off) + off += INT_OFF + + self.gasha_id = decode_int(data, off) + off += INT_OFF + + self.step_no = decode_byte(data, off) + off += BYTE_OFF + + self.step_progress_flag = decode_byte(data, off) + off += BYTE_OFF + + self.platform = decode_byte(data, off) + off += BYTE_OFF + + self.use_type_1 = decode_byte(data, off) + off += BYTE_OFF + + self.use_type_2 = decode_byte(data, off) + off += BYTE_OFF + + self.provision_num = decode_byte(data, off) + off += BYTE_OFF + + self.premium_lottery_num = decode_byte(data, off) + off += BYTE_OFF + + self.premium_lottery_executable_num = decode_byte(data, off) + off += BYTE_OFF + + self.use_type_1_first_use_num = decode_int(data, off) + off += INT_OFF + + self.use_type_1_base_use_num = decode_int(data, off) + off += INT_OFF + + self.use_type_1_extra_use_num = decode_int(data, off) + off += INT_OFF + + self.use_type_2_first_use_num = decode_int(data, off) + off += INT_OFF + + self.use_type_2_base_use_num = decode_int(data, off) + off += INT_OFF + + self.use_type_2_extra_use_num = decode_int(data, off) + off += INT_OFF + + self.yui_chance_log_open_rate, new_off = decode_str(data, off) + off += new_off + + self.yui_chance_rarity_up_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaHeaderData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_header_id = data['GashaHeaderId'] + ret.gasha_id = data['GashaId'] + ret.step_no = data['StepNo'] + ret.step_progress_flag = data['StepProgressFlag'] + ret.platform = data['Platform'] + ret.use_type_1 = data['UseType1'] + ret.use_type_2 = data['UseType2'] + ret.provision_num = data['ProvisionNum'] + ret.premium_lottery_num = data['PremiumLotteryNum'] + ret.premium_lottery_executable_num = data['PremiumLotteryExecutableNum'] + ret.use_type_1_first_use_num = data['UseType1FirstUseNum'] + ret.use_type_1_base_use_num = data['UseType1BaseUseNum'] + ret.use_type_1_extra_use_num = data['UseType1ExtraUseNum'] + ret.use_type_2_first_use_num = data['UseType2FirstUseNum'] + ret.use_type_2_base_use_num = data['UseType2BaseUseNum'] + ret.use_type_2_extra_use_num = data['UseType2ExtraUseNum'] + ret.yui_chance_log_open_rate = data['YuiChanceLogOpenRate'] + ret.yui_chance_rarity_up_rate = data['YuiChanceRarityUpRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_header_id) \ + + encode_int(self.gasha_id) \ + + encode_byte(self.step_no) \ + + encode_byte(self.step_progress_flag) \ + + encode_byte(self.platform) \ + + encode_byte(self.use_type_1) \ + + encode_byte(self.use_type_2) \ + + encode_byte(self.provision_num) \ + + encode_byte(self.premium_lottery_num) \ + + encode_byte(self.premium_lottery_executable_num) \ + + encode_int(self.use_type_1_first_use_num) \ + + encode_int(self.use_type_1_base_use_num) \ + + encode_int(self.use_type_1_extra_use_num) \ + + encode_int(self.use_type_2_first_use_num) \ + + encode_int(self.use_type_2_base_use_num) \ + + encode_int(self.use_type_2_extra_use_num) \ + + encode_str(self.yui_chance_log_open_rate) \ + + encode_str(self.yui_chance_rarity_up_rate) + +class GashaLotteryRarityData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_lottery_rarity_id = decode_int(data, off) + off += INT_OFF + + self.gasha_id = decode_int(data, off) + off += INT_OFF + + self.gasha_header_id = decode_int(data, off) + off += INT_OFF + + self.lottery_rarity_type = decode_byte(data, off) + off += BYTE_OFF + + self.lottery_rarity_rate, new_off = decode_str(data, off) + off += new_off + + self.premium_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaLotteryRarityData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_lottery_rarity_id = data['GashaLotteryRarityId'] + ret.gasha_id = data['GashaId'] + ret.gasha_header_id = data['GashaHeaderId'] + ret.lottery_rarity_type = data['LotteryRarityType'] + ret.lottery_rarity_rate = data['LotteryRarityRate'] + ret.premium_rate = data['PremiumRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_lottery_rarity_id) \ + + encode_int(self.gasha_id) \ + + encode_int(self.gasha_header_id) \ + + encode_byte(self.lottery_rarity_type) \ + + encode_str(self.lottery_rarity_rate) \ + + encode_str(self.premium_rate) + +class GashaPrizeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_prize_id = decode_int(data, off) + off += INT_OFF + + self.gasha_id = decode_int(data, off) + off += INT_OFF + + self.lottery_rarity_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.unanalyzed_log_grade_id = decode_int(data, off) + off += INT_OFF + + self.correction_rate = decode_short(data, off) + off += SHORT_OFF + + self.pickup_flag = decode_byte(data, off) + off += BYTE_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaPrizeData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_prize_id = data['GashaPrizeId'] + ret.gasha_id = data['GashaId'] + ret.lottery_rarity_type = data['LotteryRarityType'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.unanalyzed_log_grade_id = data['UnanalyzedLogGradeId'] + ret.correction_rate = data['CorrectionRate'] + ret.pickup_flag = data['PickupFlag'] + ret.property_table_sub_id = data['PropertyTableSubId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_prize_id) \ + + encode_int(self.gasha_id) \ + + encode_byte(self.lottery_rarity_type) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.unanalyzed_log_grade_id) \ + + encode_short(self.correction_rate) \ + + encode_byte(self.pickup_flag) \ + + encode_int(self.property_table_sub_id) + +class ComebackEventData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.comeback_event_id = decode_int(data, off) + off += INT_OFF + + self.comeback_event_sub_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.reward_set_sub_id = decode_int(data, off) + off += INT_OFF + + self.require_days = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ComebackEventData": + ret = cls(b"\x00" * 99, 0) + ret.comeback_event_id = data['ComebackEventId'] + ret.comeback_event_sub_id = data['ComebackEventSubId'] + ret.display_name = data['DisplayName'] + ret.reward_set_sub_id = data['RewardSetSubId'] + ret.require_days = data['RequireDays'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.comeback_event_id) \ + + encode_int(self.comeback_event_sub_id) \ + + encode_str(self.display_name) \ + + encode_int(self.reward_set_sub_id) \ + + encode_short(self.require_days) + +class AdBannerData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self.category = decode_byte(data, off) + off += BYTE_OFF + + self.sort_num = decode_int(data, off) + off += INT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self.active_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AdBannerData": + ret = cls(b"\x00" * 99, 0) + ret.ad_banner_id = data['AdBannerId'] + ret.category = data['Category'] + ret.sort_num = data['SortNum'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + ret.active_flag = data['ActiveFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ad_banner_id) \ + + encode_byte(self.category) \ + + encode_int(self.sort_num) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) \ + + encode_byte(self.active_flag) + +class EventsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.event_id = decode_int(data, off) + off += INT_OFF + + self.event_type = decode_byte(data, off) + off += BYTE_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.param_1, new_off = decode_str(data, off) + off += new_off + + self.param_2, new_off = decode_str(data, off) + off += new_off + + self.param_3, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.chat_open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.chat_open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.news_id = decode_int(data, off) + off += INT_OFF + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EventsData": + ret = cls(b"\x00" * 99, 0) + ret.event_id = data['EventId'] + ret.event_type = data['EventType'] + ret.title = data['Title'] + ret.param_1 = data['Param1'] + ret.param_2 = data['Param2'] + ret.param_3 = data['Param3'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.posting_start_date = data['PostingStartDate'] + ret.posting_end_date = data['PostingEndDate'] + ret.chat_open_start_date = data['ChatOpenStartDate'] + ret.chat_open_end_date = data['ChatOpenEndDate'] + ret.news_id = data['NewId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.event_id) \ + + encode_byte(self.event_type) \ + + encode_str(self.title) \ + + encode_str(self.param_1) \ + + encode_str(self.param_2) \ + + encode_str(self.param_3) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_date_str(self.posting_start_date) \ + + encode_date_str(self.posting_end_date) \ + + encode_date_str(self.chat_open_start_date) \ + + encode_date_str(self.chat_open_end_date) \ + + encode_int(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class TreasureHuntsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntsData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.title = data['Title'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_id) \ + + encode_str(self.title) + +class TreasureHuntWholeTasksData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_whole_task_id = decode_int(data, off) + off += INT_OFF + + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.task_type = decode_byte(data, off) + off += BYTE_OFF + + self.achievement_possible_num = decode_short(data, off) + off += SHORT_OFF + + self.get_event_point = decode_int(data, off) + off += INT_OFF + + self.condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.condition_value_accumulation_flag = decode_byte(data, off) + off += BYTE_OFF + + self.condition_value_1, new_off = decode_str(data, off) + off += new_off + + self.condition_value_2, new_off = decode_str(data, off) + off += new_off + + self.condition_value_3, new_off = decode_str(data, off) + off += new_off + + self.condition_value_4, new_off = decode_str(data, off) + off += new_off + + self.condition_value_5, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntWholeTasksData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_whole_task_id = data['TreasureHuntWholeTaskId'] + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.task_type = data['TaskType'] + ret.achievement_possible_num = data['AchievementPossibleNum'] + ret.get_event_point = data['GetEventPoint'] + ret.condition_type = data['ConditionType'] + ret.condition_value_accumulation_flag = data['ConditionValueAccumulationFlag'] + ret.condition_value_1 = data['ConditionValue1'] + ret.condition_value_2 = data['ConditionValue2'] + ret.condition_value_3 = data['ConditionValue3'] + ret.condition_value_4 = data['ConditionValue4'] + ret.condition_value_5 = data['ConditionValue5'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_whole_task_id) \ + + encode_int(self.treasure_hunt_id) \ + + encode_byte(self.task_type) \ + + encode_short(self.achievement_possible_num) \ + + encode_int(self.get_event_point) \ + + encode_byte(self.condition_type) \ + + encode_byte(self.condition_value_accumulation_flag) \ + + encode_str(self.condition_value_1) \ + + encode_str(self.condition_value_2) \ + + encode_str(self.condition_value_3) \ + + encode_str(self.condition_value_4) \ + + encode_str(self.condition_value_5) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) + +class TreasureHuntIndividualTasksData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_individual_task_id = decode_int(data, off) + off += INT_OFF + + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.task_type = decode_byte(data, off) + off += BYTE_OFF + + self.quest_scene_id = decode_short(data, off) + off += SHORT_OFF + + self.achievement_possible_num = decode_short(data, off) + off += SHORT_OFF + + self.get_event_point = decode_int(data, off) + off += INT_OFF + + self.condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.condition_value_accumulation_flag = decode_byte(data, off) + off += BYTE_OFF + + self.condition_value_1, new_off = decode_str(data, off) + off += new_off + + self.condition_value_2, new_off = decode_str(data, off) + off += new_off + + self.condition_value_3, new_off = decode_str(data, off) + off += new_off + + self.condition_value_4, new_off = decode_str(data, off) + off += new_off + + self.condition_value_5, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntIndividualTasksData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_individual_task_id = data['TreasureHuntIndividualTaskId'] + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.task_type = data['TaskType'] + ret.quest_scene_id = data['QuestSceneId'] + ret.achievement_possible_num = data['AchievementPossibleNum'] + ret.get_event_point = data['GetEventPoint'] + ret.condition_type = data['ConditionType'] + ret.condition_value_accumulation_flag = data['ConditionValueAccumulationFlag'] + ret.condition_value_1 = data['ConditionValue1'] + ret.condition_value_2 = data['ConditionValue2'] + ret.condition_value_3 = data['ConditionValue3'] + ret.condition_value_4 = data['ConditionValue4'] + ret.condition_value_5 = data['ConditionValue5'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_individual_task_id) \ + + encode_int(self.treasure_hunt_id) \ + + encode_byte(self.task_type) \ + + encode_short(self.quest_scene_id) \ + + encode_short(self.achievement_possible_num) \ + + encode_int(self.get_event_point) \ + + encode_byte(self.condition_type) \ + + encode_byte(self.condition_value_accumulation_flag) \ + + encode_str(self.condition_value_1) \ + + encode_str(self.condition_value_2) \ + + encode_str(self.condition_value_3) \ + + encode_str(self.condition_value_4) \ + + encode_str(self.condition_value_5) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) + +class TreasureHuntSpecialEffectsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_special_effect_id = decode_int(data, off) + off += INT_OFF + + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.special_effect_type = decode_byte(data, off) + off += BYTE_OFF + + self.special_effect_coefficient, new_off = decode_str(data, off) + off += new_off + + self.target_common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.target_common_reward_id = decode_int(data, off) + off += INT_OFF + + self.obtaining_location_type = decode_byte(data, off) + off += BYTE_OFF + + self.obtaining_location_param, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntSpecialEffectsData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_special_effect_id = data['TreasureHuntSpecialEffectId'] + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.special_effect_type = data['SpecialEffectType'] + ret.special_effect_coefficient = data['SpecialEffectCoefficient'] + ret.target_common_reward_type = data['TargetCommonRewardType'] + ret.target_common_reward_id = data['TargetCommonRewardId'] + ret.obtaining_location_type = data['ObtainingLocationType'] + ret.obtaining_location_param = data['ObtainingLocationParam'] + ret.open_start_date = data['OpenStartDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_special_effect_id) \ + + encode_int(self.treasure_hunt_id) \ + + encode_byte(self.special_effect_type) \ + + encode_str(self.special_effect_coefficient) \ + + encode_byte(self.target_common_reward_type) \ + + encode_int(self.target_common_reward_id) \ + + encode_byte(self.obtaining_location_type) \ + + encode_str(self.obtaining_location_param) \ + + encode_date_str(self.open_start_date) + +class TreasureHuntEventPointRewardCommonRewardsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_event_point_reward_common_reward_id = decode_int(data, off) + off += INT_OFF + + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.need_event_point = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntEventPointRewardCommonRewardsData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_event_point_reward_common_reward_id = data['TreasureHuntEventPointRewardCommonRewardId'] + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.need_event_point = data['NeedEventPoint'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_event_point_reward_common_reward_id) \ + + encode_int(self.treasure_hunt_id) \ + + encode_int(self.need_event_point) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class TreasureHuntEventPointRewardTitlesData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_event_point_reward_title_id = decode_int(data, off) + off += INT_OFF + + self.treasure_hunt_id = decode_int(data, off) + off += INT_OFF + + self.need_event_point = decode_int(data, off) + off += INT_OFF + + self.title_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntEventPointRewardTitlesData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_event_point_reward_title_id = data['TreasureHuntEventPointRewardTitleId'] + ret.treasure_hunt_id = data['TreasureHuntId'] + ret.need_event_point = data['NeedEventPoint'] + ret.title_id = data['TitleId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_event_point_reward_title_id) \ + + encode_int(self.treasure_hunt_id) \ + + encode_int(self.need_event_point) \ + + encode_int(self.title_id) + +class TreasureHuntTaskTextData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.treasure_hunt_task_text_id = decode_int(data, off) + off += INT_OFF + + self.task_type = decode_byte(data, off) + off += BYTE_OFF + + self.condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.format, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "TreasureHuntTaskTextData": + ret = cls(b"\x00" * 99, 0) + ret.treasure_hunt_task_text_id = data['TreasureHuntTaskTextId'] + ret.task_type = data['TaskType'] + ret.condition_type = data['ConditionType'] + ret.format = data['Format'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.treasure_hunt_task_text_id) \ + + encode_byte(self.task_type) \ + + encode_byte(self.condition_type) \ + + encode_str(self.format) + +class BnidSerialCodeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.bnid_item_id, new_off = decode_str(data, off) + off += new_off + + self.serial_code_type = decode_byte(data, off) + off += BYTE_OFF + + self.category = decode_byte(data, off) + off += BYTE_OFF + + self.description, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BnidSerialCodeData": + ret = cls(b"\x00" * 99, 0) + ret.bnid_item_id = data['BnidItemId'] + ret.serial_code_type = data['SerialCodeType'] + ret.category = data['Category'] + ret.description = data['Description'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.bnid_item_id) \ + + encode_byte(self.serial_code_type) \ + + encode_byte(self.category) \ + + encode_str(self.description) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) + +class BnidSerialCodeRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.bnid_item_id, new_off = decode_str(data, off) + off += new_off + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BnidSerialCodeRewardData": + ret = cls(b"\x00" * 99, 0) + ret.bnid_item_id = data['BnidItemId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.bnid_item_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class SupportLogData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.support_log_id = decode_int(data, off) + off += INT_OFF + + self.chara_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.rarity = decode_byte(data, off) + off += BYTE_OFF + + self.support_log_type_id = decode_short(data, off) + off += SHORT_OFF + + self.sale_price = decode_int(data, off) + off += INT_OFF + + self.composition_exp = decode_int(data, off) + off += INT_OFF + + self.awakening_exp = decode_int(data, off) + off += INT_OFF + + self.use_rank = decode_short(data, off) + off += SHORT_OFF + + self.group_no = decode_short(data, off) + off += SHORT_OFF + + self.adaptable_appear_append = decode_short(data, off) + off += SHORT_OFF + + self.punisher_appear_append = decode_short(data, off) + off += SHORT_OFF + + self.state, new_off = decode_str(data, off) + off += new_off + + self.passive_state, new_off = decode_str(data, off) + off += new_off + + self.power_default_value = decode_int(data, off) + off += INT_OFF + + self.clt_default_value = decode_int(data, off) + off += INT_OFF + + self.awakening_1_power = decode_int(data, off) + off += INT_OFF + + self.awakening_1_clt = decode_int(data, off) + off += INT_OFF + + self.awakening_2_power = decode_int(data, off) + off += INT_OFF + + self.awakening_2_clt = decode_int(data, off) + off += INT_OFF + + self.awakening_3_power = decode_int(data, off) + off += INT_OFF + + self.awakening_3_clt = decode_int(data, off) + off += INT_OFF + + self.awakening_4_power = decode_int(data, off) + off += INT_OFF + + self.awakening_4_clt = decode_int(data, off) + off += INT_OFF + + self.awakening_5_power = decode_int(data, off) + off += INT_OFF + + self.awakening_5_clt = decode_int(data, off) + off += INT_OFF + + self.normal_leader = decode_int(data, off) + off += INT_OFF + + self.holo_leader = decode_int(data, off) + off += INT_OFF + + self.ui_display_power_title, new_off = decode_str(data, off) + off += new_off + + self.ui_display_power_content, new_off = decode_str(data, off) + off += new_off + + self.prefab, new_off = decode_str(data, off) + off += new_off + + self.cutin_mode, new_off = decode_str(data, off) + off += new_off + + self.skill_voice_id, new_off = decode_str(data, off) + off += new_off + + self.skill_name, new_off = decode_str(data, off) + off += new_off + + self.name_in_skill_cutin, new_off = decode_str(data, off) + off += new_off + + self.skill_text, new_off = decode_str(data, off) + off += new_off + + self.skill_text_in_skill_cutin, new_off = decode_str(data, off) + off += new_off + + self.chara_info, new_off = decode_str(data, off) + off += new_off + + self.cutin_image, new_off = decode_str(data, off) + off += new_off + + self.cutin_image_awake, new_off = decode_str(data, off) + off += new_off + + self.status_chara_icon, new_off = decode_str(data, off) + off += new_off + + self.status_chara_icon_awake, new_off = decode_str(data, off) + off += new_off + + self.collection_display_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.collection_empty_frame_display_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SupportLogData": + ret = cls(b"\x00" * 99, 0) + ret.support_log_id = data['SupportLogId'] + ret.chara_id = data['CharaId'] + ret.name = data['Name'] + ret.rarity = data['Rarity'] + ret.support_log_type_id = data['SupportLogTypeId'] + ret.sale_price = data['SalePrice'] + ret.composition_exp = data['CompositionExp'] + ret.awakening_exp = data['AwakeningExp'] + ret.use_rank = data['UseRank'] + ret.group_no = data['GroupNo'] + ret.adaptable_appear_append = data['AdaptableAppearAppend'] + ret.punisher_appear_append = data['PunisherAppearAppend'] + ret.state = data['State'] + ret.passive_state = data['PassiveState'] + ret.power_default_value = data['PowerDefaultValue'] + ret.clt_default_value = data['CltDefaultValue'] + ret.awakening_1_power = data['Awakening1Power'] + ret.awakening_1_clt = data['Awakening1Clt'] + ret.awakening_2_power = data['Awakening2Power'] + ret.awakening_2_clt = data['Awakening2Clt'] + ret.awakening_3_power = data['Awakening3Power'] + ret.awakening_3_clt = data['Awakening3Clt'] + ret.awakening_4_power = data['Awakening4Power'] + ret.awakening_4_clt = data['Awakening4Clt'] + ret.awakening_5_power = data['Awakening5Power'] + ret.awakening_5_clt = data['Awakening5Clt'] + ret.normal_leader = data['NormalLeader'] + ret.holo_leader = data['HoloLeader'] + ret.ui_display_power_title = data['UiDisplayPowerTitle'] + ret.ui_display_power_content = data['UiDisplayPowerContent'] + ret.prefab = data['Prefab'] + ret.cutin_mode = data['CutinMode'] + ret.skill_voice_id = data['SkillVoiceId'] + ret.skill_name = data['SkillName'] + ret.name_in_skill_cutin = data['NameInSkillCutin'] + ret.skill_text = data['SkillText'] + ret.skill_text_in_skill_cutin = data['SkillTextInSkillCutin'] + ret.chara_info = data['CharaInfo'] + ret.cutin_image = data['CutinImage'] + ret.cutin_image_awake = data['CutinImageAwake'] + ret.status_chara_icon = data['StatusCharaIcon'] + ret.status_chara_icon_awake = data['StatusCharaIconAwake'] + ret.collection_display_start_date = data['CollectionDisplayStartDate'] + ret.collection_empty_frame_display_flag = data['CollectionEmptyFrameDisplayFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.support_log_id) \ + + encode_short(self.chara_id) \ + + encode_str(self.name) \ + + encode_byte(self.rarity) \ + + encode_short(self.support_log_type_id) \ + + encode_int(self.sale_price) \ + + encode_int(self.composition_exp) \ + + encode_int(self.awakening_exp) \ + + encode_short(self.use_rank) \ + + encode_short(self.group_no) \ + + encode_short(self.adaptable_appear_append) \ + + encode_short(self.punisher_appear_append) \ + + encode_str(self.state) \ + + encode_str(self.passive_state) \ + + encode_int(self.power_default_value) \ + + encode_int(self.clt_default_value) \ + + encode_int(self.awakening_1_power) \ + + encode_int(self.awakening_1_clt) \ + + encode_int(self.awakening_2_power) \ + + encode_int(self.awakening_2_clt) \ + + encode_int(self.awakening_3_power) \ + + encode_int(self.awakening_3_clt) \ + + encode_int(self.awakening_4_power) \ + + encode_int(self.awakening_4_clt) \ + + encode_int(self.awakening_5_power) \ + + encode_int(self.awakening_5_clt) \ + + encode_int(self.normal_leader) \ + + encode_int(self.holo_leader) \ + + encode_str(self.ui_display_power_title) \ + + encode_str(self.ui_display_power_content) \ + + encode_str(self.prefab) \ + + encode_str(self.cutin_mode) \ + + encode_str(self.skill_voice_id) \ + + encode_str(self.skill_name) \ + + encode_str(self.name_in_skill_cutin) \ + + encode_str(self.skill_text) \ + + encode_str(self.skill_text_in_skill_cutin) \ + + encode_str(self.chara_info) \ + + encode_str(self.cutin_image) \ + + encode_str(self.cutin_image_awake) \ + + encode_str(self.status_chara_icon) \ + + encode_str(self.status_chara_icon_awake) \ + + encode_date_str(self.collection_display_start_date) \ + + encode_byte(self.collection_empty_frame_display_flag) + +class SupportLogTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.support_log_type_id = decode_short(data, off) + off += SHORT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "SupportLogTypeData": + ret = cls(b"\x00" * 99, 0) + ret.support_log_type_id = data['SupportLogTypeId'] + ret.name = data['Name'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_short(self.support_log_type_id) \ + + encode_str(self.name) + +class EpisodeAppendData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.episode_append_id = decode_int(data, off) + off += INT_OFF + + self.episode_append_type = decode_byte(data, off) + off += BYTE_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.sale_possible_flag = decode_int(data, off) + off += INT_OFF + + self.sale_price = decode_int(data, off) + off += INT_OFF + + self.episode_append_icon, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EpisodeAppendData": + ret = cls(b"\x00" * 99, 0) + ret.episode_append_id = data['EpisodeAppendId'] + ret.episode_append_type = data['EpisodeAppendType'] + ret.name = data['Name'] + ret.flavor_text = data['FlavorText'] + ret.sale_possible_flag = data['SalePossibleFlag'] + ret.sale_price = data['SalePrice'] + ret.episode_append_icon = data['EpisodeAppendIcon'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.episode_append_id) \ + + encode_byte(self.episode_append_type) \ + + encode_str(self.name) \ + + encode_str(self.flavor_text) \ + + encode_int(self.sale_possible_flag) \ + + encode_int(self.sale_price) \ + + encode_str(self.episode_append_icon) + +class QuestDefragMatchQuestData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_quest_id = decode_int(data, off) + off += INT_OFF + + self.quest_scene_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_quest_boss_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.display_name, new_off = decode_str(data, off) + off += new_off + + self.map_no = decode_int(data, off) + off += INT_OFF + + self.unit_data1, new_off = decode_str(data, off) + off += new_off + + self.unit_data2, new_off = decode_str(data, off) + off += new_off + + self.unit_data3, new_off = decode_str(data, off) + off += new_off + + self.adaptable_rate, new_off = decode_str(data, off) + off += new_off + + self.punisher_flag = decode_byte(data, off) + off += BYTE_OFF + + self.comment_details, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestDefragMatchQuestData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_quest_id = data['DefragMatchQuestId'] + ret.quest_scene_id = data['QuestSceneId'] + ret.defrag_match_quest_boss_table_sub_id = data['DefragMatchBossTableSubId'] + ret.display_name = data['DisplayName'] + ret.map_no = data['VsModeNo'] + ret.unit_data1 = data['UnitData1'] + ret.unit_data2 = data['UnitData2'] + ret.unit_data3 = data['UnitData3'] + ret.adaptable_rate = data['AdaptableRate'] + ret.punisher_flag = data['PunisherFlag'] + ret.comment_details = data['CommentDetails'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_quest_id) \ + + encode_int(self.quest_scene_id) \ + + encode_int(self.defrag_match_quest_boss_table_sub_id) \ + + encode_str(self.display_name) \ + + encode_int(self.map_no) \ + + encode_str(self.unit_data1) \ + + encode_str(self.unit_data2) \ + + encode_str(self.unit_data3) \ + + encode_str(self.adaptable_rate) \ + + encode_byte(self.punisher_flag) \ + + encode_str(self.comment_details) + +class QuestDefragMatchQuestBossTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_quest_boss_table_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_quest_boss_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.wave = decode_int(data, off) + off += INT_OFF + + self.type = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.rate = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestDefragMatchQuestBossTableData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_quest_boss_table_id = data['DefragMatchBossTableId'] + ret.defrag_match_quest_boss_table_sub_id = data['DefragMatchBossTableSubId'] + ret.wave = data['Wave'] + ret.type = data['Type'] + ret.unit_id = data['UnitId'] + ret.rate = data['Rate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_quest_boss_table_id) \ + + encode_int(self.defrag_match_quest_boss_table_sub_id) \ + + encode_int(self.wave) \ + + encode_int(self.type) \ + + encode_int(self.unit_id) \ + + encode_int(self.rate) + +class DefragMatchData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.season_no = decode_short(data, off) + off += SHORT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.first_attack_bonus_coefficient, new_off = decode_str(data, off) + off += new_off + + self.last_attack_bonus_coefficient, new_off = decode_str(data, off) + off += new_off + + self.memo, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_id = data['DefragMatchId'] + ret.season_no = data['SeasonNo'] + ret.title = data['Title'] + ret.first_attack_bonus_coefficient = data['FirstAttackBonusCoefficient'] + ret.last_attack_bonus_coefficient = data['LastAttackBonusCoefficient'] + ret.memo = data['Memo'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_id) \ + + encode_short(self.season_no) \ + + encode_str(self.title) \ + + encode_str(self.first_attack_bonus_coefficient) \ + + encode_str(self.last_attack_bonus_coefficient) \ + + encode_str(self.memo) + +class DefragMatchSeedData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_seed_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.check_type = decode_byte(data, off) + off += BYTE_OFF + + self.need_class_num = decode_short(data, off) + off += SHORT_OFF + + self.need_cleared_trial_tower_id = decode_short(data, off) + off += SHORT_OFF + + self.get_league_point = decode_int(data, off) + off += INT_OFF + + self.get_league_score = decode_short(data, off) + off += SHORT_OFF + + self.set_class_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchSeedData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_seed_id = data['DefragMatchSeedId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.check_type = data['CheckType'] + ret.need_class_num = data['NeedClassNum'] + ret.need_cleared_trial_tower_id = data['NeedClearedTrialTowerId'] + ret.get_league_point = data['GetLeaguePoint'] + ret.get_league_score = data['GetLeagueScore'] + ret.set_class_num = data['SetClassNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_seed_id) \ + + encode_int(self.defrag_match_id) \ + + encode_byte(self.check_type) \ + + encode_short(self.need_class_num) \ + + encode_short(self.need_cleared_trial_tower_id) \ + + encode_int(self.get_league_point) \ + + encode_short(self.get_league_score) \ + + encode_short(self.set_class_num) + +class DefragMatchSpecialEffectData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_special_effec_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.special_effect_type = decode_byte(data, off) + off += BYTE_OFF + + self.special_effect_coefficient, new_off = decode_str(data, off) + off += new_off + + self.target_chara_id = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchSpecialEffectData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_special_effec_id = data['DefragMatchSpecialEffectsId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.special_effect_type = data['SpecialEffectType'] + ret.special_effect_coefficient = data['SpecialEffectCoefficient'] + ret.target_chara_id = data['TargetCharaId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_special_effec_id) \ + + encode_int(self.defrag_match_id) \ + + encode_byte(self.special_effect_type) \ + + encode_str(self.special_effect_coefficient) \ + + encode_short(self.target_chara_id) + +class DefragMatchGradeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_grade_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.class_num = decode_short(data, off) + off += SHORT_OFF + + self.grade_name, new_off = decode_str(data, off) + off += new_off + + self.class_name, new_off = decode_str(data, off) + off += new_off + + self.promotion_line_league_score = decode_short(data, off) + off += SHORT_OFF + + self.demotion_line_league_score = decode_short(data, off) + off += SHORT_OFF + + self.league_score_decrease_flag = decode_byte(data, off) + off += BYTE_OFF + + self.target_league_point_1 = decode_int(data, off) + off += INT_OFF + + self.target_league_point_2 = decode_int(data, off) + off += INT_OFF + + self.mvp_add_league_point = decode_int(data, off) + off += INT_OFF + + self.mob_level = decode_short(data, off) + off += SHORT_OFF + + self.normal_boss_level = decode_short(data, off) + off += SHORT_OFF + + self.adaptable_level = decode_short(data, off) + off += SHORT_OFF + + self.punisher_level = decode_short(data, off) + off += SHORT_OFF + + self.punisher_appearance_rate, new_off = decode_str(data, off) + off += new_off + + self.cpu_level = decode_short(data, off) + off += SHORT_OFF + + self.record_medal_drop_num = decode_short(data, off) + off += SHORT_OFF + + self.target_league_point_coefficient, new_off = decode_str(data, off) + off += new_off + + self.reward_table_sub_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchGradeData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_grade_id = data['DefragMatchGradeId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.class_num = data['ClassNum'] + ret.grade_name = data['GradeName'] + ret.class_name = data['ClassName'] + ret.promotion_line_league_score = data['PromotionLineLeagueScore'] + ret.demotion_line_league_score = data['DemotionLineLeagueScore'] + ret.league_score_decrease_flag = data['LeagueScoreDecreaseFlag'] + ret.target_league_point_1 = data['TargetLeaguePoint1'] + ret.target_league_point_2 = data['TargetLeaguePoint2'] + ret.mvp_add_league_point = data['MvpAddLeaguePoint'] + ret.mob_level = data['MobLevel'] + ret.normal_boss_level = data['NormalBossLevel'] + ret.adaptable_level = data['AdaptableLevel'] + ret.punisher_level = data['PunisherLevel'] + ret.punisher_appearance_rate = data['PunisherAppearanceRate'] + ret.cpu_level = data['CpuLevel'] + ret.record_medal_drop_num = data['RecordMedalDropNum'] + ret.target_league_point_coefficient = data['TargetLeaguePointCoefficient'] + ret.reward_table_sub_id = data['RewardTableSubId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_grade_id) \ + + encode_int(self.defrag_match_id) \ + + encode_short(self.class_num) \ + + encode_str(self.grade_name) \ + + encode_str(self.class_name) \ + + encode_short(self.promotion_line_league_score) \ + + encode_short(self.demotion_line_league_score) \ + + encode_byte(self.league_score_decrease_flag) \ + + encode_int(self.target_league_point_1) \ + + encode_int(self.target_league_point_2) \ + + encode_int(self.mvp_add_league_point) \ + + encode_short(self.mob_level) \ + + encode_short(self.normal_boss_level) \ + + encode_short(self.adaptable_level) \ + + encode_short(self.punisher_level) \ + + encode_str(self.punisher_appearance_rate) \ + + encode_short(self.cpu_level) \ + + encode_short(self.record_medal_drop_num) \ + + encode_str(self.target_league_point_coefficient) \ + + encode_int(self.reward_table_sub_id) + +class DefragMatchCpuUnitData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_cpu_unit_id = decode_int(data, off) + off += INT_OFF + + self.appearance_start_defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.appearance_end_defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.appearance_start_class_num = decode_int(data, off) + off += INT_OFF + + self.appearance_end_class_num = decode_int(data, off) + off += INT_OFF + + self.hero_log_hero_log_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_log_level = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.hero_log_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.hero_log_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_equipment_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_enhancement_value = decode_short(data, off) + off += SHORT_OFF + + self.main_weapon_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.main_weapon_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.main_weapon_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_equipment_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_enhancement_value = decode_short(data, off) + off += SHORT_OFF + + self.sub_equipment_awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self.sub_equipment_property1_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property1_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property1_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property2_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property3_value2 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_property_id = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_value1 = decode_int(data, off) + off += INT_OFF + + self.sub_equipment_property4_value2 = decode_int(data, off) + off += INT_OFF + + self.skill_slot1_skill_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_slot2_skill_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_slot3_skill_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_slot4_skill_id = decode_short(data, off) + off += SHORT_OFF + + self.skill_slot5_skill_id = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchCpuUnitData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_cpu_unit_id = data['DefragMatchCpuUnitsId'] + ret.appearance_start_defrag_match_id = data['AppearanceStartDefragMatchId'] + ret.appearance_end_defrag_match_id = data['AppearanceEndDefragMatchId'] + ret.appearance_start_class_num = data['AppearanceStartClassNum'] + ret.appearance_end_class_num = data['AppearanceEndClassNum'] + ret.hero_log_hero_log_id = data['HeroLogHeroLogId'] + ret.hero_log_log_level = data['HeroLogLogLevel'] + ret.hero_log_awakening_stage = data['HeroLogAwakeningStage'] + ret.hero_log_property1_property_id = data['HeroLogProperty1PropertyId'] + ret.hero_log_property1_value1 = data['HeroLogProperty1Value1'] + ret.hero_log_property1_value2 = data['HeroLogProperty1Value2'] + ret.hero_log_property2_property_id = data['HeroLogProperty2PropertyId'] + ret.hero_log_property2_value1 = data['HeroLogProperty2Value1'] + ret.hero_log_property2_value2 = data['HeroLogProperty2Value2'] + ret.hero_log_property3_property_id = data['HeroLogProperty3PropertyId'] + ret.hero_log_property3_value1 = data['HeroLogProperty3Value1'] + ret.hero_log_property3_value2 = data['HeroLogProperty3Value2'] + ret.hero_log_property4_property_id = data['HeroLogProperty4PropertyId'] + ret.hero_log_property4_value1 = data['HeroLogProperty4Value1'] + ret.hero_log_property4_value2 = data['HeroLogProperty4Value2'] + ret.main_weapon_equipment_id = data['MainWeaponEquipmentId'] + ret.main_weapon_enhancement_value = data['MainWeaponEnhancementValue'] + ret.main_weapon_awakening_stage = data['MainWeaponAwakeningStage'] + ret.main_weapon_property1_property_id = data['MainWeaponProperty1PropertyId'] + ret.main_weapon_property1_value1 = data['MainWeaponProperty1Value1'] + ret.main_weapon_property1_value2 = data['MainWeaponProperty1Value2'] + ret.main_weapon_property2_property_id = data['MainWeaponProperty2PropertyId'] + ret.main_weapon_property2_value1 = data['MainWeaponProperty2Value1'] + ret.main_weapon_property2_value2 = data['MainWeaponProperty2Value2'] + ret.main_weapon_property3_property_id = data['MainWeaponProperty3PropertyId'] + ret.main_weapon_property3_value1 = data['MainWeaponProperty3Value1'] + ret.main_weapon_property3_value2 = data['MainWeaponProperty3Value2'] + ret.main_weapon_property4_property_id = data['MainWeaponProperty4PropertyId'] + ret.main_weapon_property4_value1 = data['MainWeaponProperty4Value1'] + ret.main_weapon_property4_value2 = data['MainWeaponProperty4Value2'] + ret.sub_equipment_equipment_id = data['SubEquipmentEquipmentId'] + ret.sub_equipment_enhancement_value = data['SubEquipmentEnhancementValue'] + ret.sub_equipment_awakening_stage = data['SubEquipmentAwakeningStage'] + ret.sub_equipment_property1_property_id = data['SubEquipmentProperty1PropertyId'] + ret.sub_equipment_property1_value1 = data['SubEquipmentProperty1Value1'] + ret.sub_equipment_property1_value2 = data['SubEquipmentProperty1Value2'] + ret.sub_equipment_property2_property_id = data['SubEquipmentProperty2PropertyId'] + ret.sub_equipment_property2_value1 = data['SubEquipmentProperty2Value1'] + ret.sub_equipment_property2_value2 = data['SubEquipmentProperty2Value2'] + ret.sub_equipment_property3_property_id = data['SubEquipmentProperty3PropertyId'] + ret.sub_equipment_property3_value1 = data['SubEquipmentProperty3Value1'] + ret.sub_equipment_property3_value2 = data['SubEquipmentProperty3Value2'] + ret.sub_equipment_property4_property_id = data['SubEquipmentProperty4PropertyId'] + ret.sub_equipment_property4_value1 = data['SubEquipmentProperty4Value1'] + ret.sub_equipment_property4_value2 = data['SubEquipmentProperty4Value2'] + ret.skill_slot1_skill_id = data['SkillSlot1SkillId'] + ret.skill_slot2_skill_id = data['SkillSlot2SkillId'] + ret.skill_slot3_skill_id = data['SkillSlot3SkillId'] + ret.skill_slot4_skill_id = data['SkillSlot4SkillId'] + ret.skill_slot5_skill_id = data['SkillSlot5SkillId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_cpu_unit_id) \ + + encode_int(self.appearance_start_defrag_match_id) \ + + encode_int(self.appearance_end_defrag_match_id) \ + + encode_int(self.appearance_start_class_num) \ + + encode_int(self.appearance_end_class_num) \ + + encode_int(self.hero_log_hero_log_id) \ + + encode_short(self.hero_log_log_level) \ + + encode_short(self.hero_log_awakening_stage) \ + + encode_int(self.hero_log_property1_property_id) \ + + encode_int(self.hero_log_property1_value1) \ + + encode_int(self.hero_log_property1_value2) \ + + encode_int(self.hero_log_property2_property_id) \ + + encode_int(self.hero_log_property2_value1) \ + + encode_int(self.hero_log_property2_value2) \ + + encode_int(self.hero_log_property3_property_id) \ + + encode_int(self.hero_log_property3_value1) \ + + encode_int(self.hero_log_property3_value2) \ + + encode_int(self.hero_log_property4_property_id) \ + + encode_int(self.hero_log_property4_value1) \ + + encode_int(self.hero_log_property4_value2) \ + + encode_int(self.main_weapon_equipment_id) \ + + encode_short(self.main_weapon_enhancement_value) \ + + encode_short(self.main_weapon_awakening_stage) \ + + encode_int(self.main_weapon_property1_property_id) \ + + encode_int(self.main_weapon_property1_value1) \ + + encode_int(self.main_weapon_property1_value2) \ + + encode_int(self.main_weapon_property2_property_id) \ + + encode_int(self.main_weapon_property2_value1) \ + + encode_int(self.main_weapon_property2_value2) \ + + encode_int(self.main_weapon_property3_property_id) \ + + encode_int(self.main_weapon_property3_value1) \ + + encode_int(self.main_weapon_property3_value2) \ + + encode_int(self.main_weapon_property4_property_id) \ + + encode_int(self.main_weapon_property4_value1) \ + + encode_int(self.main_weapon_property4_value2) \ + + encode_int(self.sub_equipment_equipment_id) \ + + encode_short(self.sub_equipment_enhancement_value) \ + + encode_short(self.sub_equipment_awakening_stage) \ + + encode_int(self.sub_equipment_property1_property_id) \ + + encode_int(self.sub_equipment_property1_value1) \ + + encode_int(self.sub_equipment_property1_value2) \ + + encode_int(self.sub_equipment_property2_property_id) \ + + encode_int(self.sub_equipment_property2_value1) \ + + encode_int(self.sub_equipment_property2_value2) \ + + encode_int(self.sub_equipment_property3_property_id) \ + + encode_int(self.sub_equipment_property3_value1) \ + + encode_int(self.sub_equipment_property3_value2) \ + + encode_int(self.sub_equipment_property4_property_id) \ + + encode_int(self.sub_equipment_property4_value1) \ + + encode_int(self.sub_equipment_property4_value2) \ + + encode_short(self.skill_slot1_skill_id) \ + + encode_short(self.skill_slot2_skill_id) \ + + encode_short(self.skill_slot3_skill_id) \ + + encode_short(self.skill_slot4_skill_id) \ + + encode_short(self.skill_slot5_skill_id) + +class DefragMatchCpuSupportLogData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_cpu_support_log_id = decode_int(data, off) + off += INT_OFF + + self.appearance_start_defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.appearance_end_defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.appearance_start_class_num = decode_int(data, off) + off += INT_OFF + + self.appearance_end_class_num = decode_int(data, off) + off += INT_OFF + + self.support_log_id = decode_int(data, off) + off += INT_OFF + + self.awakening_stage = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchCpuSupportLogData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_cpu_support_log_id = data['DefragMatchCpuSupportLogId'] + ret.appearance_start_defrag_match_id = data['AppearanceStartDefragMatchId'] + ret.appearance_end_defrag_match_id = data['AppearanceEndDefragMatchId'] + ret.appearance_start_class_num = data['AppearanceStartClassNum'] + ret.appearance_end_class_num = data['AppearanceEndClassNum'] + ret.support_log_id = data['SupportLogId'] + ret.awakening_stage = data['AwakeningStage'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_cpu_support_log_id) \ + + encode_int(self.appearance_start_defrag_match_id) \ + + encode_int(self.appearance_end_defrag_match_id) \ + + encode_int(self.appearance_start_class_num) \ + + encode_int(self.appearance_end_class_num) \ + + encode_int(self.support_log_id) \ + + encode_short(self.awakening_stage) + +class DefragMatchPeriodBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_period_bonus_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.bonus_type = decode_byte(data, off) + off += BYTE_OFF + + self.param_1, new_off = decode_str(data, off) + off += new_off + + self.param_2, new_off = decode_str(data, off) + off += new_off + + self.param_3, new_off = decode_str(data, off) + off += new_off + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchPeriodBonusData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_period_bonus_id = data['DefragMatchPeriodBonusId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.bonus_type = data['BonusType'] + ret.param_1 = data['Param1'] + ret.param_2 = data['Param2'] + ret.param_3 = data['Param3'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_period_bonus_id) \ + + encode_int(self.defrag_match_id) \ + + encode_byte(self.bonus_type) \ + + encode_str(self.param_1) \ + + encode_str(self.param_2) \ + + encode_str(self.param_3) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class DefragMatchRandomBonusTableData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_random_bonus_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_random_bonus_condition_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.condition_value1 = decode_int(data, off) + off += INT_OFF + + self.condition_value2 = decode_int(data, off) + off += INT_OFF + + self.get_league_point = decode_int(data, off) + off += INT_OFF + + self.random_bonus_num = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchRandomBonusTableData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_random_bonus_id = data['DefragMatchRandomBonusId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.defrag_match_random_bonus_condition_id = data['DefragMatchRandomBonusConditionId'] + ret.name = data['Name'] + ret.condition_value1 = data['ConditionValue1'] + ret.condition_value2 = data['ConditionValue2'] + ret.get_league_point = data['GetLeaguePoint'] + ret.random_bonus_num = data['RandomBonusNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_random_bonus_id) \ + + encode_int(self.defrag_match_id) \ + + encode_int(self.defrag_match_random_bonus_condition_id) \ + + encode_str(self.name) \ + + encode_int(self.condition_value1) \ + + encode_int(self.condition_value2) \ + + encode_int(self.get_league_point) \ + + encode_byte(self.random_bonus_num) + +class DefragMatchRandomBonusConditionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_random_bonus_condition_id = decode_int(data, off) + off += INT_OFF + + self.format, new_off = decode_str(data, off) + off += new_off + + self.hud_format, new_off = decode_str(data, off) + off += new_off + + self.format_param_size = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchRandomBonusConditionData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_random_bonus_condition_id = data['DefragMatchRandomBonusConditionId'] + ret.format = data['Format'] + ret.hud_format = data['HudFormat'] + ret.format_param_size = data['FormatParamSize'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_random_bonus_condition_id) \ + + encode_str(self.format) \ + + encode_str(self.hud_format) \ + + encode_int(self.format_param_size) + +class DefragMatchRareDropData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.defrag_match_rare_drop_id = decode_int(data, off) + off += INT_OFF + + self.defrag_match_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_int(data, off) + off += INT_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_int(data, off) + off += INT_OFF + + self.strength_min = decode_int(data, off) + off += INT_OFF + + self.strength_max = decode_int(data, off) + off += INT_OFF + + self.property_table_sub_id = decode_int(data, off) + off += INT_OFF + + self.drop_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "DefragMatchRareDropData": + ret = cls(b"\x00" * 99, 0) + ret.defrag_match_rare_drop_id = data['DefragMatchRareDropId'] + ret.defrag_match_id = data['DefragMatchId'] + ret.unit_id = data['UnitId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength_min = data['StrengthMin'] + ret.strength_max = data['StrengthMax'] + ret.property_table_sub_id = data['PropertyTableSubId'] + ret.drop_rate = data['DropRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.defrag_match_rare_drop_id) \ + + encode_int(self.defrag_match_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_int(self.common_reward_num) \ + + encode_int(self.strength_min) \ + + encode_int(self.strength_max) \ + + encode_int(self.property_table_sub_id) \ + + encode_str(self.drop_rate) + +class YuiMedalShopData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.yui_medal_shop_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.description, new_off = decode_str(data, off) + off += new_off + + self.selling_yui_medal = decode_short(data, off) + off += SHORT_OFF + + self.selling_col = decode_int(data, off) + off += INT_OFF + + self.selling_event_item_id = decode_int(data, off) + off += INT_OFF + + self.selling_event_item_num = decode_int(data, off) + off += INT_OFF + + self.selling_ticket_num = decode_int(data, off) + off += INT_OFF + + self.purchase_limit = decode_short(data, off) + off += SHORT_OFF + + self.pick_up_flag = decode_byte(data, off) + off += BYTE_OFF + + self.product_category = decode_byte(data, off) + off += BYTE_OFF + + self.sales_type = decode_byte(data, off) + off += BYTE_OFF + + self.target_days = decode_byte(data, off) + off += BYTE_OFF + + self.target_hour = decode_byte(data, off) + off += BYTE_OFF + + self.interval_hour = decode_byte(data, off) + off += BYTE_OFF + + self.sales_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.sales_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.sort = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "YuiMedalShopData": + ret = cls(b"\x00" * 99, 0) + ret.yui_medal_shop_id = data['YuiMedalShopId'] + ret.name = data['Name'] + ret.description = data['Description'] + ret.selling_yui_medal = data['SellingYuiMedal'] + ret.selling_col = data['SellingCol'] + ret.selling_event_item_id = data['SellingEventItemId'] + ret.selling_event_item_num = data['SellingEventItemNum'] + ret.selling_ticket_num = data['SellingTicketNum'] + ret.purchase_limit = data['PurchaseLimit'] + ret.pick_up_flag = data['PickUpFlag'] + ret.product_category = data['ProductCategory'] + ret.sales_type = data['SalesType'] + ret.target_days = data['TargetDays'] + ret.target_hour = data['TargetHour'] + ret.interval_hour = data['IntervalHour'] + ret.sales_start_date = data['SalesStartDate'] + ret.sales_end_date = data['SalesEndDate'] + ret.sort = data['Sort'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.yui_medal_shop_id) \ + + encode_str(self.name) \ + + encode_str(self.description) \ + + encode_short(self.selling_yui_medal) \ + + encode_int(self.selling_col) \ + + encode_int(self.selling_event_item_id) \ + + encode_int(self.selling_event_item_num) \ + + encode_int(self.selling_ticket_num) \ + + encode_short(self.purchase_limit) \ + + encode_byte(self.pick_up_flag) \ + + encode_byte(self.product_category) \ + + encode_byte(self.sales_type) \ + + encode_byte(self.target_days) \ + + encode_byte(self.target_hour) \ + + encode_byte(self.interval_hour) \ + + encode_date_str(self.sales_start_date) \ + + encode_date_str(self.sales_end_date) \ + + encode_byte(self.sort) + +class YuiMedalShopItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.yui_medal_shop_item_id = decode_int(data, off) + off += INT_OFF + + self.yui_medal_shop_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "YuiMedalShopItemData": + ret = cls(b"\x00" * 99, 0) + ret.yui_medal_shop_item_id = data['YuiMedalShopItemId'] + ret.yui_medal_shop_id = data['YuiMedalShopId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.yui_medal_shop_item_id) \ + + encode_int(self.yui_medal_shop_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class EventSceneData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.event_scene_id = decode_int(data, off) + off += INT_OFF + + self.event_id = decode_int(data, off) + off += INT_OFF + + self.scene_type = decode_byte(data, off) + off += BYTE_OFF + + self.episode_no = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.param_1, new_off = decode_str(data, off) + off += new_off + + self.param_2, new_off = decode_str(data, off) + off += new_off + + self.param_3, new_off = decode_str(data, off) + off += new_off + + self.adv_name, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EventSceneData": + ret = cls(b"\x00" * 99, 0) + ret.event_scene_id = data['EventSceneId'] + ret.event_id = data['EventId'] + ret.scene_type = data['SceneType'] + ret.episode_no = data['EpisodeNo'] + ret.title = data['Title'] + ret.param_1 = data['Param1'] + ret.param_2 = data['Param2'] + ret.param_3 = data['Param3'] + ret.adv_name = data['AdvName'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.event_scene_id) \ + + encode_int(self.event_id) \ + + encode_byte(self.scene_type) \ + + encode_int(self.episode_no) \ + + encode_str(self.title) \ + + encode_str(self.param_1) \ + + encode_str(self.param_2) \ + + encode_str(self.param_3) \ + + encode_str(self.adv_name) + +class GenericCampaignPeriodData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.generic_campaign_period_id = decode_int(data, off) + off += INT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GenericCampaignPeriodData": + ret = cls(b"\x00" * 99, 0) + ret.generic_campaign_period_id = data['GenericCampaignPeriodId'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.generic_campaign_period_id) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class BeginnerMissionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_id = data['BeginnerMissionId'] + ret.name = data['Name'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_id) \ + + encode_str(self.name) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class BeginnerMissionConditionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_condition_id = decode_int(data, off) + off += INT_OFF + + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + + self.seat_num = decode_short(data, off) + off += SHORT_OFF + + self.mission_num = decode_short(data, off) + off += SHORT_OFF + + self.display_content, new_off = decode_str(data, off) + off += new_off + + self.display_tips, new_off = decode_str(data, off) + off += new_off + + self.condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.condition_param_1, new_off = decode_str(data, off) + off += new_off + + self.condition_param_2, new_off = decode_str(data, off) + off += new_off + + self.condition_param_3, new_off = decode_str(data, off) + off += new_off + + self.required_achievement_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionConditionData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_condition_id = data['BeginnerMissionConditionId'] + ret.beginner_mission_id = data['BeginnerMissionId'] + ret.seat_num = data['SeatNum'] + ret.mission_num = data['MissionNum'] + ret.display_content = data['DisplayContent'] + ret.display_tips = data['DisplayTips'] + ret.condition_type = data['ConditionType'] + ret.condition_param_1 = data['ConditionParam1'] + ret.condition_param_2 = data['ConditionParam2'] + ret.condition_param_3 = data['ConditionParam3'] + ret.required_achievement_num = data['RequiredAchievementNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_condition_id) \ + + encode_int(self.beginner_mission_id) \ + + encode_short(self.seat_num) \ + + encode_short(self.mission_num) \ + + encode_str(self.display_content) \ + + encode_str(self.display_tips) \ + + encode_byte(self.condition_type) \ + + encode_str(self.condition_param_1) \ + + encode_str(self.condition_param_2) \ + + encode_str(self.condition_param_3) \ + + encode_short(self.required_achievement_num) + +class BeginnerMissionRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + + self.beginner_mission_condition_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionRewardData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_id = data['BeginnerMissionId'] + ret.beginner_mission_condition_id = data['BeginnerMissionConditionId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_id) \ + + encode_int(self.beginner_mission_condition_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class BeginnerMissionSeatConditionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + + self.beginner_mission_seat_condition_id = decode_int(data, off) + off += INT_OFF + + self.seat_num = decode_short(data, off) + off += SHORT_OFF + + self.mission_seat_num = decode_short(data, off) + off += SHORT_OFF + + self.display_content, new_off = decode_str(data, off) + off += new_off + + self.display_tips, new_off = decode_str(data, off) + off += new_off + + self.condition_type = decode_byte(data, off) + off += BYTE_OFF + + self.condition_param_1, new_off = decode_str(data, off) + off += new_off + + self.condition_param_2, new_off = decode_str(data, off) + off += new_off + + self.condition_param_3, new_off = decode_str(data, off) + off += new_off + + self.required_achievement_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionSeatConditionData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_id = data['BeginnerMissionId'] + ret.beginner_mission_seat_condition_id = data['BeginnerMissionSeatConditionId'] + ret.seat_num = data['SeatNum'] + ret.mission_seat_num = data['MissionSeatNum'] + ret.display_content = data['DisplayContent'] + ret.display_tips = data['DisplayTips'] + ret.condition_type = data['ConditionType'] + ret.condition_param_1 = data['ConditionParam1'] + ret.condition_param_2 = data['ConditionParam2'] + ret.condition_param_3 = data['ConditionParam3'] + ret.required_achievement_num = data['RequiredAchievementNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_id) \ + + encode_int(self.beginner_mission_seat_condition_id) \ + + encode_short(self.seat_num) \ + + encode_short(self.mission_seat_num) \ + + encode_str(self.display_content) \ + + encode_str(self.display_tips) \ + + encode_byte(self.condition_type) \ + + encode_str(self.condition_param_1) \ + + encode_str(self.condition_param_2) \ + + encode_str(self.condition_param_3) \ + + encode_short(self.required_achievement_num) + +class BeginnerMissionSeatRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.beginner_mission_id = decode_int(data, off) + off += INT_OFF + + self.beginner_mission_seat_condition_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "BeginnerMissionSeatRewardData": + ret = cls(b"\x00" * 99, 0) + ret.beginner_mission_id = data['BeginnerMissionId'] + ret.beginner_mission_seat_condition_id = data['BeginnerMissionSeatConditionId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.beginner_mission_id) \ + + encode_int(self.beginner_mission_seat_condition_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class EventItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.event_item_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.flavor_text, new_off = decode_str(data, off) + off += new_off + + self.event_item_icon, new_off = decode_str(data, off) + off += new_off + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EventItemData": + ret = cls(b"\x00" * 99, 0) + ret.event_item_id = data['EventItemId'] + ret.name = data['Name'] + ret.flavor_text = data['FlavorText'] + ret.event_item_icon = data['EventItemIcon'] + ret.unit_id = data['UnitId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.event_item_id) \ + + encode_str(self.name) \ + + encode_str(self.flavor_text) \ + + encode_str(self.event_item_icon) \ + + encode_int(self.unit_id) + +class EventMonsterData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.event_monster_id = decode_int(data, off) + off += INT_OFF + + self.event_id = decode_int(data, off) + off += INT_OFF + + self.unit_id = decode_int(data, off) + off += INT_OFF + + self.event_item_id = decode_int(data, off) + off += INT_OFF + + self.drop_rate, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "EventMonsterData": + ret = cls(b"\x00" * 99, 0) + ret.event_monster_id = data['EventMonsterId'] + ret.event_id = data['EventId'] + ret.unit_id = data['UnitId'] + ret.event_item_id = data['EventItemId'] + ret.drop_rate = data['DropRate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.event_monster_id) \ + + encode_int(self.event_id) \ + + encode_int(self.unit_id) \ + + encode_int(self.event_item_id) \ + + encode_str(self.drop_rate) + +class YuiMedalBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.yui_medal_bonus_id = decode_int(data, off) + off += INT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "YuiMedalBonusData": + ret = cls(b"\x00" * 99, 0) + ret.yui_medal_bonus_id = data['YuiMedalBonusId'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.yui_medal_bonus_id) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class YuiMedalBonusConditionData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.yui_medal_bonus_condition_id = decode_int(data, off) + off += INT_OFF + + self.yui_medal_bonus_id = decode_int(data, off) + off += INT_OFF + + self.target_days = decode_int(data, off) + off += INT_OFF + + self.get_num = decode_int(data, off) + off += INT_OFF + + self.loop_flag = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "YuiMedalBonusConditionData": + ret = cls(b"\x00" * 99, 0) + ret.yui_medal_bonus_condition_id = data['YuiMedalBonusConditionId'] + ret.yui_medal_bonus_id = data['YuiMedalBonusId'] + ret.target_days = data['TargetDays'] + ret.get_num = data['GetNum'] + ret.loop_flag = data['LoopFlag'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.yui_medal_bonus_condition_id) \ + + encode_int(self.yui_medal_bonus_id) \ + + encode_int(self.target_days) \ + + encode_int(self.get_num) \ + + encode_byte(self.loop_flag) + +class GashaMedalData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_medal_id = decode_int(data, off) + off += INT_OFF + + self.gasha_medal_type = decode_byte(data, off) + off += BYTE_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_medal_id = data['GashaMedalId'] + ret.gasha_medal_type = data['GashaMedalType'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_medal_id) \ + + encode_byte(self.gasha_medal_type) + +class GashaMedalTypeData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_medal_type = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.medal_icon, new_off = decode_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalTypeData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_medal_type = data['GashaMedalType'] + ret.name = data['Name'] + ret.medal_icon = data['MedalIcon'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_medal_type) \ + + encode_str(self.name) \ + + encode_str(self.medal_icon) + +class GashaMedalSettingData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_id = decode_int(data, off) + off += INT_OFF + + self.gasha_medal_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalSettingData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_id = data['GashaId'] + ret.gasha_medal_id = data['GashaMedalId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_id) \ + + encode_int(self.gasha_medal_id) + +class GashaMedalBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.analyze_amount = decode_byte(data, off) + off += BYTE_OFF + + self.get_gasha_medal_num = decode_int(data, off) + off += INT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalBonusData": + ret = cls(b"\x00" * 99, 0) + ret.analyze_amount = data['AnalyzeAmount'] + ret.get_gasha_medal_num = data['GetGashaMedalNum'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_byte(self.analyze_amount) \ + + encode_int(self.get_gasha_medal_num) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class GashaMedalShopData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_medal_shop_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.gasha_medal_id = decode_int(data, off) + off += INT_OFF + + self.use_gasha_medal_num = decode_int(data, off) + off += INT_OFF + + self.purchase_limit = decode_short(data, off) + off += SHORT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalShopData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_medal_shop_id = data['GashaMedalShopId'] + ret.name = data['Name'] + ret.gasha_medal_id = data['GashaMedalId'] + ret.use_gasha_medal_num = data['UseGashaMedalNum'] + ret.purchase_limit = data['PurchaseLimit'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_medal_shop_id) \ + + encode_str(self.name) \ + + encode_int(self.gasha_medal_id) \ + + encode_int(self.use_gasha_medal_num) \ + + encode_short(self.purchase_limit) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class GashaMedalShopItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_medal_shop_item_id = decode_int(data, off) + off += INT_OFF + + self.gasha_medal_shop_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaMedalShopItemData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_medal_shop_item_id = data['GashaMedalShopItemId'] + ret.gasha_medal_shop_id = data['GashaMedalShopId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_medal_shop_item_id) \ + + encode_int(self.gasha_medal_shop_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class ResEarnCampaignApplicationData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.res_earn_campaign_application_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.news_id, new_off = decode_str(data, off) + off += new_off + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ResEarnCampaignApplicationData": + ret = cls(b"\x00" * 99, 0) + ret.res_earn_campaign_application_id = data['ResEarnCampaignApplicationId'] + ret.title = data['Title'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.posting_start_date = data['PostingStartDate'] + ret.posting_end_date = data['PostingEndDate'] + ret.news_id = data['NewsId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.res_earn_campaign_application_id) \ + + encode_str(self.title) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_date_str(self.posting_start_date) \ + + encode_date_str(self.posting_end_date) \ + + encode_str(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class ResEarnCampaignApplicationProductData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.res_earn_campaign_application_product_id = decode_int(data, off) + off += INT_OFF + + self.res_earn_campaign_application_id = decode_int(data, off) + off += INT_OFF + + self.award_name, new_off = decode_str(data, off) + off += new_off + + self.name, new_off = decode_str(data, off) + off += new_off + + self.need_application_point = decode_short(data, off) + off += SHORT_OFF + + self.winning_num = decode_short(data, off) + off += SHORT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ResEarnCampaignApplicationProductData": + ret = cls(b"\x00" * 99, 0) + ret.res_earn_campaign_application_product_id = data['ResEarnCampaignApplicationProductId'] + ret.res_earn_campaign_application_id = data['ResEarnCampaignApplicationId'] + ret.award_name = data['AwardName'] + ret.name = data['Name'] + ret.need_application_point = data['NeedApplicationPoint'] + ret.winning_num = data['WinningNum'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.res_earn_campaign_application_product_id) \ + + encode_int(self.res_earn_campaign_application_id) \ + + encode_str(self.award_name) \ + + encode_str(self.name) \ + + encode_short(self.need_application_point) \ + + encode_short(self.winning_num) + +class ResEarnCampaignShopData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.res_earn_campaign_shop_id = decode_int(data, off) + off += INT_OFF + + self.res_earn_campaign_application_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.selling_yui_medal = decode_short(data, off) + off += SHORT_OFF + + self.selling_col = decode_int(data, off) + off += INT_OFF + + self.selling_event_item_id = decode_int(data, off) + off += INT_OFF + + self.selling_event_item_num = decode_int(data, off) + off += INT_OFF + + self.purchase_limit = decode_short(data, off) + off += SHORT_OFF + + self.get_application_point = decode_short(data, off) + off += SHORT_OFF + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ResEarnCampaignShopData": + ret = cls(b"\x00" * 99, 0) + ret.res_earn_campaign_shop_id = data['ResEarnCampaignShopId'] + ret.res_earn_campaign_application_id = data['ResEarnCampaignApplicationId'] + ret.name = data['Name'] + ret.selling_yui_medal = data['SellingYuiMedal'] + ret.selling_col = data['SellingCol'] + ret.selling_event_item_id = data['SellingEventItemId'] + ret.selling_event_item_num = data['SellingEventItemNum'] + ret.purchase_limit = data['PurchaseLimit'] + ret.get_application_point = data['GetApplicationPoint'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.res_earn_campaign_shop_id) \ + + encode_int(self.res_earn_campaign_application_id) \ + + encode_str(self.name) \ + + encode_short(self.selling_yui_medal) \ + + encode_int(self.selling_col) \ + + encode_int(self.selling_event_item_id) \ + + encode_int(self.selling_event_item_num) \ + + encode_short(self.purchase_limit) \ + + encode_short(self.get_application_point) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class ResEarnCampaignShopItemData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.res_earn_campaign_shop_item_id = decode_int(data, off) + off += INT_OFF + + self.res_earn_campaign_shop_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "ResEarnCampaignShopItemData": + ret = cls(b"\x00" * 99, 0) + ret.res_earn_campaign_shop_item_id = data['ResEarnCampaignShopItemId'] + ret.res_earn_campaign_shop_id = data['ResEarnCampaignShopId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.res_earn_campaign_shop_item_id) \ + + encode_int(self.res_earn_campaign_shop_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class PayingYuiMedalBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.paying_yui_medal_bonus_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.reward_yui_medal_num = decode_int(data, off) + off += INT_OFF + + self.news_id, new_off = decode_str(data, off) + off += new_off + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PayingYuiMedalBonusData": + ret = cls(b"\x00" * 99, 0) + ret.paying_yui_medal_bonus_id = data['PayingYuiMedalBonusId'] + ret.title = data['Title'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.reward_yui_medal_num = data['RewardYuiMedalNum'] + ret.news_id = data['NewsId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.paying_yui_medal_bonus_id) \ + + encode_str(self.title) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_int(self.reward_yui_medal_num) \ + + encode_str(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class AcLoginBonusData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.ac_login_bonus_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.reward_set_sub_id = decode_int(data, off) + off += INT_OFF + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "AcLoginBonusData": + ret = cls(b"\x00" * 99, 0) + ret.ac_login_bonus_id = data['AcLoginBonusId'] + ret.title = data['Title'] + ret.reward_set_sub_id = data['RewardSetSubId'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.ac_login_bonus_id) \ + + encode_str(self.title) \ + + encode_int(self.reward_set_sub_id) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) + +class PlayCampaignData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.play_campaign_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.campaign_type = decode_byte(data, off) + off += BYTE_OFF + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.news_id, new_off = decode_str(data, off) + off += new_off + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PlayCampaignData": + ret = cls(b"\x00" * 99, 0) + ret.play_campaign_id = data['PlayCampaignId'] + ret.title = data['Title'] + ret.campaign_type = data['CampaignType'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.posting_start_date = data['PostingStartDate'] + ret.posting_end_date = data['PostingEndDate'] + ret.news_id = data['NewsId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.play_campaign_id) \ + + encode_str(self.title) \ + + encode_byte(self.campaign_type) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_date_str(self.posting_start_date) \ + + encode_date_str(self.posting_end_date) \ + + encode_str(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class PlayCampaignRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.play_campaign_reward_id = decode_int(data, off) + off += INT_OFF + + self.play_campaign_id = decode_int(data, off) + off += INT_OFF + + self.product_no = decode_int(data, off) + off += INT_OFF + + self.product_name, new_off = decode_str(data, off) + off += new_off + + self.product_type = decode_byte(data, off) + off += BYTE_OFF + + self.resource_type = decode_byte(data, off) + off += BYTE_OFF + + self.required_credit_num = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self.title_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "PlayCampaignRewardData": + ret = cls(b"\x00" * 99, 0) + ret.play_campaign_reward_id = data['PlayCampaignRewardId'] + ret.play_campaign_id = data['PlayCampaignId'] + ret.product_no = data['ProductNo'] + ret.product_name = data['ProductName'] + ret.product_type = data['ProductType'] + ret.resource_type = data['ResourceType'] + ret.required_credit_num = data['RequiredCreditNum'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + ret.title_id = data['TitleId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.play_campaign_reward_id) \ + + encode_int(self.play_campaign_id) \ + + encode_int(self.product_no) \ + + encode_str(self.product_name) \ + + encode_byte(self.product_type) \ + + encode_byte(self.resource_type) \ + + encode_int(self.required_credit_num) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) \ + + encode_int(self.title_id) + +class GashaFreeCampaignData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.gasha_free_campaign_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.start_date, new_off = decode_date_str(data, off) + off += new_off + + self.end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "GashaFreeCampaignData": + ret = cls(b"\x00" * 99, 0) + ret.gasha_free_campaign_id = data['GashaFreeCampaignId'] + ret.name = data['Name'] + ret.start_date = data['StartDate'] + ret.end_date = data['EndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.gasha_free_campaign_id) \ + + encode_str(self.name) \ + + encode_date_str(self.start_date) \ + + encode_date_str(self.end_date) + +class QuestDropBoostCampaignData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.quest_drop_boost_campaign_id = decode_int(data, off) + off += INT_OFF + + self.name, new_off = decode_str(data, off) + off += new_off + + self.consume_ticket_num = decode_int(data, off) + off += INT_OFF + + self.drop_magnification = decode_byte(data, off) + off += BYTE_OFF + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "QuestDropBoostCampaignData": + ret = cls(b"\x00" * 99, 0) + ret.quest_drop_boost_campaign_id = data['QuestDropBoostCampaignId'] + ret.name = data['Name'] + ret.consume_ticket_num = data['ConsumeTicketNum'] + ret.drop_magnification = data['DropMagnification'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.quest_drop_boost_campaign_id) \ + + encode_str(self.name) \ + + encode_int(self.consume_ticket_num) \ + + encode_byte(self.drop_magnification) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) + +class FirstTicketPurchaseCampaignData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.first_ticket_purchase_campaign_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.not_target_applying_base_date, new_off = decode_date_str(data, off) + off += new_off + + self.target_ticket_purchase_id_list, new_off = decode_str(data, off) + off += new_off + + self.news_id, new_off = decode_str(data, off) + off += new_off + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "FirstTicketPurchaseCampaignData": + ret = cls(b"\x00" * 99, 0) + ret.first_ticket_purchase_campaign_id = data['FirstTicketPurchaseCampaignId'] + ret.title = data['Title'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.not_target_applying_base_date = data['NotTargetApplyingBaseDate'] + ret.target_ticket_purchase_id_list = data['TargetTicketPurchaseIdList'] + ret.news_id = data['NewsId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.first_ticket_purchase_campaign_id) \ + + encode_str(self.title) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_date_str(self.not_target_applying_base_date) \ + + encode_str(self.target_ticket_purchase_id_list) \ + + encode_str(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class LinkedSiteRegCampaignsData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.linked_site_reg_campaign_id = decode_int(data, off) + off += INT_OFF + + self.title, new_off = decode_str(data, off) + off += new_off + + self.open_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.open_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_start_date, new_off = decode_date_str(data, off) + off += new_off + + self.posting_end_date, new_off = decode_date_str(data, off) + off += new_off + + self.news_id, new_off = decode_str(data, off) + off += new_off + + self.help_id = decode_int(data, off) + off += INT_OFF + + self.ad_banner_id = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "LinkedSiteRegCampaignsData": + ret = cls(b"\x00" * 99, 0) + ret.linked_site_reg_campaign_id = data['LinkedSiteRegCampaignId'] + ret.title = data['Title'] + ret.open_start_date = data['OpenStartDate'] + ret.open_end_date = data['OpenEndDate'] + ret.posting_start_date = data['PostingStartDate'] + ret.posting_end_date = data['PostingEndDate'] + ret.news_id = data['NewsId'] + ret.help_id = data['HelpId'] + ret.ad_banner_id = data['AdBannerId'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.linked_site_reg_campaign_id) \ + + encode_str(self.title) \ + + encode_date_str(self.open_start_date) \ + + encode_date_str(self.open_end_date) \ + + encode_date_str(self.posting_start_date) \ + + encode_date_str(self.posting_end_date) \ + + encode_str(self.news_id) \ + + encode_int(self.help_id) \ + + encode_int(self.ad_banner_id) + +class LinkedSiteRegCampaignRewardData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.linked_site_reg_campaign_reward_id = decode_int(data, off) + off += INT_OFF + + self.linked_site_reg_campaign_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_type = decode_byte(data, off) + off += BYTE_OFF + + self.common_reward_id = decode_int(data, off) + off += INT_OFF + + self.common_reward_num = decode_short(data, off) + off += SHORT_OFF + + self.strength = decode_int(data, off) + off += INT_OFF + + self.property1_property_id = decode_int(data, off) + off += INT_OFF + + self.property1_value1 = decode_int(data, off) + off += INT_OFF + + self.property1_value2 = decode_int(data, off) + off += INT_OFF + + self.property2_property_id = decode_int(data, off) + off += INT_OFF + + self.property2_value1 = decode_int(data, off) + off += INT_OFF + + self.property2_value2 = decode_int(data, off) + off += INT_OFF + + self.property3_property_id = decode_int(data, off) + off += INT_OFF + + self.property3_value1 = decode_int(data, off) + off += INT_OFF + + self.property3_value2 = decode_int(data, off) + off += INT_OFF + + self.property4_property_id = decode_int(data, off) + off += INT_OFF + + self.property4_value1 = decode_int(data, off) + off += INT_OFF + + self.property4_value2 = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, data: Dict) -> "LinkedSiteRegCampaignRewardData": + ret = cls(b"\x00" * 99, 0) + ret.linked_site_reg_campaign_reward_id = data['LinkedSiteRegCampaignRewardId'] + ret.linked_site_reg_campaign_id = data['LinkedSiteRegCampaignId'] + ret.common_reward_type = data['CommonRewardType'] + ret.common_reward_id = data['CommonRewardId'] + ret.common_reward_num = data['CommonRewardNum'] + ret.strength = data['Strength'] + ret.property1_property_id = data['Property1PropertyId'] + ret.property1_value1 = data['Property1Value1'] + ret.property1_value2 = data['Property1Value2'] + ret.property2_property_id = data['Property2PropertyId'] + ret.property2_value1 = data['Property2Value1'] + ret.property2_value2 = data['Property2Value2'] + ret.property3_property_id = data['Property3PropertyId'] + ret.property3_value1 = data['Property3Value1'] + ret.property3_value2 = data['Property3Value2'] + ret.property4_property_id = data['Property4PropertyId'] + ret.property4_value1 = data['Property4Value1'] + ret.property4_value2 = data['Property4Value2'] + return ret + + def make(self) -> bytes: + return super().make() \ + + encode_int(self.linked_site_reg_campaign_reward_id) \ + + encode_int(self.linked_site_reg_campaign_id) \ + + encode_byte(self.common_reward_type) \ + + encode_int(self.common_reward_id) \ + + encode_short(self.common_reward_num) \ + + encode_int(self.strength) \ + + encode_int(self.property1_property_id) \ + + encode_int(self.property1_value1) \ + + encode_int(self.property1_value2) \ + + encode_int(self.property2_property_id) \ + + encode_int(self.property2_value1) \ + + encode_int(self.property2_value2) \ + + encode_int(self.property3_property_id) \ + + encode_int(self.property3_value1) \ + + encode_int(self.property3_value2) \ + + encode_int(self.property4_property_id) \ + + encode_int(self.property4_value1) \ + + encode_int(self.property4_value2) + +class EpisodeAppendUserData(BaseHelper): + def __init__(self, data: bytes, offset: int): + off = offset + self.user_episode_append_id, new_off = decode_str(data, off) + off += new_off + + self.user_id, new_off = decode_str(data, off) + off += new_off + + self.episode_append_id = decode_int(data, off) + off += INT_OFF + + self.own_num = decode_int(data, off) + off += INT_OFF + + self._sz = off - offset + + @classmethod + def from_args(cls, episode_id: int = 0, user_id: int = 0, episode_append_id: int = 0, own_num: int = 99) -> "EpisodeAppendUserData": + ret = cls(b"\x00" * 996, 0) + ret.user_episode_append_id = episode_id + ret.user_id = user_id + ret.episode_append_id = episode_append_id + ret.own_num = own_num + + def make(self) -> bytes: + return super().make() \ + + encode_str(self.user_episode_append_id) \ + + encode_str(self.user_id) \ + + encode_int(self.episode_append_id) \ + + encode_int(self.own_num) diff --git a/titles/sao/index.py b/titles/sao/index.py index 1f68c43..8a775c6 100644 --- a/titles/sao/index.py +++ b/titles/sao/index.py @@ -1,17 +1,20 @@ from typing import Tuple, Dict, List from starlette.requests import Request -from starlette.responses import Response +from starlette.responses import Response, PlainTextResponse, FileResponse from starlette.routing import Route import yaml import logging, coloredlogs from logging.handlers import TimedRotatingFileHandler from os import path from Crypto.Cipher import Blowfish -from hashlib import md5 +from Crypto.Hash import MD5 import secrets +import traceback +import sys -from core import CoreConfig, Utils +from core import CoreConfig from core.title import BaseServlet +from core.utils import Utils from titles.sao.config import SaoConfig from titles.sao.const import SaoConstants from titles.sao.base import SaoBase @@ -55,13 +58,15 @@ class SaoServlet(BaseServlet): self.static_hash = None if self.game_cfg.hash.verify_hash: - self.static_hash = md5(self.game_cfg.hash.hash_base.encode()).digest() # Greate hashing guys, really validates the data - + self.static_hash = MD5.new(self.game_cfg.hash.hash_base.encode()).digest() # Greate hashing guys, really validates the data + def get_routes(self) -> List[Route]: return [ - Route("/{datecode:int}/proto/if/{category:str}/{endpoint:str}", self.render_POST, methods=['POST']) + Route("/{datecode:int}/proto/if/{category:str}/{endpoint:str}", self.render_POST, methods=['POST']), + Route("/saoresource/{resource_type:str}/{resource_id:int}/{endpoint:str}", self.handle_resource), + Route("/system_status.php", self.render_system_status), ] - + @classmethod def is_game_enabled(cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str) -> bool: game_cfg = SaoConfig() @@ -75,29 +80,25 @@ class SaoServlet(BaseServlet): return False return True - - def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]: - port_ssl = Utils.get_title_port_ssl(self.core_cfg) - port_normal = Utils.get_title_port(self.core_cfg) - proto = "http" - port = f":{port_normal}" if port_normal != 80 else "" + def get_allnet_info(self, game_id: str, int_ver: int, serial: str) -> Tuple[str, str]: + if self.core_cfg.server.is_using_proxy: + return (f"https://{self.core_cfg.server.hostname}/", "") - if self.game_cfg.server.use_https: - proto = "https" - port = f":{port_ssl}" if port_ssl != 443 else "" + return (f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/", "") - return (f"{proto}://{self.core_cfg.server.hostname}{port}/", "") - - - def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, str]: + def get_mucha_info(self, core_cfg: CoreConfig, cfg_dir: str) -> Tuple[bool, List[str], List[str]]: if not self.game_cfg.server.enable: return (False, [], []) return (True, SaoConstants.GAME_CDS, SaoConstants.NETID_PREFIX) + async def render_system_status(self, request: Request) -> bytes: + return PlainTextResponse("open") + async def render_POST(self, request: Request) -> bytes: - endpoint = request.path_params.get('endpoint', '') + endpoint = request.path_params['endpoint'] + ip = Utils.get_ip_addr(request) iv = b"" req_raw = await request.body() @@ -122,10 +123,23 @@ class SaoServlet(BaseServlet): else: req_data = req_raw[40:] - handler = getattr(self.base, f"handle_{cmd_str}", self.base.handle_noop) - self.logger.info(f"{endpoint} - {cmd_str} request") - self.logger.debug(f"Request: {req_raw.hex()}") - resp = await handler(req_header, req_data) + self.logger.debug(f"{endpoint} ({cmd_str}) Request from {ip}: {req_raw.hex()}") + handler = getattr(self.base, f"handle_{cmd_str}", None) + if handler is None: + self.logger.info(f"Using Generic handler for {endpoint}") + handler = self.base.handle_noop + + try: + resp = await handler(req_header, req_data, ip) + + except Exception as e: + self.logger.error(f"Error handling {endpoint} - {e}") + tp, val, tb = sys.exc_info() + traceback.print_exception(tp, val, tb, limit=5) + with open("{0}/{1}.log".format(self.core_cfg.server.log_dir, "sao"), "a") as f: + traceback.print_exception(tp, val, tb, limit=5, file=f) + + resp = SaoNoopResponse(req_header.cmd + 1).make() if resp is None: resp = SaoNoopResponse(req_header.cmd + 1).make() @@ -138,7 +152,7 @@ class SaoServlet(BaseServlet): else: self.logger.error(f"Unknown response type {type(resp)}") - return Response() + return SaoNoopResponse(req_header.cmd + 1).make() self.logger.debug(f"Response: {resp.hex()}") @@ -154,6 +168,17 @@ class SaoServlet(BaseServlet): tmp = struct.pack("!I", crypt_data_len) # does it want the length of the encrypted response?? resp = resp[:20] + tmp + iv + data_crypt self.logger.debug(f"Encrypted Response: {resp.hex()}") - - - return Response(resp, media_type="text/html; charset=utf-8") \ No newline at end of file + + return Response(resp) + + async def handle_resource(self, request: Request) -> bytes: + # TODO: better guard against path traversal attacks + resource_type = request.path_params['resource_type'].replace(".\\", "").replace("..\\", "") + resource_id = request.path_params['resource_id'] + endpoint = request.path_params['endpoint'].replace(".\\", "").replace("..\\", "") + req_ip = Utils.get_ip_addr(request) + + self.logger.debug(f"{req_ip} requested {resource_type} id {resource_id} {endpoint}") + if path.exists(f"./titles/sao/data/{resource_type}/{resource_id}/{endpoint}"): + return FileResponse(f"./titles/sao/data/{resource_type}/{resource_id}/{endpoint}") + return Response(status_code=404) diff --git a/titles/sao/read.py b/titles/sao/read.py index 92aad8c..6299bc4 100644 --- a/titles/sao/read.py +++ b/titles/sao/read.py @@ -1,24 +1,16 @@ -from typing import Optional, Dict, List -from os import walk, path -import urllib import csv +from os import path +from typing import Optional, Dict, List -from read import BaseReader from core.config import CoreConfig -from titles.sao.database import SaoData -from titles.sao.const import SaoConstants +from read import BaseReader +from .database import SaoData +from .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) + def __init__(self, config: CoreConfig, version: int, bin_dir: Optional[str], opt_dir: Optional[str], extra: Optional[str]) -> None: + super().__init__(config, version, bin_dir, opt_dir, extra) self.data = SaoData(config) try: @@ -28,225 +20,207 @@ class SaoReader(BaseReader): except IndexError: self.logger.error(f"Invalid project SAO version {version}") exit(1) - + async def read(self) -> None: if path.exists(self.bin_dir): await self.read_csv(f"{self.bin_dir}") else: self.logger.warn("Directory not found, nothing to import") - + + def load_csv_file(self, file: str) -> List[Dict]: + ret = [] + try: + fullPath = self.bin_dir + "/" if not self.bin_dir.endswith("/") else "" + fullPath += file + with open(fullPath, encoding="UTF-8") as fp: + reader = csv.DictReader(fp) + for row in reader: + tmp = {} + + fkey = list(row.keys())[0] + new_fkey = fkey.replace("// ", "") + fval = row[fkey] + row.pop(fkey) + row[new_fkey] = fval + + for k,v in row.items(): + if v == "-1": + row[k] = None + elif v.isdigit(): + row[k] = int(v) + elif v.isdecimal(): + row[k] = float(v) + elif v == "True": + row[k] = True + elif v == "False": + row[k] = False + ret.append(row) + + except Exception as e: + self.logger.warning(f"Couldn't read csv file {fullPath}, skipping - {e}") + + return ret async 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 + reader = self.load_csv_file("QuestScene.csv") + if reader: + for row in reader: + self.logger.info(f"Adding quest {row['QuestSceneId']}") + await self.data.static.put_quest(row) - self.logger.info(f"Added quest {questSceneId} | Name: {name}") - - try: - await self.data.static.put_quest( - questSceneId, - 0, - sortNo, - name, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(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("Now reading Property.csv") + reader = self.load_csv_file("Property.csv") + if reader: + for row in reader: + self.logger.info(f"Adding property {row['PropertyId']}") + await self.data.static.put_property(row) - self.logger.info(f"Added hero {heroLogId} | Name: {name}") - - try: - await self.data.static.put_hero( - 0, - heroLogId, - name, - nickname, - rarity, - skillTableSubId, - awakeningExp, - flavorText, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(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 + reader = self.load_csv_file("Equipment.csv") + if reader: + for row in reader: + self.logger.info(f"Adding equipment {row['EquipmentId']}") + await self.data.static.put_equipment(row) - self.logger.info(f"Added equipment {equipmentId} | Name: {name}") - - try: - await self.data.static.put_equipment( - 0, - equipmentId, - name, - equipmentType, - weaponTypeId, - rarity, - flavorText, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping") + self.logger.info("Now reading Skill.csv") + reader = self.load_csv_file("Skill.csv") + if reader: + for row in reader: + self.logger.info(f"Adding skill {row['SkillId']}") + await self.data.static.put_skill(row) + + self.logger.info("Now reading SkillTable.csv") + reader = self.load_csv_file("SkillTable.csv") + if reader: + for row in reader: + self.logger.info(f"Adding skill table {row['SkillId']} | SubId: {row['SkillTableSubId']}") + await self.data.static.put_skill_table(row['SkillId'], row['SkillTableSubId'], row['Level'], row['AwakeningId'], row['SkillTableId']) + + self.logger.info("Now reading HeroLog.csv") + reader = self.load_csv_file("HeroLog.csv") + if reader: + for row in reader: + self.logger.info(f"Adding hero {row['HeroLogId']}") + await self.data.static.put_hero(row) 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 + reader = self.load_csv_file("Item.csv") + if reader: + for row in reader: + self.logger.info(f"Adding item {row['ItemId']}") + await self.data.static.put_item(row) - self.logger.info(f"Added item {itemId} | Name: {name}") - - try: - await self.data.static.put_item( - 0, - itemId, - name, - itemTypeId, - rarity, - flavorText, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(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 + reader = self.load_csv_file("SupportLog.csv") + if reader: + 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"Adding support log {supportLogId}") + await self.data.static.put_support_log( + 0, + supportLogId, + charaId, + name, + rarity, + salePrice, + skillName, + enabled + ) - self.logger.info(f"Added support log {supportLogId} | Name: {name}") - - try: - await self.data.static.put_support_log( - 0, - supportLogId, - charaId, - name, - rarity, - salePrice, - skillName, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(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 + reader = self.load_csv_file("Title.csv") + if reader: + 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: - await self.data.static.put_title( - 0, - titleId, - displayName, - requirement, - rank, - imageFilePath, - enabled - ) - except Exception as err: - self.logger.error(err) - elif len(titleId) < 6: # current server code cannot have multiple lengths for the id - continue - except Exception: - self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping") + self.logger.info(f"Adding title {titleId}") + await self.data.static.put_title( + 0, + titleId, + displayName, + requirement, + rank, + imageFilePath, + enabled + ) - self.logger.info("Now reading RareDropTable.csv") - try: - fullPath = bin_dir + "/RareDropTable.csv" - with open(fullPath, encoding="UTF-8") as fp: - reader = csv.DictReader(fp) - for row in reader: - questRareDropId = row["QuestRareDropId"] - commonRewardId = row["CommonRewardId"] - enabled = True + self.logger.info("Now reading QuestRareDrop.csv") + reader = self.load_csv_file("QuestRareDrop.csv") + if reader: + for row in reader: + questRareDropId = row["QuestRareDropId"] + commonRewardId = row["CommonRewardId"] + enabled = True - self.logger.info(f"Added rare drop {questRareDropId} | Reward: {commonRewardId}") - - try: - await self.data.static.put_rare_drop( - 0, - questRareDropId, - commonRewardId, - enabled - ) - except Exception as err: - self.logger.error(err) - except Exception: - self.logger.warning(f"Couldn't read csv file in {self.bin_dir}, skipping") + self.logger.info(f"Adding rare drop {questRareDropId} | Reward: {commonRewardId}") + await self.data.static.put_rare_drop( + 0, + questRareDropId, + commonRewardId, + enabled + ) + + self.logger.info("Now reading RewardTable.csv") + reader = self.load_csv_file("RewardTable.csv") + if reader: + for row in reader: + self.logger.info(f"Adding reward table {row['RewardTableId']} | Sub-ID: {row['RewardTableSubId']} | Reward {row['CommonRewardId']}") + await self.data.static.put_reward_table(row) + + self.logger.info("Now reading ExBonusTable.csv") + reader = self.load_csv_file("ExBonusTable.csv") + if reader: + for row in reader: + self.logger.info(f"Adding ex bonus {row['ExBonusTableId']} | Sub-ID: {row['ExBonusTableSubId']} | Reward {row['CommonRewardId']}") + await self.data.static.put_ex_bonus(row) + + self.logger.info("Now reading PlayerTraceTable.csv") + reader = self.load_csv_file("PlayerTraceTable.csv") + if reader: + for row in reader: + self.logger.info(f"Adding trace table {row['PlayerTraceTableId']} | Sub-ID: {row['PlayerTraceTableSubId']} | Reward {row['CommonRewardId']}") + await self.data.static.put_player_trace(row) + + self.logger.info("Now reading Episode.csv") + reader = self.load_csv_file("Episode.csv") + if reader: + for row in reader: + self.logger.info(f"Adding episode {row['EpisodeId']}") + await self.data.static.put_episode(row) + + self.logger.info("Now reading TrialTower.csv") + reader = self.load_csv_file("TrialTower.csv") + if reader: + for row in reader: + self.logger.info(f"Adding tower {row['TrialTowerId']}") + await self.data.static.put_tower(row) + + self.logger.info("Now reading ExTowerQuests.csv") + reader = self.load_csv_file("ExTowerQuests.csv") + if reader: + for row in reader: + self.logger.info(f"Adding ex tower {row['ExTowerQuestId']}") + await self.data.static.put_ex_tower(row) + + self.logger.info("Now reading SideQuest.csv") + reader = self.load_csv_file("SideQuest.csv") + if reader: + for row in reader: + self.logger.info(f"Adding side quest {row['SideQuestId']}") + await self.data.static.put_side_quest(row) diff --git a/titles/sao/schema/item.py b/titles/sao/schema/item.py index 332e135..b0c58e9 100644 --- a/titles/sao/schema/item.py +++ b/titles/sao/schema/item.py @@ -1,6 +1,6 @@ from typing import Optional, Dict, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BOOLEAN, INTEGER, BIGINT from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select, update, delete from sqlalchemy.engine import Row @@ -17,15 +17,30 @@ equipment_data = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("equipment_id", Integer, nullable=False), + Column("equipment_id", BIGINT, ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade"), nullable=False), Column("enhancement_value", Integer, nullable=False), Column("enhancement_exp", Integer, nullable=False), Column("awakening_exp", Integer, nullable=False), Column("awakening_stage", Integer, nullable=False), Column("possible_awakening_flag", Integer, nullable=False), + Column("is_shop_purchase", BOOLEAN, nullable=False, server_default="0"), + Column("is_protect", BOOLEAN, nullable=False, server_default="0"), + Column("property1_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property1_value1", INTEGER, nullable=False, server_default="0"), + Column("property1_value2", INTEGER, nullable=False, server_default="0"), + Column("property2_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property2_value1", INTEGER, nullable=False, server_default="0"), + Column("property2_value2", INTEGER, nullable=False, server_default="0"), + Column("property3_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property3_value1", INTEGER, nullable=False, server_default="0"), + Column("property3_value2", INTEGER, nullable=False, server_default="0"), + Column("property4_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property4_value1", INTEGER, nullable=False, server_default="0"), + Column("property4_value2", INTEGER, nullable=False, server_default="0"), + Column("converted_card_num", INTEGER, nullable=False, server_default="0"), Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), UniqueConstraint("user", "equipment_id", name="sao_equipment_data_uk"), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) item_data = Table( @@ -40,7 +55,7 @@ item_data = Table( Column("item_id", Integer, nullable=False), Column("get_date", TIMESTAMP, nullable=False, server_default=func.now()), UniqueConstraint("user", "item_id", name="sao_item_data_uk"), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) hero_log_data = Table( @@ -52,19 +67,38 @@ hero_log_data = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("user_hero_log_id", Integer, nullable=False), + Column("hero_log_id", BIGINT, ForeignKey("sao_static_hero_list.HeroLogId", ondelete="cascade", onupdate="cascade"), 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("main_weapon", INTEGER, ForeignKey("sao_equipment_data.id", ondelete="set null", onupdate="set null")), + Column("sub_equipment", INTEGER, ForeignKey("sao_equipment_data.id", ondelete="set null", onupdate="set null")), + Column("skill_slot1_skill_id", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="set null", onupdate="set null")), + Column("skill_slot2_skill_id", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="set null", onupdate="set null")), + Column("skill_slot3_skill_id", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="set null", onupdate="set null")), + Column("skill_slot4_skill_id", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="set null", onupdate="set null")), + Column("skill_slot5_skill_id", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="set null", onupdate="set null")), + Column("max_level_extend_num", INTEGER, nullable=False, server_default="0"), + Column("is_awakenable", BOOLEAN, nullable=False, server_default="0"), + Column("awakening_stage", INTEGER, nullable=False, server_default="0"), + Column("awakening_exp", INTEGER, nullable=False, server_default="0"), + Column("is_shop_purchase", BOOLEAN, nullable=False, server_default="0"), + Column("is_protect", BOOLEAN, nullable=False, server_default="0"), + Column("property1_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property1_value1", INTEGER, nullable=False, server_default="0"), + Column("property1_value2", INTEGER, nullable=False, server_default="0"), + Column("property2_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property2_value1", INTEGER, nullable=False, server_default="0"), + Column("property2_value2", INTEGER, nullable=False, server_default="0"), + Column("property3_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property3_value1", INTEGER, nullable=False, server_default="0"), + Column("property3_value2", INTEGER, nullable=False, server_default="0"), + Column("property4_property_id", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False, server_default="2"), + Column("property4_value1", INTEGER, nullable=False, server_default="0"), + Column("property4_value2", INTEGER, nullable=False, server_default="0"), + Column("converted_card_num", INTEGER, nullable=False, server_default="0"), 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", + UniqueConstraint("user", "hero_log_id", name="sao_hero_log_data_uk"), + mysql_charset="utf8mb4" ) hero_party = Table( @@ -77,11 +111,11 @@ hero_party = Table( 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), + Column("user_hero_log_id_1", Integer, ForeignKey("sao_hero_log_data.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("user_hero_log_id_2", Integer, ForeignKey("sao_hero_log_data.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("user_hero_log_id_3", Integer, ForeignKey("sao_hero_log_data.id", ondelete="cascade", onupdate="cascade"), nullable=False), UniqueConstraint("user", "user_party_team_id", name="sao_hero_party_uk"), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) quest = Table( @@ -93,15 +127,28 @@ quest = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("episode_id", Integer, nullable=False), + Column("quest_type", INTEGER, nullable=False, server_default="1"), + Column("quest_scene_id", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade"), nullable=False), Column("quest_clear_flag", Boolean, nullable=False), Column("clear_time", Integer, nullable=False), Column("combo_num", Integer, nullable=False), Column("total_damage", Integer, nullable=False), Column("concurrent_destroying_num", Integer, nullable=False), Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), - UniqueConstraint("user", "episode_id", name="sao_player_quest_uk"), - mysql_charset="utf8mb4", + UniqueConstraint("user", "quest_scene_id", name="sao_player_quest_uk"), + mysql_charset="utf8mb4" +) + +ex_bonus = Table( + "sao_player_ex_bonus", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("quest_scene_id", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("ex_bonus_table_id", BIGINT, ForeignKey("sao_static_ex_bonus.ExBonusTableId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("quest_clear_flag", BOOLEAN, nullable=False, server_default="0"), + UniqueConstraint("user", "quest_scene_id", "ex_bonus_table_id", name="sao_player_ex_bonus_uk"), + mysql_charset="utf8mb4" ) sessions = Table( @@ -114,12 +161,12 @@ sessions = Table( nullable=False, ), Column("user_party_team_id", Integer, nullable=False), - Column("episode_id", Integer, nullable=False), + Column("episode_id", Integer, nullable=False), # TODO: Change to quest scene id 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", + mysql_charset="utf8mb4" ) end_sessions = Table( @@ -135,7 +182,7 @@ end_sessions = Table( Column("play_result_flag", Boolean, nullable=False), Column("reward_data", JSON, nullable=True), Column("play_date", TIMESTAMP, nullable=False, server_default=func.now()), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) class SaoItemData(BaseData): @@ -148,13 +195,17 @@ class SaoItemData(BaseData): quest_drop_boost_apply_flag=quest_drop_boost_apply_flag ) - conflict = sql.on_duplicate_key_update(user=user_id) + conflict = sql.on_duplicate_key_update( + 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 + ) result = await 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 + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to create SAO session for user {user_id}!") async def create_end_session(self, user_id: int, quest_id: int, play_result_flag: bool, reward_data: JSON) -> Optional[int]: sql = insert(end_sessions).values( @@ -164,13 +215,16 @@ class SaoItemData(BaseData): reward_data=reward_data, ) - conflict = sql.on_duplicate_key_update(user=user_id) + conflict = sql.on_duplicate_key_update( + play_result_flag=play_result_flag, + reward_data=reward_data + ) result = await self.execute(conflict) - if result is None: - self.logger.error(f"Failed to create SAO end session for user {user_id}!") - return None - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + + self.logger.error(f"Failed to create SAO end session for user {user_id}!") async def put_item(self, user_id: int, item_id: int) -> Optional[int]: sql = insert(item_data).values( @@ -178,51 +232,93 @@ class SaoItemData(BaseData): item_id=item_id, ) - conflict = sql.on_duplicate_key_update( - item_id=item_id, - ) + conflict = sql.on_duplicate_key_update(item_id=item_id) result = await self.execute(conflict) - if result is None: - self.logger.error( - f"{__name__} failed to insert item! user: {user_id}, item_id: {item_id}" - ) - return None - - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert item! user: {user_id}, item_id: {item_id}") - async def put_equipment_data(self, user_id: int, equipment_id: int, enhancement_value: int, enhancement_exp: int, awakening_exp: int, awakening_stage: int, possible_awakening_flag: int) -> Optional[int]: + async def put_equipment(self, user_id: int, equipment_id: int) -> Optional[int]: sql = insert(equipment_data).values( user=user_id, equipment_id=equipment_id, - enhancement_value=enhancement_value, - enhancement_exp=enhancement_exp, - awakening_exp=awakening_exp, - awakening_stage=awakening_stage, - possible_awakening_flag=possible_awakening_flag, + enhancement_value=1, + enhancement_exp=200, + awakening_exp=0, + awakening_stage=0, + possible_awakening_flag=0, ) - conflict = sql.on_duplicate_key_update( - enhancement_value=enhancement_value, - enhancement_exp=enhancement_exp, - awakening_exp=awakening_exp, - awakening_stage=awakening_stage, - possible_awakening_flag=possible_awakening_flag, - ) + conflict = sql.on_duplicate_key_update(equipment_id=equipment_id) result = await self.execute(conflict) - if result is None: - self.logger.error( - f"{__name__} failed to insert equipment! user: {user_id}, equipment_id: {equipment_id}" - ) - return None + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert equipment! user: {user_id}, equipment_id: {equipment_id}") + + async def put_ex_bonus(self, user_id: int, quest_scene_id: int, ex_bonus_id: int, clear: bool = False) -> Optional[int]: + sql = insert(ex_bonus).values( + user=user_id, + quest_scene_id=quest_scene_id, + ex_bonus_table_id=ex_bonus_id, + quest_clear_flag=clear, + ) - return result.lastrowid + conflict = sql.on_duplicate_key_update(quest_clear_flag=clear) + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert ex bonus status! user: {user_id}, quest_scene_id: {quest_scene_id}, ex_bonus_id: {ex_bonus_id}, clear: {clear}") + + async def add_equipment_enhancement_exp(self, user_weapon_id: int, enhancement_exp: int) -> None: + result = await self.execute( + equipment_data.update(equipment_data.c.id == user_weapon_id) + .values(enhancement_exp=equipment_data.c.enhancement_exp + enhancement_exp) + ) + if not result: + self.logger.error(f"Failed to give equipment {user_weapon_id} {enhancement_exp} xp!") + + async def get_equipment_enhancement_exp(self, user_weapon_id: int) -> Optional[int]: + result = await self.execute(select(equipment_data.c.enhancement_exp).where(equipment_data.c.id==user_weapon_id)) + if result: + row = result.fetchone() + if row: + return row['enhancement_exp'] + return 0 + self.logger.error(f"Failed to get equipment {user_weapon_id} xp!") + + async def set_equipment_enhancement_value(self, user_equip_id: int, enhancement_val: int) -> None: + result = await self.execute(equipment_data.update(equipment_data.c.id == user_equip_id).values(enhancement_value=enhancement_val)) + if result is None: + self.logger.error(f"Failed to set equipment {user_equip_id} level to {enhancement_val}!") + + async def add_hero_xp(self, user_hero_log_id: int, add_xp: int) -> Optional[int]: + result = await self.execute( + hero_log_data.update(hero_log_data.c.id == user_hero_log_id) + .values(log_exp=hero_log_data.c.log_exp + add_xp) + ) + if not result: + self.logger.error(f"Failed to give hero {user_hero_log_id} {add_xp} xp!") + + async def get_hero_xp(self, user_hero_log_id: int) -> Optional[int]: + result = await self.execute(select(hero_log_data.c.log_exp).where(hero_log_data.c.id==user_hero_log_id)) + if result: + row = result.fetchone() + if row: + return row['log_exp'] + return 0 + self.logger.error(f"Failed to get hero xp for {user_hero_log_id}") + + async def set_hero_level(self, user_hero_log_id: int, new_level: int): + result = await self.execute(hero_log_data.update(hero_log_data.c.id == user_hero_log_id).values(log_level=new_level)) + if result is None: + self.logger.error(f"Failed to set hero {user_hero_log_id} level to {new_level}!") async 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, + hero_log_id=user_hero_log_id, log_level=log_level, log_exp=log_exp, main_weapon=main_weapon, @@ -243,17 +339,36 @@ class SaoItemData(BaseData): 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, + skill_slot5_skill_id=skill_slot5_skill_id ) result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert hero! user: {user_id}, user_hero_log_id: {user_hero_log_id}") + + async def set_user_hero_weapons(self, user_hero_id: int, main_weapon: int, sub_weapon: int) -> None: + sql = hero_log_data.update(hero_log_data.c.id == user_hero_id).values( + main_weapon=main_weapon, + sub_equipment=sub_weapon, + ) + result = await self.execute(sql) if result is None: - self.logger.error( - f"{__name__} failed to insert hero! user: {user_id}, user_hero_log_id: {user_hero_log_id}" - ) + self.logger.error(f"Failed to update user hero {user_hero_id} weapons {main_weapon}/{sub_weapon}") return None - return result.lastrowid + async def set_user_hero_skills(self, user_hero_id: int, skill1: int, skill2: int, skill3: int, skill4: int, skill5: int) -> None: + sql = hero_log_data.update(hero_log_data.c.id == user_hero_id).values( + skill_slot1_skill_id = skill1, + skill_slot2_skill_id = skill2, + skill_slot3_skill_id = skill3, + skill_slot4_skill_id = skill4, + skill_slot5_skill_id = skill5, + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to update user hero {user_hero_id} skills {skill1}/{skill2}/{skill3}/{skill4}/{skill5}") + return None async 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( @@ -267,22 +382,19 @@ class SaoItemData(BaseData): 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, + user_hero_log_id_3=user_hero_log_id_3 ) result = await 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 + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert hero party! user: {user_id}, user_party_team_id: {user_party_team_id}") - return result.lastrowid - - async def put_player_quest(self, user_id: int, episode_id: int, quest_clear_flag: bool, clear_time: int, combo_num: int, total_damage: int, concurrent_destroying_num: int) -> Optional[int]: + async def put_player_quest(self, user_id: int, quest_type: int, quest_scene_id: int, quest_clear_flag: bool, clear_time: int, combo_num: int, total_damage: int, concurrent_destroying_num: int) -> Optional[int]: sql = insert(quest).values( user=user_id, - episode_id=episode_id, + quest_type=quest_type, + quest_scene_id=quest_scene_id, quest_clear_flag=quest_clear_flag, clear_time=clear_time, combo_num=combo_num, @@ -299,13 +411,9 @@ class SaoItemData(BaseData): ) result = await self.execute(conflict) - if result is None: - self.logger.error( - f"{__name__} failed to insert quest! user: {user_id}, episode_id: {episode_id}" - ) - return None - - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert quest! user: {user_id}, quest_scene_id: {quest_scene_id}") async def get_user_equipment(self, user_id: int, equipment_id: int) -> Optional[Dict]: sql = equipment_data.select(equipment_data.c.user == user_id and equipment_data.c.equipment_id == equipment_id) @@ -315,6 +423,14 @@ class SaoItemData(BaseData): return None return result.fetchone() + async def get_user_equipment_by_id(self, equipment_user_id: int) -> Optional[Row]: + sql = equipment_data.select(equipment_data.c.id == equipment_user_id) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchone() + async def get_user_equipments( self, user_id: int ) -> Optional[List[Row]]: @@ -349,6 +465,38 @@ class SaoItemData(BaseData): return None return result.fetchall() + async def get_player_ex_bonus_status(self, user_id: int) -> Optional[List[Row]]: + sql = equipment_data.select(ex_bonus.c.user == user_id) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def get_player_ex_bonus_by_quest(self, user_id: int, quest_scene_id) -> Optional[List[Row]]: + sql = ex_bonus.select(and_(ex_bonus.c.user == user_id, ex_bonus.c.quest_scene_id == quest_scene_id)) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def get_user_item_by_id( + self, user_item_id: int + ) -> Optional[Row]: + sql = item_data.select(item_data.c.id == user_item_id) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchone() + + async def get_user_hero_by_id(self, user_hero_id: int) -> Optional[Row]: + result = await self.execute(hero_log_data.select(hero_log_data.c.id == user_hero_id)) + if result is None: + return None + return result.fetchone() + async def get_hero_log( self, user_id: int, user_hero_log_id: int = None ) -> Optional[List[Row]]: @@ -358,7 +506,7 @@ class SaoItemData(BaseData): 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, + hero_log_data.c.hero_log_id == user_hero_log_id if user_hero_log_id is not None else True, ) ) @@ -366,6 +514,12 @@ class SaoItemData(BaseData): if result is None: return None return result.fetchone() + + async def get_hero_log_by_id(self, user_hero_log_id: int) -> Optional[Row]: + result = await self.execute(hero_log_data.select(hero_log_data.c.id == user_hero_log_id)) + if result is None: + return None + return result.fetchone() async def get_hero_logs( self, user_id: int @@ -373,17 +527,17 @@ class SaoItemData(BaseData): """ A catch-all hero lookup given a profile """ - sql = hero_log_data.select( - and_( - hero_log_data.c.user == user_id, - ) - ) - - result = await self.execute(sql) + result = await self.execute(hero_log_data.select(hero_log_data.c.user == user_id)) if result is None: return None return result.fetchall() + async def get_hero_party_by_id(self, party_id: int) -> Optional[Row]: + result = await self.execute(hero_party.select(hero_party.c.id == party_id)) + if result is None: + return None + return result.fetchone() + async def get_hero_party( self, user_id: int, user_party_team_id: int = None ) -> Optional[List[Row]]: @@ -397,18 +551,18 @@ class SaoItemData(BaseData): result = await self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() async def get_quest_log( - self, user_id: int, episode_id: int = None - ) -> Optional[List[Row]]: + self, user_id: int, quest_scene_id: int + ) -> Optional[Row]: """ - A catch-all quest lookup given a profile and episode_id + A catch-all quest lookup given a profile and quest_scene_id """ sql = quest.select( and_( quest.c.user == user_id, - quest.c.episode_id == episode_id if episode_id is not None else True, + quest.c.quest_scene_id == quest_scene_id ) ) @@ -420,16 +574,7 @@ class SaoItemData(BaseData): async def get_quest_logs( self, user_id: int ) -> Optional[List[Row]]: - """ - A catch-all quest lookup given a profile - """ - sql = quest.select( - and_( - quest.c.user == user_id, - ) - ) - - result = await self.execute(sql) + result = await self.execute(quest.select(quest.c.user == user_id)) if result is None: return None return result.fetchall() @@ -437,13 +582,7 @@ class SaoItemData(BaseData): async 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() - ) + sql = sessions.select(sessions.c.user == user_id).order_by(sessions.c.play_date.desc()) result = await self.execute(sql) if result is None: @@ -453,54 +592,36 @@ class SaoItemData(BaseData): async def get_end_session( self, user_id: int = None ) -> Optional[List[Row]]: - sql = end_sessions.select( - and_( - end_sessions.c.user == user_id, - ) - ).order_by( - end_sessions.c.play_date.desc() - ) + sql = end_sessions.select(end_sessions.c.user == user_id).order_by(end_sessions.c.play_date.desc()) result = await self.execute(sql) if result is None: return None return result.fetchone() - - async def remove_hero_log(self, user_id: int, user_hero_log_id: int) -> None: - sql = hero_log_data.delete( - and_( - hero_log_data.c.user == user_id, - hero_log_data.c.user_hero_log_id == user_hero_log_id, - ) - ) + + async def remove_end_session(self, end_id: int) -> None: + result = await self.execute(end_sessions.delete(end_sessions.c.id == end_id)) + if result is None: + self.logger.error(f"Failed to delete end session {end_id}") + + async def remove_hero_log(self, user_hero_log_id: int) -> None: + sql = hero_log_data.delete(hero_log_data.c.id == user_hero_log_id) result = await self.execute(sql) - if result is None: - self.logger.error( - f"{__name__} failed to remove hero log! profile: {user_id}, user_hero_log_id: {user_hero_log_id}" - ) - return None + if not result: + self.logger.error(f"Failed to remove hero log id: {user_hero_log_id}") - async def remove_equipment(self, user_id: int, equipment_id: int) -> None: - sql = equipment_data.delete( - and_(equipment_data.c.user == user_id, equipment_data.c.equipment_id == equipment_id) - ) + async def remove_equipment(self, equipment_id: int) -> None: + sql = equipment_data.delete(equipment_data.c.id == equipment_id) result = await self.execute(sql) - if result is None: - self.logger.error( - f"{__name__} failed to remove equipment! profile: {user_id}, equipment_id: {equipment_id}" - ) - return None + if not result: + self.logger.error(f"Failed to remove equipment id {equipment_id}") - async def remove_item(self, user_id: int, item_id: int) -> None: - sql = item_data.delete( - and_(item_data.c.user == user_id, item_data.c.item_id == item_id) - ) + async def remove_item(self, user_item_id: int) -> None: + sql = item_data.delete(item_data.c.id == user_item_id) result = await self.execute(sql) - if result is None: - self.logger.error( - f"{__name__} failed to remove item! profile: {user_id}, item_id: {item_id}" - ) - return None \ No newline at end of file + if not result: + self.logger.error(f"Failed to remove item {user_item_id}!") + diff --git a/titles/sao/schema/profile.py b/titles/sao/schema/profile.py index 27d284d..4a6df93 100644 --- a/titles/sao/schema/profile.py +++ b/titles/sao/schema/profile.py @@ -1,13 +1,12 @@ -from typing import Optional, Dict, List +from typing import Optional, Tuple, List from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy.types import Integer, String, BOOLEAN, INTEGER, BIGINT, VARCHAR, TIMESTAMP from sqlalchemy.schema import ForeignKey -from sqlalchemy.sql import func, select, update, delete +from sqlalchemy.sql import func, select 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", @@ -24,9 +23,70 @@ profile = Table( 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="300"), + Column("own_vp", Integer, server_default="0"), Column("own_yui_medal", Integer, server_default="0"), Column("setting_title_id", Integer, server_default="20005"), + Column("my_shop", INTEGER), + Column("fav_hero", INTEGER, ForeignKey("sao_hero_log_data.id", ondelete="set null", onupdate="cascade")), + Column("when_register", TIMESTAMP, server_default=func.now()), + Column("last_login_date", TIMESTAMP), + Column("last_yui_medal_date", TIMESTAMP), + Column("last_bonus_yui_medal_date", TIMESTAMP), + Column("last_comeback_date", TIMESTAMP), + Column("last_login_bonus_date", TIMESTAMP), + Column("ad_confirm_date", TIMESTAMP), + Column("login_ct", INTEGER, server_default="0"), + mysql_charset="utf8mb4" +) + +beginner_mission = Table( + "sao_player_beginner_mission", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True), + Column("beginner_mission_id", INTEGER, nullable=False), + Column("condition_id", INTEGER, nullable=False), + Column("is_seat", BOOLEAN, nullable=False, server_default="0"), + Column("achievement_num", INTEGER, nullable=False), + Column("complete_flag", BOOLEAN, nullable=False, server_default="0"), + Column("complete_date", TIMESTAMP), + Column("reward_received_flag", BOOLEAN, nullable=False, server_default="0"), + Column("reward_received_date", TIMESTAMP), + UniqueConstraint("user", "condition_id", name="sao_player_beginner_mission_uk"), + mysql_charset="utf8mb4" +) + +resource_card = Table( + "sao_player_resource_card", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("common_reward_type", INTEGER, nullable=False), + Column("common_reward_id", INTEGER, nullable=False), + Column("holographic_flag", BOOLEAN, nullable=False, server_default="0"), + Column("serial", VARCHAR(20), unique=True), + mysql_charset="utf8mb4" +) + +hero_card = Table( + "sao_player_hero_card", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("user_hero_id", INTEGER, ForeignKey("sao_hero_log_data.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("holographic_flag", BOOLEAN, nullable=False, server_default="0"), + Column("serial", VARCHAR(20), unique=True), + mysql_charset="utf8mb4" +) + +tutorial = Table( + "sao_player_tutorial", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("tutorial_byte", INTEGER, nullable=False), + UniqueConstraint("user", "tutorial_byte", name="sao_player_tutorial_uk"), + mysql_charset="utf8mb4" ) class SaoProfileData(BaseData): @@ -35,10 +95,81 @@ class SaoProfileData(BaseData): conflict = sql.on_duplicate_key_update(user=user_id) result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error("Failed to create SAO profile!") + + async def set_my_shop(self, user_id: int, store_id: int): + result = await self.execute(profile.update(profile.c.user == user_id).values(my_shop = store_id)) if result is None: - self.logger.error(f"Failed to create SAO profile for user {user_id}!") - return None - return result.lastrowid + self.logger.error(f"Failed to set my shop for user {user_id} to {store_id}!") + + async def user_login(self, user_id: int) -> Optional[Row]: + sql = profile.update(profile.c.user == user_id).values( + login_ct=profile.c.login_ct + 1, + last_login_date=func.now() + ) + result = await self.execute(sql) + if result: + return result.last_updated_params() + self.logger.error(f"Failed to create log in user {user_id}!") + + async def update_yui_medal_date(self, user_id: int) -> None: + sql = profile.update(profile.c.user == user_id).values( + last_yui_medal_date=func.now() + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to update user {user_id} yui medal date!") + + async def add_yui_medals(self, user_id: int, num_medals: int = 1): + sql = profile.update(profile.c.user == user_id).values( + own_yui_medal=profile.c.own_yui_medal + num_medals + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to give user {user_id} {num_medals} yui medals!") + + async def add_col(self, user_id: int, num_col: int) -> None: + sql = profile.update(profile.c.user == user_id).values( + own_col=profile.c.own_col + num_col + ) + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to give user {user_id} {num_col} Col!") + + async def add_vp(self, user_id: int, num_vp: int) -> None: + sql = profile.update(profile.c.user == user_id).values( + own_vp=profile.c.own_vp + num_vp + ) + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to give user {user_id} {num_vp} VP!") + + async def add_exp(self, user_id: int, xp_ammount: int) -> Optional[int]: + sql = profile.update(profile.c.user == user_id).values( + rank_exp=profile.c.rank_exp + xp_ammount + ) + result = await self.execute(sql) + if not result: + self.logger.error(f"Failed to give user {user_id} {xp_ammount} xp!") + + async def get_exp(self, user_id: int) -> Optional[int]: + result = await self.execute(select(profile.c.rank_exp).where(profile.c.user==user_id)) + if result: + row = result.fetchone() + if row: + return row['rank_exp'] + return 0 + self.logger.error(f"Failed to query rank xp for user {user_id}") + + async def set_level(self, user_id: int, level: int): + sql = profile.update(profile.c.user == user_id).values( + rank_num=level + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to set user {user_id} level to {level}!") async 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( @@ -63,17 +194,118 @@ class SaoProfileData(BaseData): ) result = await self.execute(conflict) - if result is None: - self.logger.error( - f"{__name__} failed to insert profile! user: {user_id}" - ) - return None - - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert profile! user: {user_id}") async def get_profile(self, user_id: int) -> Optional[Row]: sql = profile.select(profile.c.user == user_id) result = await self.execute(sql) if result is None: return None - return result.fetchone() \ No newline at end of file + return result.fetchone() + + async def set_profile_name(self, user_id: int, new_name: str) -> None: + sql = profile.update(profile.c.user == user_id).values( + nick_name=new_name + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to update nickname {new_name} for user {user_id}") + + async def add_tutorial_byte(self, user_id: int, tutorial_byte: int) -> None: + sql = insert(tutorial).values( + user = user_id, + tutorial_byte = tutorial_byte + ) + + conflict = sql.on_duplicate_key_update(tutorial_byte = tutorial_byte) + result = await self.execute(conflict) + if result is None: + self.logger.error(f"Failed to add tutorial byte {tutorial_byte} to user {user_id}") + + async def get_tutorial_bytes(self, user_id: int) -> Optional[List[Row]]: + sql = tutorial.select(tutorial.c.user == user_id) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() + + async def put_hero_card(self, user_id: int, serial: str, user_hero_id: int, is_holo: bool) -> Optional[int]: + sql = insert(hero_card).values( + user=user_id, + user_hero_id=user_hero_id, + holographic_flag=is_holo, + serial=serial + ) + + conflict = sql.on_duplicate_key_update( + holographic_flag=is_holo, + serial=serial + ) + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert card {serial} for user {user_id} as hero {user_hero_id}") + + async def get_hero_card(self, serial: str) -> Optional[Row]: + result = await self.execute(hero_card.select(hero_card.c.serial == serial)) + if result is None: + return None + return result.fetchone() + + async def put_resource_card(self, user_id: int, serial: str, reward_type: int, reward_id: int, is_holo: bool) -> Optional[int]: + sql = insert(resource_card).values( + user=user_id, + common_reward_type=reward_type, + common_reward_id=reward_id, + holographic_flag=is_holo, + serial=serial + ) + + conflict = sql.on_duplicate_key_update( + holographic_flag=is_holo, + serial=serial + ) + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert card {serial} for user {user_id} as resource {reward_id}") + + async def get_resource_card(self, serial: str) -> Optional[int]: + result = await self.execute(resource_card.select(resource_card.c.serial == serial)) + if result is None: + return None + return result.fetchone() + + async def update_beginner_mission_date(self, user_id: int) -> None: + sql = profile.update(profile.c.user == user_id).values( + ad_confirm_date=func.now() + ) + result = await self.execute(sql) + if result is None: + self.logger.error(f"Failed to update user {user_id} yui medal date!") + + async def put_beginner_mission(self, user_id: int, beginner_mission_id: int, condition_id: int, is_seat: bool, achievement_num: int) -> Optional[int]: + pass + + async def complete_beginner_mission(self, user_id: int, condition_id: int) -> None: + pass + + async def reward_beginner_mission(self, user_id: int, condition_id: int) -> None: + pass + + async def get_beginner_missions(self, user_id: int) -> Optional[List[Row]]: + pass + + async def get_beginner_missions_by_mission_id(self, user_id: int, beginner_mission_id: int) -> Optional[List[Row]]: + pass + + async def get_beginner_mission(self, user_id: int, condition_id: int) -> Optional[Row]: + pass + + async def set_title(self, user_id: int, title_id: int) -> None: + result = await self.execute(profile.update(profile.c.user == user_id).values(setting_title_id=title_id)) + if not result: + self.logger.error(f"Failed to set user {user_id} title to {title_id}") diff --git a/titles/sao/schema/static.py b/titles/sao/schema/static.py index 8b5b1a1..7f4f679 100644 --- a/titles/sao/schema/static.py +++ b/titles/sao/schema/static.py @@ -1,81 +1,193 @@ -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 typing import Dict, List, Optional, Union +from sqlalchemy import Table, Column, UniqueConstraint, ForeignKey +from sqlalchemy.types import Integer, String, BIGINT, Boolean, INTEGER, VARCHAR, BOOLEAN, DECIMAL 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 +from core.data import cached 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", + Column("QuestSceneId", BIGINT, primary_key=True, nullable=False), + Column("SortNo", INTEGER, nullable=False), + Column("Tutorial", BOOLEAN, nullable=False), + Column("ColRate", DECIMAL, nullable=False), + Column("LimitDefault", INTEGER, nullable=False), + Column("LimitResurrection", INTEGER, nullable=False), + Column("RewardTableSubId", INTEGER, nullable=False), + Column("PlayerTraceTableSubId", INTEGER, nullable=False), + Column("SuccessPlayerExp", INTEGER, nullable=False), + Column("FailedPlayerExp", INTEGER, nullable=False), + Column("PairExpRate", INTEGER, nullable=False), + Column("TrioExpRate", INTEGER, nullable=False), + Column("SingleRewardVp", INTEGER, nullable=False), + Column("PairRewardVp", INTEGER, nullable=False), + Column("TrioRewardVp", INTEGER, nullable=False), + mysql_charset="utf8mb4" ) -hero = Table( - "sao_static_hero_list", +prop = Table( + "sao_static_property", 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", + Column("PropertyId", BIGINT, primary_key=True, nullable=False), + Column("PropertyTargetType", INTEGER, nullable=False), + Column("PropertyName", VARCHAR(255), nullable=False), + Column("PropertyName_en", VARCHAR(255)), + Column("PropertyNameFormat", VARCHAR(255), nullable=False), + Column("PropertyNameFormat_en", VARCHAR(255)), + Column("PropertyTypeId", INTEGER, nullable=False), + Column("Value1Min", INTEGER, nullable=False), + Column("Value1Max", INTEGER, nullable=False), + Column("Value2Min", INTEGER, nullable=False), + Column("Value2Max", INTEGER, nullable=False), + mysql_charset="utf8mb4" +) + +player_trace = Table( + "sao_static_trace_table", + metadata, + Column("PlayerTraceTableId", BIGINT, primary_key=True, nullable=False), + Column("PlayerTraceTableSubId", INTEGER, nullable=False), + Column("CommonRewardType", INTEGER, nullable=False), + Column("CommonRewardId", INTEGER, nullable=False), + Column("CommonRewardNum", INTEGER, nullable=False), + Column("Rate", INTEGER, nullable=False), + mysql_charset="utf8mb4" +) + +skill = Table( + "sao_static_skill", + metadata, + Column("SkillId", BIGINT, nullable=False, primary_key=True), + Column("WeaponTypeId", INTEGER, nullable=False), + Column("Name", VARCHAR(255), nullable=False), + Column("Name_en", VARCHAR(255)), + Column("Attack", BOOLEAN, nullable=False), + Column("Passive", BOOLEAN, nullable=False), + Column("Pet", BOOLEAN, nullable=False), + Column("Level", INTEGER, nullable=False), + Column("SkillCondition", INTEGER, nullable=False), + Column("CoolTime", INTEGER, nullable=False), + Column("SkillIcon", VARCHAR(255), nullable=False), + Column("FriendSkillIcon", VARCHAR(255), nullable=False), + Column("InfoText", VARCHAR(255), nullable=False), + Column("InfoText_en", VARCHAR(255)), + mysql_charset="utf8mb4" +) + +skill_table = Table( + "sao_static_skill_table", + metadata, + Column("SkillTableId", BIGINT, nullable=False, primary_key=True), + Column("SkillId", BIGINT, ForeignKey("sao_static_skill.SkillId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("SkillTableSubId", INTEGER, nullable=False), + Column("LevelObtained", INTEGER, nullable=False), # Level + Column("AwakeningId", INTEGER, nullable=False), + 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", + Column("EquipmentId", BIGINT, primary_key=True, nullable=False), + Column("EquipmentType", INTEGER, nullable=False), + Column("WeaponTypeId", INTEGER, nullable=False), + Column("Name", VARCHAR(255), nullable=False), + Column("Name_en", VARCHAR(255)), + Column("Rarity", INTEGER, nullable=False), + Column("Power", INTEGER, nullable=False), + Column("StrengthIncrement", INTEGER, nullable=False), + Column("SkillCondition", INTEGER, nullable=False), + Column("Property1PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property1Value1", INTEGER, nullable=False), + Column("Property1Value2", INTEGER, nullable=False), + Column("Property2PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property2Value1", INTEGER, nullable=False), + Column("Property2Value2", INTEGER, nullable=False), + Column("Property3PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property3Value1", INTEGER, nullable=False), + Column("Property3Value2", INTEGER, nullable=False), + Column("Property4PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property4Value1", INTEGER, nullable=False), + Column("Property4Value2", INTEGER, nullable=False), + Column("SalePrice", INTEGER, nullable=False), + Column("CompositionExp", INTEGER, nullable=False), + Column("AwakeningExp", INTEGER, nullable=False), + Column("FlavorText", VARCHAR(255), nullable=False), + Column("FlavorText_en", VARCHAR(255)), + mysql_charset="utf8mb4" +) + +hero = Table( + "sao_static_hero_list", + metadata, + Column("HeroLogId", BIGINT, primary_key=True, nullable=False), + Column("CharaId", INTEGER, nullable=False), + Column("Name", VARCHAR(255), nullable=False), + Column("Nickname", VARCHAR(255), nullable=False), + Column("Name_en", VARCHAR(255)), + Column("Nickname_en", VARCHAR(255)), + Column("Rarity", INTEGER, nullable=False), + Column("WeaponTypeId", INTEGER, nullable=False), + Column("HeroLogRoleId", INTEGER, nullable=False), + Column("CostumeTypeId", INTEGER, nullable=False), + Column("UnitId", INTEGER, nullable=False), + Column("DefaultEquipmentId1", BIGINT, ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + Column("DefaultEquipmentId2", BIGINT, ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")), + Column("SkillTableSubId", INTEGER, nullable=False), + Column("HpMin", INTEGER, nullable=False), + Column("HpMax", INTEGER, nullable=False), + Column("StrMin", INTEGER, nullable=False), + Column("StrMax", INTEGER, nullable=False), + Column("VitMin", INTEGER, nullable=False), + Column("VitMax", INTEGER, nullable=False), + Column("IntMin", INTEGER, nullable=False), + Column("IntMax", INTEGER, nullable=False), + Column("Property1PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property1Value1", INTEGER, nullable=False), + Column("Property1Value2", INTEGER, nullable=False), + Column("Property2PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property2Value1", INTEGER, nullable=False), + Column("Property2Value2", INTEGER, nullable=False), + Column("Property3PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property3Value1", INTEGER, nullable=False), + Column("Property3Value2", INTEGER, nullable=False), + Column("Property4PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property4Value1", INTEGER, nullable=False), + Column("Property4Value2", INTEGER, nullable=False), + Column("FlavorText", VARCHAR(255), nullable=False), + Column("FlavorText_en", VARCHAR(255)), + Column("SalePrice", INTEGER, nullable=False), + Column("CompositionExp", INTEGER, nullable=False), + Column("AwakeningExp", INTEGER, nullable=False), + Column("Slot4UnlockLevel", INTEGER, nullable=False), + Column("Slot5UnlockLevel", INTEGER, nullable=False), + Column("CollectionEmptyFrameDisplayFlag", BOOLEAN, nullable=False), + 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", + Column("ItemId", INTEGER, nullable=False, primary_key=True), + Column("ItemTypeId", INTEGER, nullable=False), + Column("Name", VARCHAR(255), nullable=False), + Column("Name_en", VARCHAR(255)), + Column("Rarity", INTEGER, nullable=False), + Column("Value", INTEGER, nullable=False), + Column("PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("PropertyValue1Min", INTEGER, nullable=False), + Column("PropertyValue1Max", INTEGER, nullable=False), + Column("PropertyValue2Min", INTEGER, nullable=False), + Column("PropertyValue2Max", INTEGER, nullable=False), + Column("FlavorText", VARCHAR(255), nullable=False), + Column("FlavorText_en", VARCHAR(255)), + Column("SalePrice", INTEGER, nullable=False), + Column("ItemIcon", VARCHAR(255), nullable=False), + mysql_charset="utf8mb4" ) support = Table( @@ -93,7 +205,7 @@ support = Table( UniqueConstraint( "version", "supportLogId", name="sao_static_support_log_list_uk" ), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) rare_drop = Table( @@ -107,7 +219,7 @@ rare_drop = Table( UniqueConstraint( "version", "questRareDropId", "commonRewardId", name="sao_static_rare_drop_list_uk" ), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" ) title = Table( @@ -124,91 +236,361 @@ title = Table( UniqueConstraint( "version", "titleId", name="sao_static_title_list_uk" ), - mysql_charset="utf8mb4", + mysql_charset="utf8mb4" +) + +reward = Table( + "sao_static_reward", + metadata, + Column("RewardTableId", BIGINT, primary_key=True, nullable=False), + Column("RewardTableSubId", INTEGER, nullable=False), + Column("UnanalyzedLogGradeId", INTEGER, nullable=False), + Column("CommonRewardType", INTEGER, nullable=False), + Column("CommonRewardId", INTEGER, nullable=False), + Column("CommonRewardNum", INTEGER, nullable=False), + Column("StrengthMin", INTEGER, nullable=False), + Column("StrengthMax", INTEGER, nullable=False), + Column("PropertyTableSubId", INTEGER, nullable=False), + Column("QuestInfoDisplayFlag", BOOLEAN, nullable=False), + Column("Rate", INTEGER, nullable=False), + mysql_charset="utf8mb4" +) + +ex_bonus = Table( + "sao_static_ex_bonus", + metadata, + Column("ExBonusTableId", BIGINT, primary_key=True, nullable=False), + Column("ExBonusTableSubId", INTEGER, nullable=False), + Column("ExBonusConditionId", INTEGER, nullable=False), + Column("ConditionValue1", INTEGER, nullable=False), + Column("ConditionValue2", INTEGER, nullable=False), + Column("CommonRewardType", INTEGER, nullable=False), + Column("CommonRewardId", INTEGER, nullable=False), + Column("CommonRewardNum", INTEGER, nullable=False), + Column("Strength", INTEGER, nullable=False), + Column("Property1PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property1Value1", INTEGER, nullable=False), + Column("Property1Value2", INTEGER, nullable=False), + Column("Property2PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property2Value1", INTEGER, nullable=False), + Column("Property2Value2", INTEGER, nullable=False), + Column("Property3PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property3Value1", INTEGER, nullable=False), + Column("Property3Value2", INTEGER, nullable=False), + Column("Property4PropertyId", BIGINT, ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("Property4Value1", INTEGER, nullable=False), + Column("Property4Value2", INTEGER, nullable=False), + mysql_charset="utf8mb4" +) + +episode = Table( + "sao_static_episode", + metadata, + Column("EpisodeId", BIGINT, nullable=False, primary_key=True), + Column("EpisodeChapterId", INTEGER, nullable=False), + Column("ReleaseEpisodeId", INTEGER, nullable=False), + Column("Title", VARCHAR(255), nullable=False), + Column("CommentSummary", VARCHAR(255), nullable=False), + Column("ExBonusTableSubId", INTEGER, nullable=False), + Column("QuestSceneId", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade")), + mysql_charset="utf8mb4" +) + +tower = Table( + "sao_static_tower", + metadata, + Column("TowerId", BIGINT, nullable=False, primary_key=True), + Column("ReleaseTowerId", INTEGER, nullable=False), + Column("ExBonusTableSubId", INTEGER, nullable=False), + Column("QuestSceneId", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade"), nullable=False), + mysql_charset="utf8mb4" +) + +ex_tower = Table( + "sao_static_ex_tower", + metadata, + Column("ExTowerQuestId", BIGINT, nullable=False, primary_key=True), + Column("ExTowerId", INTEGER, nullable=False), + Column("ReleaseExTowerQuestId", INTEGER, nullable=False), + Column("Title", VARCHAR(255), nullable=False), + Column("Title_en", VARCHAR(255)), + Column("ExBonusTableSubId", INTEGER, nullable=False), + Column("QuestSceneId", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade"), nullable=False), + mysql_charset="utf8mb4" +) + +side_quest = Table( + "sao_static_side_quest", + metadata, + Column("SideQuestId", BIGINT, nullable=False, unique=True, primary_key=True), + Column("DisplayName", VARCHAR(255), nullable=False), + Column("DisplayName_en", VARCHAR(255)), + Column("EpisodeNum", INTEGER, nullable=False), + Column("ExBonusTableSubId", INTEGER, nullable=False), + Column("QuestSceneId", BIGINT, ForeignKey("sao_static_quest.QuestSceneId", ondelete="cascade", onupdate="cascade"), nullable=False), + mysql_charset="utf8mb4" ) class SaoStaticData(BaseData): - async def put_quest( self, questSceneId: int, version: int, sortNo: int, name: str, enabled: bool ) -> Optional[int]: + async def put_quest(self, data: Dict[str, str]) -> Optional[int]: sql = insert(quest).values( - questSceneId=questSceneId, - version=version, - sortNo=sortNo, - name=name, - enabled=enabled, + QuestSceneId=data["QuestSceneId"], + SortNo=data["SortNo"], + Tutorial=data["Tutorial"], + ColRate=data["ColRate"], + LimitDefault=data["LimitDefault"], + LimitResurrection=data["LimitResurrection"], + RewardTableSubId=data["RewardTableSubId"], + PlayerTraceTableSubId=data["PlayerTraceTableSubId"], + SuccessPlayerExp=data["SuccessPlayerExp"], + FailedPlayerExp=data["FailedPlayerExp"], + PairExpRate=data["PairExpRate"], + TrioExpRate=data["TrioExpRate"], + SingleRewardVp=data["SingleRewardVp"], + PairRewardVp=data["PairRewardVp"], + TrioRewardVp=data["TrioRewardVp"], ) - conflict = sql.on_duplicate_key_update( - name=name, questSceneId=questSceneId, version=version - ) + conflict = sql.on_duplicate_key_update(QuestSceneId=data["QuestSceneId"]) result = await self.execute(conflict) if result is None: return None - return result.lastrowid + return data["QuestSceneId"] - async def put_hero( self, version: int, heroLogId: int, name: str, nickname: str, rarity: int, skillTableSubId: int, awakeningExp: int, flavorText: str, enabled: bool ) -> Optional[int]: + async def put_hero(self, data: Dict[str, str]) -> Optional[int]: sql = insert(hero).values( - version=version, - heroLogId=heroLogId, - name=name, - nickname=nickname, - rarity=rarity, - skillTableSubId=skillTableSubId, - awakeningExp=awakeningExp, - flavorText=flavorText, - enabled=enabled + HeroLogId=data["HeroLogId"], + CharaId=data["CharaId"], + Name=data["Name"], + Nickname=data["Nickname"], + Rarity=data["Rarity"], + WeaponTypeId=data["WeaponTypeId"], + HeroLogRoleId=data["HeroLogRoleId"], + CostumeTypeId=data["CostumeTypeId"], + UnitId=data["UnitId"], + DefaultEquipmentId1=data["DefaultEquipmentId1"], + DefaultEquipmentId2=data["DefaultEquipmentId2"], + SkillTableSubId=data["SkillTableSubId"], + HpMin=data["HpMin"], + HpMax=data["HpMax"], + StrMin=data["StrMin"], + StrMax=data["StrMax"], + VitMin=data["VitMin"], + VitMax=data["VitMax"], + IntMin=data["IntMin"], + IntMax=data["IntMax"], + Property1PropertyId=data["Property1PropertyId"], + Property1Value1=data["Property1Value1"], + Property1Value2=data["Property1Value2"], + Property2PropertyId=data["Property2PropertyId"], + Property2Value1=data["Property2Value1"], + Property2Value2=data["Property2Value2"], + Property3PropertyId=data["Property3PropertyId"], + Property3Value1=data["Property3Value1"], + Property3Value2=data["Property3Value2"], + Property4PropertyId=data["Property4PropertyId"], + Property4Value1=data["Property4Value1"], + Property4Value2=data["Property4Value2"], + FlavorText=data["FlavorText"], + SalePrice=data["SalePrice"], + CompositionExp=data["CompositionExp"], + AwakeningExp=data["AwakeningExp"], + Slot4UnlockLevel=data["Slot4UnlockLevel"], + Slot5UnlockLevel=data["Slot5UnlockLevel"], + CollectionEmptyFrameDisplayFlag=data["CollectionEmptyFrameDisplayFlag"], ) - conflict = sql.on_duplicate_key_update( - name=name, heroLogId=heroLogId - ) + conflict = sql.on_duplicate_key_update(HeroLogId=data["HeroLogId"]) result = await self.execute(conflict) if result is None: return None - return result.lastrowid + return data["HeroLogId"] - async def put_equipment( self, version: int, equipmentId: int, name: str, equipmentType: int, weaponTypeId:int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + async def put_hero_translation(self, hero_id: int, name_en: str, nickname_en: str, flavor_text_en: str) -> None: + result = await self.execute(hero.update(hero.c.HeroLogId == hero_id).values( + Name=name_en, + Nickname=nickname_en, + FlavorText=flavor_text_en, + )) + + result = await self.execute(result) + if result is None: + self.logger.error(f"Failed to add translated text for hero {hero_id}") + + async def put_equipment(self, data: Dict[str, str]) -> Optional[int]: sql = insert(equipment).values( - version=version, - equipmentId=equipmentId, - name=name, - equipmentType=equipmentType, - weaponTypeId=weaponTypeId, - rarity=rarity, - flavorText=flavorText, - enabled=enabled + EquipmentId=data["EquipmentId"], + EquipmentType=data["EquipmentType"], + WeaponTypeId=data["WeaponTypeId"], + Name=data["Name"], + Rarity=data["Rarity"], + Power=data["Power"], + StrengthIncrement=data["StrengthIncrement"], + SkillCondition=data["SkillCondition"], + Property1PropertyId=data["Property1PropertyId"], + Property1Value1=data["Property1Value1"], + Property1Value2=data["Property1Value2"], + Property2PropertyId=data["Property2PropertyId"], + Property2Value1=data["Property2Value1"], + Property2Value2=data["Property2Value2"], + Property3PropertyId=data["Property3PropertyId"], + Property3Value1=data["Property3Value1"], + Property3Value2=data["Property3Value2"], + Property4PropertyId=data["Property4PropertyId"], + Property4Value1=data["Property4Value1"], + Property4Value2=data["Property4Value2"], + SalePrice=data["SalePrice"], + CompositionExp=data["CompositionExp"], + AwakeningExp=data["AwakeningExp"], + FlavorText=data["FlavorText"], ) - conflict = sql.on_duplicate_key_update( - name=name, equipmentId=equipmentId - ) + conflict = sql.on_duplicate_key_update(EquipmentId=data["EquipmentId"]) result = await self.execute(conflict) if result is None: return None - return result.lastrowid + return data["EquipmentId"] - async def put_item( self, version: int, itemId: int, name: str, itemTypeId: int, rarity: int, flavorText: str, enabled: bool ) -> Optional[int]: + async def put_equipment_translation(self, equip_id: int, name_en: str, flavor_text_en: str) -> None: + result = await self.execute(equipment.update(equipment.c.EquipmentId == equip_id).values( + Name_en=name_en, + FlavorText_en=flavor_text_en, + )) + + result = await self.execute(result) + if result is None: + self.logger.error(f"Failed to add translated text for equipment {equip_id}") + + async def put_item(self, data: Dict[str, str]) -> Optional[int]: sql = insert(item).values( - version=version, - itemId=itemId, - name=name, - itemTypeId=itemTypeId, - rarity=rarity, - flavorText=flavorText, - enabled=enabled + ItemId=data["ItemId"], + ItemTypeId=data["ItemTypeId"], + Name=data["Name"], + Rarity=data["Rarity"], + Value=data["Value"], + PropertyId=data["PropertyId"], + PropertyValue1Min=data["PropertyValue1Min"], + PropertyValue1Max=data["PropertyValue1Max"], + PropertyValue2Min=data["PropertyValue2Min"], + PropertyValue2Max=data["PropertyValue2Max"], + FlavorText=data["FlavorText"], + SalePrice=data["SalePrice"], + ItemIcon=data["ItemIcon"], + ) - conflict = sql.on_duplicate_key_update( - name=name, itemId=itemId - ) + conflict = sql.on_duplicate_key_update(ItemId=data["ItemId"]) result = await self.execute(conflict) if result is None: return None - return result.lastrowid + return data["ItemId"] + + async def put_item_translation(self, item_id: int, name_en: str, flavor_text_en: str) -> None: + result = await self.execute(item.update(item.c.ItemId == item_id).values( + Name_en=name_en, + FlavorText_en=flavor_text_en, + )) + + result = await self.execute(result) + if result is None: + self.logger.error(f"Failed to add translated text for item {item_id}") + + async def put_property(self, data: Dict[str, str]) -> None: + sql = insert(prop).values( + PropertyId=data["PropertyId"], + PropertyTargetType=data["PropertyTargetType"], + PropertyName=data["PropertyName"], + PropertyNameFormat=data["PropertyNameFormat"], + PropertyTypeId=data["PropertyTypeId"], + Value1Min=data["Value1Min"], + Value1Max=data["Value1Max"], + Value2Min=data["Value2Min"], + Value2Max=data["Value2Max"], + ) + + conflict = sql.on_duplicate_key_update(PropertyId=data["PropertyId"]) + + result = await self.execute(conflict) + if result is None: + return None + return data["PropertyId"] + async def put_property_translation(self, property_id: int, name_en: str, name_fmt: str) -> None: + result = await self.execute(prop.update(prop.c.PropertyId == property_id).values( + Name_en=name_en, + PropertyNameFormat_en=name_fmt, + )) + + result = await self.execute(result) + if result is None: + self.logger.error(f"Failed to add translated text for property {property_id}") + + async def put_skill(self, data: Dict[str, str]) -> None: + sql = insert(skill).values( + SkillId=data["SkillId"], + WeaponTypeId=data["WeaponTypeId"], + Name=data["Name"], + Attack=data["Attack"], + Passive=data["Passive"], + Pet=data["Pet"], + Level=data["Level"], + SkillCondition=data["SkillCondition"], + CoolTime=data["CoolTime"], + SkillIcon=data["SkillIcon"], + FriendSkillIcon=data["FriendSkillIcon"], + InfoText=data["InfoText"], + ) + + conflict = sql.on_duplicate_key_update(SkillId=data["SkillId"]) + + result = await self.execute(conflict) + if result is None: + return None + return data["SkillId"] + + async def put_skill_translation(self, skill_id: int, name_en: str, info_txt: str) -> None: + result = await self.execute(skill.update(skill.c.SkillId == skill_id).values( + Name_en=name_en, + InfoText_en=info_txt, + )) + + result = await self.execute(result) + if result is None: + self.logger.error(f"Failed to add translated text for skill {skill_id}") + + async def put_skill_table(self, skill_id: int, sub_id: int, level: int, awakening: int, table_id: int) -> None: + sql = insert(skill_table).values( + SkillTableId=table_id, + SkillId=skill_id, + SkillTableSubId=sub_id, + LevelObtained=level, + AwakeningId=awakening, + ) + conflict = sql.on_duplicate_key_update(SkillTableId=table_id) + result = await self.execute(conflict) + if result is None: + self.logger.error(f"Failed to add skill table {skill_id}") + + async def put_player_trace(self, data: Dict[str, str]) -> None: + sql = insert(player_trace).values( + PlayerTraceTableId=data["PlayerTraceTableId"], + PlayerTraceTableSubId=data["PlayerTraceTableSubId"], + CommonRewardType=data["CommonRewardType"], + CommonRewardId=data["CommonRewardId"], + CommonRewardNum=data["CommonRewardNum"], + Rate=data["Rate"], + ) + + conflict = sql.on_duplicate_key_update(PlayerTraceTableId=data["PlayerTraceTableId"]) + result = await self.execute(conflict) + if result is None: + return None + return data["PlayerTraceTableId"] + async 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, @@ -221,14 +603,12 @@ class SaoStaticData(BaseData): enabled=enabled ) - conflict = sql.on_duplicate_key_update( - name=name, supportLogId=supportLogId - ) + conflict = sql.on_duplicate_key_update(version=version) result = await self.execute(conflict) - if result is None: - return None - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert support log {supportLogId}!") async def put_rare_drop( self, version: int, questRareDropId: int, commonRewardId: int, enabled: bool ) -> Optional[int]: sql = insert(rare_drop).values( @@ -238,14 +618,12 @@ class SaoStaticData(BaseData): enabled=enabled, ) - conflict = sql.on_duplicate_key_update( - questRareDropId=questRareDropId, commonRewardId=commonRewardId, version=version - ) + conflict = sql.on_duplicate_key_update(version=version) result = await self.execute(conflict) - if result is None: - return None - return result.lastrowid + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert rare drop {questRareDropId}!") async def put_title( self, version: int, titleId: int, displayName: str, requirement: int, rank: int, imageFilePath: str, enabled: bool ) -> Optional[int]: sql = insert(title).values( @@ -258,71 +636,141 @@ class SaoStaticData(BaseData): enabled=enabled ) - conflict = sql.on_duplicate_key_update( - displayName=displayName, titleId=titleId + conflict = sql.on_duplicate_key_update(version=version) + + result = await self.execute(conflict) + if result: + return result.inserted_primary_key['id'] + self.logger.error(f"Failed to insert title {titleId}") + + async def put_reward_table(self, data: Dict[str, Union[str, bool]]) -> None: + sql = insert(reward).values( + **data ) + conflict = sql.on_duplicate_key_update(**data) + result = await self.execute(conflict) if result is None: return None - return result.lastrowid + return data['RewardTableId'] + + async def put_ex_bonus(self, data: Dict[str, int]) -> None: + sql = insert(ex_bonus).values( + **data + ) - async def get_quests_id(self, sortNo: int) -> Optional[Dict]: - sql = quest.select(quest.c.sortNo == sortNo) - - result = await self.execute(sql) + conflict = sql.on_duplicate_key_update(**data) + + result = await self.execute(conflict) + if result is None: + return None + return data['ExBonusTableId'] + + async def put_episode(self, data: Dict[str, int]) -> None: + sql = insert(episode).values( + EpisodeId=data['EpisodeId'], + EpisodeChapterId=data['EpisodeChapterId'], + ReleaseEpisodeId=data['ReleaseEpisodeId'], + Title=data['Title'], + CommentSummary=data['CommentSummary'], + ExBonusTableSubId=data['ExBonusTableSubId'], + QuestSceneId=data['QuestSceneId'] if data['QuestSceneId'] > 0 else None, + ) + + conflict = sql.on_duplicate_key_update(EpisodeId=data['EpisodeId']) + + result = await self.execute(conflict) + if result is None: + return None + return data['EpisodeId'] + + async def put_tower(self, data: Dict[str, int]) -> None: + sql = insert(tower).values( + TowerId=data['TrialTowerId'], + ReleaseTowerId=data['ReleaseTrialTowerId'], + ExBonusTableSubId=data['ExBonusTableSubId'], + QuestSceneId=data['QuestSceneId'], + ) + + conflict = sql.on_duplicate_key_update(TowerId=data['TrialTowerId']) + + result = await self.execute(conflict) + if result is None: + return None + return data['TrialTowerId'] + + async def put_ex_tower(self, data: Dict[str, int]) -> None: + sql = insert(ex_tower).values( + ExTowerQuestId=data['ExTowerQuestId'], + ExTowerId=data['ExTowerId'], + ReleaseExTowerQuestId=data['ReleaseExTowerQuestId'], + Title=data['Title'], + ExBonusTableSubId=data['ExBonusTableSubId'], + QuestSceneId=data['QuestSceneId'], + ) + + conflict = sql.on_duplicate_key_update(ExTowerQuestId=data['ExTowerQuestId']) + + result = await self.execute(conflict) + if result is None: + return None + return data['ExTowerQuestId'] + + async def put_side_quest(self, data: Dict[str, int]) -> None: + sql = insert(side_quest).values( + SideQuestId=data['SideQuestId'], + DisplayName=data['DisplayName'], + EpisodeNum=data['EpisodeNum'], + ExBonusTableSubId=data['ExBonusTableSubId'], + QuestSceneId=data['QuestSceneId'], + ) + + conflict = sql.on_duplicate_key_update(SideQuestId=data['SideQuestId']) + + result = await self.execute(conflict) + if result is None: + return None + return data['SideQuestId'] + + async def get_quest_by_id(self, quest_scene_id: int) -> Optional[Row]: + result = await self.execute(quest.select(quest.c.QuestSceneId == quest_scene_id)) if result is None: return None return result.fetchone() - async 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 = await self.execute(sql) + async def get_quests(self) -> Optional[List[Row]]: + result = await self.execute(quest.select()) if result is None: return None - return [list[2] for list in result.fetchall()] + return result.fetchall() - async def get_hero_id(self, heroLogId: int) -> Optional[Dict]: - sql = hero.select(hero.c.heroLogId == heroLogId) - - result = await self.execute(sql) + async def get_hero_by_id(self, heroLogId: int) -> Optional[Row]: + result = await self.execute(hero.select(hero.c.HeroLogId == heroLogId)) if result is None: return None return result.fetchone() - async 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 = await self.execute(sql) + async def get_heros(self) -> Optional[List[Row]]: + result = await self.execute(hero.select()) if result is None: return None - return [list[2] for list in result.fetchall()] + return result.fetchall() - async def get_equipment_id(self, equipmentId: int) -> Optional[Dict]: - sql = equipment.select(equipment.c.equipmentId == equipmentId) - - result = await self.execute(sql) + async def get_equipment_by_id(self, equipmentId: int) -> Optional[Dict]: + result = await self.execute(equipment.select(equipment.c.EquipmentId == equipmentId)) if result is None: return None return result.fetchone() - async 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 = await self.execute(sql) + async def get_equipment(self) -> Optional[List[Dict]]: + result = await self.execute(equipment.select()) if result is None: return None - return [list[2] for list in result.fetchall()] + return result.fetchall() - async def get_item_id(self, itemId: int) -> Optional[Dict]: - sql = item.select(item.c.itemId == itemId) + async def get_item_id(self, item_id: int) -> Optional[Dict]: + sql = item.select(item.c.ItemId == item_id) result = await self.execute(sql) if result is None: @@ -347,6 +795,12 @@ class SaoStaticData(BaseData): return None return [list[2] for list in result.fetchall()] + async def get_support_logs(self) -> Optional[List[Dict]]: + result = await self.execute(support.select()) + if result is None: + return None + return result.fetchall() + async 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() @@ -365,4 +819,156 @@ class SaoStaticData(BaseData): result = await self.execute(sql) if result is None: return None - return [list[2] for list in result.fetchall()] \ No newline at end of file + return [list[2] for list in result.fetchall()] + + async def get_reward_by_table(self, table_id: int) -> Optional[Row]: + result = await self.execute(reward.select(reward.c.RewardTableId == table_id)) + if result is None: + return None + return result.fetchone() + + async def get_rewards_by_subtable(self, subtable_id: int) -> Optional[List[Row]]: + result = await self.execute(reward.select(reward.c.RewardTableSubId == subtable_id)) + if result is None: + return None + return result.fetchall() + + async def get_rewards(self) -> Optional[List[Row]]: + result = await self.execute(reward.select()) + if result is None: + return None + return result.fetchall() + + async def get_ex_bonus_by_table(self, table_id: int) -> Optional[Row]: + result = await self.execute(ex_bonus.select(ex_bonus.c.ExBonusTableId == table_id)) + if result is None: + return None + return result.fetchone() + + async def get_ex_bonuses_by_subtable(self, subtable_id: int) -> Optional[List[Row]]: + result = await self.execute(ex_bonus.select(ex_bonus.c.ExBonusTableSubId == subtable_id)) + if result is None: + return None + return result.fetchall() + + async def get_ex_bonuses(self) -> Optional[List[Row]]: + result = await self.execute(ex_bonus.select()) + if result is None: + return None + return result.fetchall() + + async def get_episode_by_id(self, episode_id: int) -> Optional[Row]: + result = await self.execute(episode.select(episode.c.EpisodeId == episode_id)) + if result is None: + return None + return result.fetchone() + + async def get_episode_by_quest_id(self, quest_id: int) -> Optional[Row]: + result = await self.execute(episode.select(episode.c.QuestSceneId == quest_id)) + if result is None: + return None + return result.fetchone() + + async def get_episodes(self) -> Optional[List[Row]]: + result = await self.execute(episode.select()) + if result is None: + return None + return result.fetchall() + + async def get_tower_by_id(self, tower_id: int) -> Optional[Row]: + result = await self.execute(tower.select(tower.c.TowerId == tower_id)) + if result is None: + return None + return result.fetchone() + + async def get_tower_by_quest_id(self, quest_id: int) -> Optional[Row]: + result = await self.execute(tower.select(tower.c.QuestSceneId == quest_id)) + if result is None: + return None + return result.fetchone() + + async def get_towers(self) -> Optional[List[Row]]: + result = await self.execute(tower.select()) + if result is None: + return None + return result.fetchall() + + async def get_ex_tower_by_id(self, ex_tower_id: int) -> Optional[Row]: + result = await self.execute(ex_tower.select(ex_tower.c.ExTowerQuestId == ex_tower_id)) + if result is None: + return None + return result.fetchone() + + async def get_ex_tower_by_quest_id(self, quest_id: int) -> Optional[Row]: + result = await self.execute(ex_tower.select(ex_tower.c.QuestSceneId == quest_id)) + if result is None: + return None + return result.fetchone() + + async def get_ex_towers(self) -> Optional[List[Row]]: + result = await self.execute(ex_tower.select()) + if result is None: + return None + return result.fetchall() + + async def get_ex_towers_by_tower(self, ex_tower_id: int) -> Optional[List[Row]]: + result = await self.execute(ex_tower.select(ex_tower.c.ExTowerId == ex_tower_id)) + if result is None: + return None + return result.fetchall() + + async def get_side_quest_by_id(self, side_quest_id: int) -> Optional[Row]: + result = await self.execute(side_quest.select(side_quest.c.SideQuestId == side_quest_id)) + if result is None: + return None + return result.fetchone() + + async def get_side_quest_by_quest_id(self, quest_id: int) -> Optional[Row]: + result = await self.execute(side_quest.select(side_quest.c.QuestSceneId == quest_id)) + if result is None: + return None + return result.fetchone() + + async def get_side_quests(self) -> Optional[List[Row]]: + result = await self.execute(side_quest.select()) + if result is None: + return None + return result.fetchall() + + async def get_side_quests_by_episode(self, episode_num: int) -> Optional[List[Row]]: + result = await self.execute(side_quest.select(side_quest.c.EpisodeNum == episode_num)) + if result is None: + return None + return result.fetchall() + + async def get_player_trace_by_id(self, trace_id: int) -> Optional[Row]: + result = await self.execute(player_trace.select(player_trace.c.PlayerTraceTableId == trace_id)) + if result is None: + return None + return result.fetchone() + + async def get_player_trace_by_subid(self, trace_sub_id: int) -> Optional[List[Row]]: + result = await self.execute(player_trace.select(player_trace.c.PlayerTraceTableSubId == trace_sub_id)) + if result is None: + return None + return result.fetchall() + + async def get_player_traces(self) -> Optional[List[Row]]: + result = await self.execute(player_trace.select()) + if result is None: + return None + return result.fetchall() + + async def get_skill_table_by_subid(self, table_sub_id: int) -> Optional[List[Row]]: + result = await self.execute(skill_table.select(skill_table.c.SkillTableSubId == table_sub_id)) + + if result is None: + return None + return result.fetchall() + + async def get_skill_by_id(self, skill_id: int) -> Optional[Row]: + result = await self.execute(skill.select(skill.c.SkillId == skill_id)) + + if result is None: + return None + return result.fetchone() diff --git a/titles/sao/templates/sao_index.jinja b/titles/sao/templates/sao_index.jinja new file mode 100644 index 0000000..35d42ce --- /dev/null +++ b/titles/sao/templates/sao_index.jinja @@ -0,0 +1,182 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Sword Art Online Arcade

+{% if profile is defined and profile is not none and profile.id > 0 %} + + + +

Profile for {{ profile.nick_name }} 

+{% include "core/templates/widgets/err_banner.jinja" %} +{% include "core/templates/widgets/succ_banner.jinja" %} + +

Register Hero Card

+{% if all_heros is defined and all_heros|length > 0%} +If you have cards printed on the official network, they must be registered here in order to work with the game.
+Only hero cards are supported at this time. If a card was registered incorrectly, contact a sysadmin.
+
+ +

+ +{% else %} +Card registration is not set up on this server. Please contact a sysadmin. +{% endif %} +{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %} + +No profile information found for this account. + + +{% else %} +Login to view profile information. +{% endif %} +{% endblock content %} \ No newline at end of file diff --git a/titles/wacca/base.py b/titles/wacca/base.py index 8a37c2b..58b1ba9 100644 --- a/titles/wacca/base.py +++ b/titles/wacca/base.py @@ -833,14 +833,14 @@ class WaccaBase: # TODO: Coop and vs data async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict: coop_info = data["params"][4] - return self.handle_user_music_update_request(data) + return await self.handle_user_music_update_request(data) async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict: vs_info = data["params"][4] - return self.handle_user_music_update_request(data) + return await self.handle_user_music_update_request(data) async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict: - return self.handle_user_music_update_request(data) + return await self.handle_user_music_update_request(data) async def handle_user_mission_update_request(self, data: Dict) -> Dict: req = UserMissionUpdateRequest(data)